Является Ли Этот Локатор Служб Таким Анти-Шаблоном?

В отрасли существует устойчивый консенсус в отношении того, что Поиск сервисов является антипаттерном.

Из вики:

Стоит отметить, что в некоторых случаях локатор сервисов на самом деле является антишаблоном.

В этом посте я рассматриваю случай, когда, по моему мнению, Service Locator не является антипаттерном.

Вот что пишут в Интернете Локатор :

Некоторые считают Service Locator антишаблоном.

Это нарушает принцип инверсии зависимостей ( Принцип инверсии зависимостей ) из набора принципов ТВЕРДЫЙ .

Локатор служб скрывает зависимости данного класса вместо того, чтобы делиться ими, как в случае с шаблоном внедрения зависимостей ( Внедрение зависимости ).

Если эти зависимости изменятся, мы рискуем нарушить функциональность классов, которые их используют, что затрудняет поддержку системы.

Service Locator настолько тесно связан с DI, что некоторые авторы (Марк Зееманн, Стивен ван Дёрсен) специально предупреждать :
Service Locator — опасный шаблон, поскольку он почти работает. .

Есть только одна область, где Service Locator не справляется, и к этому не следует относиться легкомысленно.

То есть Локатор чертовски хорош и работает почти как надо, но есть один момент, который все портит. А вот и он:
Основная проблема с Поиск сервисов это влияние возможности повторного использования классов, использующих его.

Это проявляется двумя способами: * Класс тянется по Поиск сервисов как излишний Зависимость .

* Класс делает неочевидным, что это такое Зависимости являются.

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

Другими словами, именно так благословляется создание объектов и внедрение в них зависимостей:

  
  
  
  
  
   

public function __construct(IDep1 $dep1, IDep2 $dep2, IDep3 $dep3) { $this->dep1 = $dep1; $this->dep2 = $dep2; $this->dep3 = $dep3; }

а вот так - нет:

public function __construct(ILocator $locator) { $this->locator = $locator; $this->dep1 = $locator->get(IDep1::class); $this->dep2 = $locator->get(IDep2::class); $this->dep3 = $locator->get(IDep3::class); }

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

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

Если требуется зависимость, всегда лучше использовать внедрение в конструктор.

Если мы перепишем наш класс Locator следующим образом:

public function __construct(ILocator $locator = null) { if ($locator) { $this->dep1 = $locator->get(IDep1::class); } } public function setDep1(IDep1 $dep1) { $this->dep1 = $dep1; }

затем: а) делаем его независимым от наличия Локатора (например, в тестовой среде), б) явно выделяем зависимости в сеттерах (можно также аннотировать, документировать, префиксить и решать проблему «неочевидности» зависимостей любым другим доступным способом, вплоть до Ctrl+F на клавише " $locator-> получить "в коде).

Теперь мы подошли к тому моменту, когда, на мой взгляд, использование Latitude оправдано.

В комментариях к статье " В чем основная разница между внедрением зависимостей и локатором сервисов? " коллега @symbix резюмировал тему статьи следующим образом:

SL работает по принципу извлечения: конструктор «вытягивает» свои зависимости из контейнера.

DI работает по принципу push: контейнер передает свои зависимости конструктору.

То есть, по сути, контейнер DI-объектов можно использовать еще и как Service Locator:

// push deps into constructor public function __construct(IDep1 $dep1, IDep2 $dep2, IDep3 $dep3) {} // pull deps from constructor public function __construct(IContainer $container) { if ($container) { $this->dep1 = $container->get(IDep1::class); $this->dep2 = $container->get(IDep2::class); $this->dep3 = $container->get(IDep3::class); } }

Как мы отметили выше, первый способ считается приемлемым, второй — антипаттерном.

Но к чему приводит использование первого метода в промышленных масштабах? Более того, чтобы запустить приложение, при его создании мы должны внедрить в конструктор приложения его зависимости, а в их конструкторы — зависимости зависимостей и т. д. по всей иерархии.

То есть для того, чтобы объект приложения хотя бы был создан, мы должны создать все зависимости, упомянутые во всех конструкторах в цепочке, даже если мы просто собираемся получить справку о параметрах запуска приложения в консольном режиме.

«Антишаблон» Service Locator позволяет нам «вытягивать» нужные нам зависимости из контейнера при доступе к ним:

class App { /** @var \IContainer */ private $container; /** @var \IDep1 */ private $dep1; public function __construct(IContainer $container = null) { $this->container = $container; } private function initDep1() { if (!$this->dep1) { $this->dep1 = $this->container->get(IDep1::class); } return $this->dep1; } public function run() { $dep1 = $this->initDep1(); } public function setDep1(IDep1 $dep1) { $this->dep1 = $dep1; } }

Итак, приведенный выше код:

  • можно использовать без контейнера в конструкторе благодаря возможности внедрения зависимостей через сеттер (например, в тестах);
  • зависимости явно описываются через набор частных методов с префиксом

    init

    ;
  • иерархия зависимостей не расширяется при создании экземпляра данного класса, а создается по мере его использования.

В данном случае использования паттерн Service Locator вызывает у меня положительные эмоции и не вызывает отрицательных.

Ну, за небольшим исключением — при внедрении зависимостей в конструктор (режим push) DI-контейнер знает, для какого класса создаются зависимости, и может реализовать разные реализации одного и того же интерфейса на основе внутренних инструкций.

В режиме «pull» у контейнера нет информации, для кого он создает зависимости, ее нужно предоставить:

$this->dep1 = $this->container->get(IDep1::class, self::class);

В этой версии Service Locator становится «шаблоном» без каких-либо «анти».



Послесловие

В комментариях к публикации благодарность коллегам @lair И @Максклуб пришли к выводу, что проблема отложенного внедрения зависимостей при создании объектов решается в рамках парадигмы DI, если соответствующий язык программирования поддерживает дженерики или проксирование.

В случае PHP, в котором нет дженериков, требуется дополнительная генерация кода (автоматически — github.com/Ocramius/ProxyManager или вручную).

Таким образом, предлагаемое решение (реализация DI-контейнера в качестве Service Locator) все еще имеет свою нишу — проекты на языках без дженериков и проксирования, в которых дополнительная генерация кода нежелательна.

Но в подавляющем большинстве случаев лучше использовать «чистый» DI. Теги: #программирование #php #ИТ-стандарты #Идеальный код #шаблоны #шаблоны #антипаттерны #антипаттерны #мысли вслух #локатор сервисов

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.