А такой ли уж анти-паттерн этот Service Locator?

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

В индустрии сложилось устойчивое мнение, что Service Locator является анти-паттерном. Из wiki:

Стоит заметить, что в некотором случае локатор служб фактически является анти-шаблоном.

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

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

Некоторые считают Локатор Служб анти-паттерном. Он нарушает принцип инверсии зависимостей (Dependency Inversion principle) из набора принципов SOLID. Локатор Служб скрывает зависимости данного класса вместо их совместного использования, как в случае шаблона Внедрение Зависимости (Dependency Injection). В случае изменения данных зависимостей мы рискуем сломать функционал классов, которые их используют, вследствие чего затрудняется поддержка системы.

Service Locator идёт рука об руку с DI настолько близко, что некоторые авторы (Mark Seemann, Steven van Deursen) специально предупреждают:

Service Locator is a dangerous pattern because it almost works. ... There’s only one area where Service Locator falls short, and that shouldn’t be taken lightly.

Т.е., Локатор чертовски хорош и работает почти как надо, но есть один момент, который всё портит. Вот он:

The main problem with Service Locator’s the impact of reusability of the classes consuming it. This manifests itself in two ways:

* The class drags along the Service Locator as a redundant Dependency.

* The class makes it non-obvious what its Dependencies are.

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

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

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);
}

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

Property Injection should only be used when the class you’re developing has a good Local Default, and you still want to enable callers to provide different implementations of the class’s Dependency. It’s important to note that Property Injection is best used when the Dependency is optional. If the Dependency is required, Constructor Injection is always a better pick.

Если мы перепишем наш класс с Локатором в таком виде:

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

public function setDep1(IDep1 $dep1)
{
    $this->dep1 = $dep1;
}

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

Вот мы и подошли к тому моменту, когда, на мой взгляд, использование Локатора оправдано. В комментах к статье "Какое главное отличие Dependency Injection от Service Locator?" коллега @symbix резюмировал тему статьи так:

SL работает по принципу pull: конструктор "вытягивает" из контейнера свои зависимости.

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;
    }

}

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

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

  • зависимости явно описываются через набор private-методов с префиксом init;

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

В таком варианте использования паттерн Service Locator вызывает во мне положительные эмоции и не вызывает отрицательных. Ну если только за малым исключением - при внедрении зависимостей в конструктор (режим "push") DI-контейнер знает, для какого класса создаются зависимости и может внедрять различные имплементации одного и того же интерфейса на основании внутренних инструкций. В режиме "pull" у контейнера нет информации для кого он создаёт зависимости, нужно её дать:

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

Вот в таком варианте Service Locator становится очень даже "pattern" без всяких "anti".

Источник: https://habr.com/ru/post/539622/


Интересные статьи

Интересные статьи

Опыт использования новых маков с М1 начинает расставлять точки над i. Эти чипы быстрые. Очень быстрые. Но почему? В чем магия? Я смотрел видео на Youtube, где автор купил...
Площадку, открывшую миру десятки тысяч блогеров, журналистов, разработчиков, ученых, режиссеров и контент-мейкеров за последний год не критиковал только ленивый. Для резк...
Нередко при работе с Bitrix24 REST API возникает необходимость быстро получить содержимое определенных полей всех элементов какого-то списка (например, лидов). Традиционн...
Устали от постоянных звонков через Zoom и соскучились по живому общению? Как известно, современные проблемы требуют современных решений. Американский стартап PORTL Hologram предлагает сде...
Всем привет! Не так давно на работе в рамках тестирования нового бизнес-процесса мне понадобилась возможность авторизации под разными пользователями. Переход в соответствующий р...