Какой шаблон проектирования использовать при написании функции PHP, которая обрабатывает выходные данные другой функции? - PullRequest
0 голосов
/ 16 января 2020

У меня есть две функции: myFunctionA() и myFunctionB().

myFunctionA() возвращает Object, который включает ключ Page_Type, который имеет строковое значение.

myFunctionB() обрабатывает количество записей в Object, возвращаемых myFunctionA(), включая Page_Type и его строковое значение.

Позже myFunctionA() обновляется, поэтому он больше не возвращает объект, включающий ключ Page_Type но вместо этого ключ Page_Types - который имеет значение массива.

Из-за этого myFunctionB() теперь будет также необходимо обновить - больше не будет обрабатывать Page_Type, которая является строкой, но Page_Types, которая является массивом.

Если я правильно понимаю (а могу и не понимать), приведенный выше пример Dependency Request и обширного рефакторинга, который он выбрасывает, можно избежать (я думаю) развертыванием шаблона Dependency Injection (или, возможно, даже Service Locator pattern?) вместо этого (??)

Но, несмотря на чтение этой темы, я все еще не уверен в том, как njection может работать в PHP или Javascript функциях (большая часть объяснения касается таких языков программирования, как C++ и OOP, таких как Classes, тогда как я имею дело со сторонними функциями в PHP и javascript).

Есть ли любой способ структурировать мой код так, чтобы обновление myFunctionA() (любым существенным образом) не потребовало от меня также обновления myFunctionB() (и * 1044) * все остальные функции, вызывающие myFunctionA() - например, myFunctionC(), myFunctionD(), myFunctionE() et c.)?

А что, если myFunctionH() требует myFunctionG() требует myFunctionF() что требует myFunctionA()? Я не хочу находиться в положении, когда обновление myFunctionA() теперь означает, что все три функции (F, G и H) должны быть обновлены.


Попытка при ответе:

Лучший ответ, который я могу придумать в настоящее время - и это может быть не ответ наилучшей практики , потому что я пока не знаю, существует ли формальная проблема, которая соответствует с проблемой, которую я описываю в приведенном выше вопросе, - это следующая переформулировка того, как я представил установку:

У меня есть две (неизменяемые) функции: myFunctionA__v1_0() и myFunctionB__v1_0().

myFunctionA__v1_0() возвращает объект, который содержит ключ Page_Type со строковым значением.

myFunctionB__v1_0() обрабатывает количество записей в объекте, возвращаемых myFunctionA__v1_0(), включая Page_Type и его строковое значение.

Позже, myFunctionA__v1_0() все еще существует, но ему также следует myFunctionA__v2_0(), который возвращает объект, включающий ключ Page_Types - который имеет значение массива.

В порядке для myFunctionB для доступа к объекту, возвращенному myFunctionA__v2_0(), будет Теперь я также должен быть myFunctionB__v1_1(), способным обрабатывать массив Page_Types.

Это можно суммировать как:

  • myFunctionB__v1_0() требуется возвращаемый объект на myFunctionA__v1_0()
  • myFunctionB__v1_1() требуется объект, возвращаемый myFunctionA__v2_0()

Поскольку каждая функция становится неизменной после формального имени, что никогда случается в том, что мы в конечном итоге приводим пример myFunctionB__v1_0(), требующий объект, возвращаемый myFunctionA__v2_0().

Я не знаю, правильно ли я подхожу к этому, но это лучший подход До сих пор я придумал.

Ответы [ 4 ]

1 голос
/ 20 января 2020

Очень часто в программировании для провайдера - ie. myFunctionA(), чтобы ничего не знать о его потребителе (ах) myFunctionB(). только правильный способ справиться с этим - определить API заранее и никогда не изменить его;)

Я не вижу цели создания версий потребитель - эта причина должна быть "ниже по течению" myFunctionB() - ie. потребитель из myFunctionB(), что автор myFunctionB() не контролирует ... в этом случае myFunctionB() сам становится провайдером , и автор будет иметь справиться с этим (возможно, используя тот же шаблон, что и вы) ... Но это не ваша проблема для решения.

Что касается вашего провайдера myFunctionA(): Если вы не можете заранее определить интерфейс / API для самих данных - ie. вы знаете, что структура данных должна будет измениться (обратно-совместимым образом), но вы не знаете, как ... тогда вам понадобится версия что-то так или иначе .

Вы намного опережаете большинство, так как вы видите это грядущее и планируете его с самого начала.

Единственный способ избежать внести изменения в потребителя myFunctionB() в какой-то момент, значит сделать все изменения в поставщике myFunctionA() обратно совместимым способом. Изменения, которые вы описываете, не являются обратно совместимыми, потому что myFunctionB() не может знать, что делать с новым выводом из myFunctionA() без изменения.

Предлагаемое вами решение звучит так, как будто оно должно работать. Однако есть как минимум пара недостатков:

  • Это требует от вас постоянно растущего списка устаревших функций на случай, если когда-нибудь кто-нибудь запросит их данные. Это станет очень сложным в обслуживании и, вероятно, будет невозможно в долгосрочной перспективе.
  • В зависимости от того, какие изменения необходимо внести в будущем, больше не будет возможности вообще выводить данные для myFunctionA__v1_0() - in В вашем примере вы добавляете возможность нескольких page_types в вашей системе - в этом случае вы, вероятно, можете просто переписать v1_0, чтобы использовать первый, и унаследованные потребители будут счастливы. Но если вы решите полностью удалить концепцию page_types из вашей системы, вам придется планировать полное удаление v1_0 так или иначе. Таким образом, вам нужно установить sh способ сообщить об этом потребителям.

Единственный правильный способ справиться с этим - определить API заранее и никогда изменить это.

Поскольку мы установили, что:

  1. вам придется вносить обратно несовместимые изменения
  2. вы ничего не знаете о потребители и вы не можете изменять их при необходимости

Я предлагаю вместо определения неизменяемого API для ваших данных вы определяете неизменный API, который позволяет вам общаться с потребителями, когда они должны или должны обновить.

Это может показаться сложным, но это не обязательно должно быть:

Примите параметр версии в provider :

Идея состоит в том, чтобы позволить потребителю явно сообщить поставщику, какую версию возвращать.

Поставщик может выглядеть следующим образом:

function myFunctionA(string $version) {
    $page_types = ['TypeA', 'TypeB'];
    $page = new stdClass();
    $page->title = 'Page title';
    switch ($version) {
        case '1.0':
            $page->error = 'Version 1.0 no longer available. Please upgrade!';
            break;
        case '1.1':
            $page->page_type = $page_types[0];
            $page->warning = 'Deprecated version. Please upgrade!';
            break;
        case '2.0':
            $page->page_types = $page_types;
            break;
        default:
            $page->error = 'Unknown version: ' . $version;
            break;
    }
    return $page;
}

Таким образом, поставщик принимает параметр, который будет содержит версию, понятную потребителю - обычно ту, которая была самой новой, когда потребитель последний раз обновлялся.

Поставщик делает все возможное для доставки запрошенной версии

  • если это невозможно, существует «договор» для информирования потребителя ($page->error будет существовать для возвращаемого значения)
  • , если возможно , но есть доступна более новая версия, имеется другой «контракт» для информирования потребителя об этом ($page->warning будет существовать в возвращаемом значении).

И обрабатывать несколько случаев у потребителя )

Потребитель должен отправить версию, которую он ожидает, в качестве параметра.

function myFunctionB() {
    //The consumer tells the provider which version it wants:
    $page = myFunctionA('2.0');
    if ($page->error) {
        //Notify developers and throw an error
        pseudo_notify_devs($page->error);
        throw new Exception($page->error);
    } else if ($page->warning) {
        //Notify developers
        pseudo_notify_devs($page->warning);
    }
    do_stuff_with($page);
}

Вторая строка старой версии myFunctionB() - или совсем другого потребителя myFunctionC() может вместо этого запросить старая версия:

$page = myFunctionA('1.1');

Это позволяет вам вносить обратно совместимые изменения в любое время, когда вам не нужно ничего делать. Вы можете сделать все возможное, чтобы по-прежнему поддерживать старые версии, когда это возможно, обеспечивая «постепенное» ухудшение в старых потребителях .

Когда вам действительно нужно сделать критические изменения Вы можете продолжить поддерживать старую версию, прежде чем окончательно полностью ее удалить.

Мета-информация

Я не уверен, что это будет полезно ... но вы могли бы добавить некоторую метаинформацию для потребителей, используя устаревшую версию:

function myFunctionA(string $version) {
    # [...]
    if ($page->error || $page->warning) {
        $page->meta = [
            'current_version' => '3.0',
            'API_docs' => 'http://some-url.fake'
        ]
    }
    return $page;
}

Затем ее можно использовать для потребителя:

pseudo_notify_devs(
    $page->error .
    ' - Newest version: ' . $page->meta['current_version'] .
    ' - Docs: ' . $page->meta['API_docs']
);

... на вашем месте я был бы осторожен не переусердствуйте, хотя ... Всегда KISS

1 голос
/ 16 января 2020

Кажется, вы нарушили интерфейс между myFunctionA и myFunctionB, изменив тип возвращаемого значения со строки на массив. Я не думаю, что я могу помочь.

1 голос
/ 20 января 2020

Во-первых, это не внедрение зависимости.

Ваш myFunctionA() может быть вызван производителем, так как он предоставляет данные, это должно быть подтверждено Data Structure. Ваш myFunctionB() может быть вызван потребителем, так как он потребляет данные, предоставленные myFunctionA.

Таким образом, чтобы ваши Производители и ваш Потребитель работали независимо, вам нужно добавить еще один слой между ними, звоните Converter. Слой Converter преобразует Data Structure, предоставленный Producer, в хорошо известный Data Structure, который можно понять с помощью Consumer

Я очень рекомендую вам прочитать книгу Clean Code Глава 6: Объекты и структуры данных. Таким образом, вы сможете полностью понять концепцию выше, о Data Structure

Пример

Предположим, что у нас было Data Structure call Hand, у нас было свойство правой и левой руки.

class Hand {
    private $rightHand;
    private $leftHand
    // Add Constructor, getter and setter
}

myFunctionA() предоставит объект Hand, Hand - это Data Structure

function myFunctionA() {
    $hand = Hand::createHand(); //Assume a function to create new Hand object
    return $hand;
}

пусть говорят, что у нас есть еще Data Structure, вызов Leg, Нога будет возможность использовать myFunctionB ();

class Leg {
    private $rightLeg;
    private $leftLeg
    // Add Constructor, getter and setter
}

Затем нам нужно иметь конвертер, посередине, чтобы конвертировать из руки в ногу, и использовать на myFunctionB()

class Converter {
    public static function convertFromHandToLeg($hand) {
        $leg = makeFromHand($hand); //Assume a method to convert from Hand to Leg
        return $leg; 
    }
}
myFunctionB(Converter::convertFromHandToLeg($hand))

Итак, всякий раз, когда вы редактируете ответ myFunctionA(), подразумеваете, что вы собираетесь редактировать Data Structure из Hand. Вам нужно только отредактировать Converter, чтобы убедиться, что он продолжает правильно конвертировать из Hand в Leg. Вам не нужно нажимать myFunctionB и наоборот.

Это будет очень полезно, когда у вас будет другой Producer, который даст Hand, как вы упомянули в своем вопросе, myFunctionC(), myFunctionD() ... И у вас также было много других Consumer, которые будет потреблять Leg как myFunctionH(), myFunctionG() ...

Надеюсь, эта помощь

1 голос
/ 16 января 2020

Внедрение зависимостей более актуально в контексте OOP. Тем не менее, главное, что я хотел бы сделать, это перестать думать о возврате того, что у вас есть, и начать думать о том, как эти два метода работают вместе и каков их контракт.

Выясните, что является логическим выводом для myFunctionA (), закодируйте этот контракт в объект и преобразуйте имеющиеся у вас данные в этот формат. Таким образом, даже если способ извлечения данных в myFunctionA () изменится, вам нужно будет обновить только это преобразование.

Пока вы придерживаетесь этого контракта (который может быть представлен через пользовательский объект), myFunctionB () и других методов, которые ожидают получения данных в соответствии с контрактом, вам не придется менять эти методы дольше.

Поэтому мой главный вывод здесь - начать думать о нужных вам данных и передавать их не в той структуре, в которой вы их получаете, а в том смысле, в котором это наиболее целесообразно для вашего приложения.

...