---
title: "А антипаттерн ли Service Locator?"
description: "Service Locator (как и его эволюция — DI-контейнер) позволяет на этапе выполнения кода связывать в приложении различные элементы (функции и экземпляры классов) со своими зависимост"
date: 2021-01-28
---

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

Нужно помнить, что работающее приложение — это совсем не то же самое,
что код, описывающий работу приложения. Т.е., тот факт, что явная
сильная статическая (*implicit strong static*) типизация позволяет
отлавливать ошибки связывания элементов на уровне компиляции кода,
отнюдь не говорит о том, что на этапе выполнения приложения подобных
ошибок не возникнет. Элементы могут подходить друг другу по интерфейсам,
но не по контексту (дверь от шкафа-купе в кузов E46 BMW). Интерфейсы и
контекст — вот основа для связывания элементов в коде.

С интерфейсами более-менее понятно — это то, что доступно программисту
при визуальной проверке кода (а также компилятору/интерпретатору/IDE/…).
Вот фрагмент PHP-кода:

``` php
function main($dep) {
    $res = $dep(12, 'str');
    echo $res;
}

$fn = function ($num, $str) {
    return $num . $str;
};

main($fn);
```

По коду видно, что функция `main` ожидает в качестве входного аргумента
некоторую другую функцию, которой на вход можно передать два аргумента и
которая возвращает некоторое значение. То, что принимает на вход функция
и что возвращает — это и есть её интерфейс. Заголовочные файлы C/C++
являются примером описания интерфейса функций —
[string.h](https://github.com/openbsd/src/blob/master/include/string.h)

С объектами посложнее, но смысл остаётся тот же самый, интерфейс — это
правила (`IDep`) взаимодействия двух элементов, описывающие, какие
свойства и методы зависимого элемента (`$dep`) желает использовать в
своих целях основной элемент (`$app`):

``` php
interface IDep {
    public function get();
    public function put($data);
}

class Dep implements IDep {
    private $data;

    public function get() { return $this->data; }
    public function put($data) { $this->data = $data; }
}

class Main {
    public function run(IDep $dep) {
        $dep->put(4);
        echo $dep->get();
    }
}

$app = new Main();
$dep = new Dep();
$app->run($dep);
```

Если на вход методу `run()` объекта `$app` подаётся объект `$dep`,
имплементирующий ожидаемый интерфейс (`IDep`), то ни один компилятор не
ругнётся, если `$app` — это E46 кузов BMW, а `$dep` — это дверь для
шкафа-купе.

За соответствие различных различных элементов кода не только по
интерфейсам, но и по смыслу (контексту) отвечают специальные элементы
приложения — фабрики, строители, пулы и т.п. (см. “[Порождающие шаблоны
проектирования](https://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D1%80%D0%BE%D0%B6%D0%B4%D0%B0%D1%8E%D1%89%D0%B8%D0%B5_%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD%D1%8B_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F)”).
Именно они создают и соединяют элементы так, чтобы не возникало
противоречий между отдельными частями единого композита.

[Внедрение
зависимостей](https://ru.wikipedia.org/wiki/%D0%92%D0%BD%D0%B5%D0%B4%D1%80%D0%B5%D0%BD%D0%B8%D0%B5_%D0%B7%D0%B0%D0%B2%D0%B8%D1%81%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D0%B8)
— это такая техника создания элементов приложения, когда при разработке
отдельной функции/класса программист не заботится о том, каким образом
создаётся зависимость, необходимая для работы базового элемента. Он
просто декларирует интерфейсы требуемых зависимостей в ожидании, что
внешняя среда предоставит именно то, что нужно в соответствии с текущим
контекстом. Вопросы порождения элементов и их связывания выносятся на
уровень DI-контейнера. Который, кстати, может использоваться и как
Service Locator:

``` php
// Constructor injection
public function __construct(IDep $dep) {
    $this->dep = $dep;
}
```

Или другой вариант конструктора:

``` php
// Resolve through the container
public function __construct(IContainer $di) {
    $this->dep = $di->get(IDep::class);
}
```

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

``` php
$this->dep = $di->get(IDep::class, self::class);
```

В чём же
заключаются претензии к Service Locator’у? Основная — в том, что
[локатор скрывает зависимости между элементами
приложения](https://designpatternsphp.readthedocs.io/en/latest/More/ServiceLocator/README.html):

> Service Locator hides class’ dependencies instead of exposing them as
> you would do using the Dependency Injection. In case of changes of
> those dependencies you risk to break the functionality of classes
> which are using them, making your system difficult to maintain.

Типовой пример использования Service Locator’а при упоминании его в
качестве
[анти-паттерна](https://freecontent.manning.com/the-service-locator-anti-pattern/):

``` csharp
public class HomeController : Controller {
    public HomeController() { }

    public ViewResult Index() {
        IProductService service = Locator.GetService<IProductService>();
        var products = service.GetFeaturedProducts();
        return this.View(products);
    }
}
```

Можно ли считать сокрытием зависимости использование локатора в таком
виде — `Locator.GetService<IProductService>()`? Насколько я знаю
возможности современных IDE, то подобное использование даже внутри
метода или функции будет обнаружено средствами IDE и будет учтено при
рефакторинге.

Да, я согласен, что внедрение зависимостей через конструктор при
создании объекта обладает большей наглядностью:

``` csharp
public HomeController(IProductService service) { }
```

Но внедрение
зависимостей в конструкторе привносит другую проблему — чтобы запустить
приложение, контейнер должен создать полное дерево зависимостей, даже
если какие-то из них не используются в данном режиме работы. Внедрение в
конструкторе только контейнера и использование контейнера для создания
зависимостей по ходу работы приложения решает эту проблему, но является,
по мнению многих, анти-паттерном “Service Locator”:

``` php
class Main {
    private $locator;

    public function __construct(ILocator $locator) {
        $this->locator = $locator;
    }

    public function run() {
        $dep = $this->locator->get(IDep::class, self::class);
    }
}
```

[Подобный
подход](https://manningbooks.medium.com/the-service-locator-anti-pattern-58e4f708eee0)
снижает возможность переиспользования кода из-за избыточной зависимости
(от `ILocator`) и непрозрачности используемых зависимостей:

> 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.

С первым пунктом ничего не поделаешь — код действительно можно
использовать только лишь внутри приложения, в котором определён сам
локатор (`ILocator`), а вот со вторым вариантом можно справиться
организационными мерами (например, извлекать все зависимости в методах с
префиксом `dep`):

``` php
class Main {
    private $locator;

    public function __construct(ILocator $locator) {
        $this->locator = $locator;
    }

    private function depIDep() {
        return $this->locator->get(IDep::class, self::class);
    }

    public function run() {
        $dep = $this->depIDep();
    }
}
```

По большому счёту этот подход мало чем отличается от внедрения
зависимостей через setter’ы или свойства. Вот только создание
зависимостей происходит не при создании базового объекта, а при
обращении базового объекта к зависимостям.

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