Pull to refresh

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

PHPProgrammingPerfect codeIT Standards

В индустрии сложилось устойчивое мнение, что 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".

Послесловие

В комментах к публикации благодаря коллегам @lair и @Maksclubпришёл к выводам, что проблема отложенного внедрения зависимостей при создании объектов решается в рамках DI-парадигмы, если соответствующий язык программирования поддерживает generic'и или проксирование. В случае с PHP, в котором generic'и отсутствуют, необходима дополнительная кодогенерация (автоматом - github.com/Ocramius/ProxyManager, или вручную).

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

Tags:service locatorpatternsantipatternsмысли вслух
Hubs: PHP Programming Perfect code IT Standards
Total votes 28: ↑16 and ↓12 +4
Views4.2K

Comments 26

Only those users with full accounts are able to leave comments. Log in, please.

Popular right now

PHP developer (symfony, highload service)
to 150,000 ₽ВсеИнструменты.руRemote job
IT Recruiter
from 50,000 ₽Red LabRemote job
IT Recruiter
from 30,000 ₽Digital NomadsТомск
Стажер IT рекрутер
from 40,000 ₽IT and DigitalRemote job
IT Recruiter
from 800 to 1,700 $Tonti Laguna MobileRemote job