Ха, какая изящная маскировка Service Locator-а под DI. Даже может показаться, что это DI! :-)
Это первый коммент к моей предыдущей публикации "Dependency Injection, JavaScript и ES6-модули". Спасибо коллеге symbix 'у за этот коммент, т.к. именно он стало причиной погружения в тонкости отличия одного от другого. Под катом мой ответ на вопрос в заголовке.
(КДПВ особого смысла не имеет и предназначена в первую очередь для визуального опознания этой публикации в ряду других)
Для начала разберёмся с определениями (часть примеров кода я буду приводить на PHP, часть на JS, потому что эти два языка в настоящее время находятся в моём активном багаже, а часть на любом другом, потому что я спёр эти примеры из интернета).
Dependency Injection
DI же в двух словах — вместо require/import модуля вы инжектируете зависимость через параметр конструктора (или сеттер свойства). То есть за этим громким словом стоит простое «передавайте зависимости класса через параметры конструктора».
Этот коммент коллеги risedphantom 'а довольно точно передает суть явления. Чтобы облегчить понимание кода разработчиком все зависимости описываются в явном виде — в виде параметров конструктора (обычно, но не всегда):
class MainClass
{
public function __construct($dep1, $dep2, $dep3) {}
}
Всё. DI — он именно об этом. Нужные нам зависимости предоставляются кем-то там. А где этот "кто-то там" их берёт — DI это не волнует.
При анализе кода важно понимать, что именно "внутреннее" для него (и что мы можем смело менять), а что приходит/уходит за границу ответственности данного кода. Вот это волнует DI. Почему зависимости, в основном, передаются через конструктор, а не в setter'ах? Потому что разработчику так удобнее — он сразу видит все зависимости анализируемого кода в одном месте. Мы привыкли считать, что DI — это что-то на уровне классов, но параметры функции/метода — это ведь тоже DI:
function f(dep1, dep2, dep3) {}
Конструктор — это просто такой особый метод среди всех других.
Service Locator
Локатор служб — широко известный анти-шаблон. А чем он занимается? Предоставляет доступ одним объектам к другим объектам. Вот типичный интерфейс такого локатора:
interface ServiceLocator
{
public function addInstance(string $class, Service $service);
public function addClass(string $class, array $params);
public function has(string $interface): bool;
public function get(string $class): Service;
}
Локатор представляет собой контейнер, в который можно положить готовые объекты (или задать правила их создания) и затем получить доступ к этим объектам. Локатору, по большому счёту, всё равно, каким образом в нём оказываются объекты — созданы они извне явным образом и помещены в контейнер или созданы самим контейнером согласно заданным правилам.
Локатор служб отвечает за хранение объектов и предоставление к ним доступа. Всё.
Dependency Injection Container
Что такое DI-контейнер? Согласно вольного пересказа содержимого "Dependency Injection Principles, Practices, and Patterns": "a software library that that provides DI functionality and allows automating many of the tasks involved in Object Composition, Interception, and Lifetime Management. DI Containers are also known as Inversion of Control (IoC) Containers. (§3.2.2)"
Т.е., DI-контейнер в первую очередь отвечает за создание объектов, а лишь во вторую — за их хранение. В DI-контейнере может вообще не хранится ни одного созданного объекта, если само приложение не нуждается в объектах, общих для всего приложения (типа конфигурации или логгера).
Вот, например, интерфейс DI-контейнера Symfony:
interface ContainerInterface extends PsrContainerInterface
{
public function set(string $id, ?object $service);
public function get($id, int $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE);
public function has($id);
public function initialized(string $id);
public function getParameter(string $name);
public function hasParameter(string $name);
public function setParameter(string $name, $value);
}
При необходимости можно очень легко убедить себя, что интерфейс DI-контейнера очень похож на интерфейс Локатора Служб — те же самые get
, has
и set
/add
.
Чем же плох Service Locator?
А ничем. Плох не сам шаблон, а то, каким образом он иногда используется. В частности, есть такое мнение, что "Service Locator нарушает инкапсуляцию в статически типизированных языках, потому что этот паттерн нечётко выражает предусловия". И пример нарушения:
public class OrderProcessor : IOrderProcessor
{
public void Process(Order order)
{
var validator = Locator.Resolve<IOrderValidator>();
if (validator.Validate(order))
{
var shipper = Locator.Resolve<IOrderShipper>();
shipper.Ship(order);
}
}
}
Типа, вот так плохо, а хорошо — вот так:
public class OrderProcessor : IOrderProcessor
{
public OrderProcessor(IOrderValidator validator, IOrderShipper shipper)
public void Process(Order order)
}
Эй, да мы же просто вывели за скобки момент создания самих объектов с интерфейсами IOrderValidator
и IOrderShipper
! Вполне возможно, что в этом приложении где-то в другом месте есть примерно такой код:
var validator = Locator.Resolve<IOrderValidator>();
var shipper = Locator.Resolve<IOrderShipper>();
var processor = new OrderProcessor(validator, shipper);
processor.Process(order);
Composition Root
Говоря о Внедрении Зависимостей мы не можем не прийти к такому понятию, как Composition Root (далее я буду назвать это "Точка Сборки"). Это тот самый "кто-то", кому делегированы обязанности по созданию зависимостей и их внедрению.
В Точке Сборки все зависимости явно определяются, соответствующие объекты создаются и внедряются туда, где их ждут. Здесь различия между Локатором Служб и DI-контейнером минимальны. И тот и другой позволяют создавать новые объекты, хранить созданные объекты, извлекать хранимые объекты. Я бы даже взялся утверждать, что в этом месте принципиальных различий между ними нет.
Главное отличие
А где же различия между DI-контейнером и контейнером Локатора Служб наиболее явные? В любом другом месте, а особенно при статическом доступе к контейнеру:
$obj = Container::getInstance()->get($objId);
Вот оно. В таком виде в любом месте (если только это не Точка Сборки, но и там использование статического доступа весьма сомнительно) любой контейнер становится анти-паттерном с именем Service Locator.
Коллега VolCh кратко и ёмко ответил на мой вопрос:
А чем, по вашему, true DI отличается от Service Locator, замаскированного под DI?
так:
Доступом к контейнеру
По сути вся эта публикация всего лишь более детальное развёртывание вот этого его ответа.
Легальное применение Контейнера
Таким образом, является ли Контейнер DI-контейнером или контейнером Локатора Служб, очень сильно зависит от того, где и каким образом мы его используем. Как я уже сказал выше, в Точке Сборки разница между типами контейнеров исчезает. Но что мы передаём, если контейнер сам является зависимостью для какого-либо класса?
class Foo {
public function __construct(Container $container){}
}
Опять-таки, все зависит от того, каким образом мы используем контейнер. Вот два примера, один из которых точно соответствует контейнеру Локатора Служб и является анти-паттерном, а другой имеет право на жизнь при определённых условиях.
class Foo
{
private $dep;
public function __construct(Container $container)
{
$this->dep = $container->get('depId');
}
}
class Foo
{
private $container;
public function __construct(Container $container)
{
$this->container = $container;
}
public function initModules($modules) {
foreach ($modules as $initClassId) {
$init= $this->container->get($initClassId);
$init->exec();
}
}
}
Резюме
На самом деле всё несколько сложнее, DI-контейнер должен обладать более широким функционалом, чем Локатор Служб, но суть этой публикации в том, что даже самый навороченный DI-контейнер лекго и просто можно использовать, как Локатор Служб, а вот чтобы Локатор Служб использовать как DI-контейнер — нужно постараться.
P.S.
не могу не дать ссылки на блог "Programming stuff" Сергея Теплякова — уж очень там хорошо эти вопросы освещены.
Автор: flancer