А антипаттерн ли Service Locator?
Дата публикации: 2021-01-28Service Locator (как и его эволюция — DI-контейнер) позволяет на этапе выполнения кода связывать в приложении различные элементы (функции и экземпляры классов) со своими зависимостями (другими функциями и экземплярами классов).
Нужно помнить, что работающее приложение — это совсем не то же самое, что код, описывающий работу приложения. Т.е., тот факт, что явная сильная статическая (implicit strong static) типизация позволяет отлавливать ошибки связывания элементов на уровне компиляции кода, отнюдь не говорит о том, что на этапе выполнения приложения подобных ошибок не возникнет. Элементы могут подходить друг другу по интерфейсам, но не по контексту (дверь от шкафа-купе в кузов E46 BMW). Интерфейсы и контекст — вот основа для связывания элементов в коде.
С интерфейсами более-менее понятно — это то, что доступно программисту при визуальной проверке кода (а также компилятору/интерпретатору/IDE/…). Вот фрагмент PHP-кода:
function main($dep)
{
$res = $dep(12, ‘str’);
echo $res;
} fn = function(num, $str) {
return $num . $str;
};
main($fn);
По коду видно, что функция main ожидает в качестве входного аргумента некоторую другую функцию, которой на вход можно передать два аргумента и которая возвращает некоторое значение. То, что принимает на вход функция и что возвращает — это и есть её интерфейс. Заголовочные файлы C/C++ являются примером описания интерфейса функций — string.h
С объектами посложнее, но смысл остаётся тот же самый, интерфейс — это правила (IDep) взаимодействия двух элементов, описывающие, какие свойства и методы зависимого элемента ($dep) желает использовать в своих целях основной элемент ($app):
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 — это дверь для шкафа-купе.
За соответствие различных различных элементов кода не только по интерфейсам, но и по смыслу (контексту) отвечают специальные элементы приложения — фабрики, строители, пулы и т.п. (см. “Порождающие шаблоны проектирования”). Именно они создают и соединяют элементы так, чтобы не возникало противоречий между отдельными частями единого композита.
Внедрение зависимостей — это такая техника создания элементов приложения, когда при разработке отдельной функции/класса программист не заботится о том, каким образом создаётся зависимость, необходимая для работы базового элемента. Он просто декларирует интерфейсы требуемых зависимостей в ожидании, что внешняя среда предоставит именно то, что нужно в соответствии с текущим контекстом. Вопросы порождения элементов и их связывания выносятся на уровень DI-контейнера. Который, кстати, может использоваться и как Service Locator:
// DI in constructor public function __construct(IDep $dep)
{
$this->dep = $dep;
}// DI as ServiceLocator public function __construct(IContainer $di)
{
$this->dep = $di->get(IDep::class);
}
Во втором случае мы теряем контекст — у контейнера нет информации, для какого класса создаётся зависимость. Лучше было бы использовать его в таком виде:
$this->dep = $di->get(IDep::class, self::class); В чём же заключаются претензии к Service Locator’у? Основная — в том, что локатор скрывает зависимости между элементами приложения:
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’а при упоминании его в качестве анти-паттерна:
public class HomeController : Controller
{
public HomeController() { } public ViewResult Index()
{
IProductService service =
Locator.GetService(); var products = service.GetFeaturedProducts(); return this.View(products); } }
Можно ли считать сокрытием зависимости использование локатора в таком виде — Locator.GetService<IProductService>()? Насколько я знаю возможности современных IDE, то подобное использование даже внутри метода или функции будет обнаружено средствами IDE и будет учтено при рефакторинге.
Да, я согласен, что внедрение зависимостей через конструктор при создании объекта обладает большей наглядностью:
public HomeController(IProductService service) { } Но внедрение зависимостей в конструкторе привносит другую проблему — чтобы запустить приложение, контейнер должен создать полное дерево зависимостей, даже если какие-то из них не используются в данном режиме работы. Внедрение в конструкторе только контейнера и использование контейнера для создания зависимостей по ходу работы приложения решает эту проблему, но является, по мнению многих, анти-паттерном “Service Locator”:
class Main
{
private $locator; public function __construct(ILocator $locator)
{
$this->locator = $locator;
}
public function run()
{
$dep = $this->locator->get(IDep::class, self::class);
} }
Подобный подход снижает возможность переиспользования кода из-за избыточной зависимости (от 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):
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) и создавать зависимости не при создании базового объекта, а по мере необходимости.