Pull to refresh

Comments 25

Но что мы передаём, если контейнер сам является зависимостью для какого-либо класса?

В этом случае проблемой является зависимость от контейнера без действительной на то необходмости.


Классу Foo в примере номер 1 незачем знать о контейнере, ведь ему на самом деле нужен Dep, а не Container. Если мы хотим использовать Foo с другой зависимостью, которую передадим явно, нам придется изгаляться с созданием нового контейнера на пустом месте.


Иногда это действительно нужно — скажем, для реализации абстрактной фабрики, или в вашем втором примере. Там зависимость от контейнера обоснована.


В общем, тут все верно, я только уточнил :-)


Composition Root, "в этом месте принципиальных различий между ними нет."

Важное отличие в том, где мы определяем, какая конкретная реализация соответствует какому идентификатору зависимости (идентификатором может быть интерфейс, абстрактный класс, символ, константа — не важно). Качественный DI позволяет сопоставить разные реализации одному и тому же идентификатору для разных потребителей. Пример (с вымышленным DI на вымышленном языке):


interface ICacher
{
     get(key: string): any;
     put(key: string, value: any): void;
}

class RedisCacher implements ICacher { ... }
class MemcachedCacher implements ICacher { ... }

class DocumentRepository
{
    constructor(cacher: ICacher) { ... }
}

class SessionRepository
{
    constructor(cacher: ICacher) { ... }
}

// DI config
container.whenCreating(DocumentRepository).willUse([ICacher => RedisCacher]);
container.whenCreating(SessionRepository).willUse([ICacher => SessionCacher]);

С сервис-локатором так сделать не получится.

А, еще — почему я за 2 секунды отнес вашу реализацию к SL, а не к DI?


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


Изящность я увидел в том, что процесс pull-а вынесен из самого конструктора на уровнь синтаксического сахара JS — и с точки зрения самого конструктора вроде бы даже и push. Но, тем не менее, контейнер остается не в курсе о конкретном потребителе, и не может принять решение о том, что именно отдать для данного идентификатора, соответствие идентификатора реализации предопределено в виде жесткой связи. Но такая жесткая связь присуща и некоторым простейшим примитивным реализациям DI. Без рефлексии ослабить эту связь вряд ли получится, так что при изначальной постановке задачи — обойтись средствами JS — это, наверное, максимально возможное приближение к DI.

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

Я тут подумал, что можно сделать "чистый DI", который бы push'ил зависимости в функцию-конструктор. Нужно при анализе прокси-спецификации в конструкторе делать на одно прерывание больше, после чего, обладая всей полнотой информации по зависимостям, push'ить их в конструктор без прокси-объекта (в виде обычного JS-объекта).


Подобная реализация контейнера всё ещё будет относится вами к Service Locator'ам? ;)

Надо вникать в код, но навскидку вроде нормальный DI получится. Главное рекурсивный резолвинг не сломать — чтобы и для резолвинга прокси где надо были, и в конструкторы без проксей, в чистом виде прилетало.

Я ссылки на код дал в ключевые места — на прерывание процесса конструирования объекта при обнаружении незнакомой зависимости и на вызов функции-конструктора с прокси-объектом для анализа зависимостей. Вполне возможно изменить код так, чтобы после получения информации по всем зависимостям и их созданию вызов функции-конструктора происходил классическим способом через new с передачей в качестве входного аргумента простого spec-объекта с зависимостями, а не прокси-объекта.

Принципиальная разница DI и SL в моменте создания зависимостей.
В DI при создании сервиса создаются и зависимости (если не созданы ранее).
В SL в общем случае при создании сервиса зависимости не создаются. Создание зависимости происходит только при необходимости, в методе get. Это позволяет здорово сэкономить время/память/другие ресурсы.
SL используется, когда нужно использовать один из множества однотипных альтернативных сервисов. Например, когда нужно отправить сообщение пользователю по одному из множества возможных каналов: email, sms, slack, telegram, etc.
> а часть на любом другом, потому что я спёр эти примеры из интернета

Можно было бы перевести примеры к одному языку, возможно бы это позволило понять лучше разницу между DI и SL.

> Легальное применение Контейнера

Оба примера используют принцип Dependency Injection и паттерн Service Locator. От того что get в конструкторе или дергается по запросу сути не мееняет.

> Чем же плох Service Locator?

Плох он ровным счетом тем же чем плохи глобальные переменные. Куча объектов зависят от одного и того же локаторы который возвращает всем один и те же сервисы. Тем самым меняя сервис для одного компонента, велика вероятность сломать что-то другое. Именно поэтому статический доступ к контейнеру, является ничем иным как Service Locator.

Разница DI и SL в том что один это принцип отвязывания объекта (функции) от зависимостей, а SL паттерн при котором зависимости объекта сводятся к одному единственному объекту, который ответственнен за другие зависимости. Хоть SL и считается антипатерном (см. аналогию с использованием глобальных переменных), в некоторых случаях является самым подходящим, например по причине упомянутой коментом выше — «ленивое» создание сервисов по запросу.
Ну для меня основная причина применять SL вместо DI это грубо говоря возможность получить список объектов(имплементаций одного интерфейса) и выбрать «подходящий» в зависимости от каких-то динамически меняющихся факторов. Но не сказал бы что такое очень часто бывает нужнo и обычно это можно решить более глубокой специализацией этих самых интерфейсов.

П.С. А «ленивую загрузку» кстати поддерживают и DI-фреймворки. Не все, и не все одинаково хорошо и удобно, но в целом если хочешь «lazy», то не обязательно нужен SL.
UFO just landed and posted this here
DI — это паттерн решения проблемы. SL — конкретный подход для реализации этого паттерна.

Не всегда. Зависит от того, что скрывается за аббревиатурой DI. Если это dependency inversion, то всё так. Если же речь про dependency injection, то нет. В общем виде ситуация следующая:
1. Есть IoC (Inversion of Control)
2. Есть DI (dependency inversion) — частный случай IoC
3. Есть ещё один DI (dependency injection) — решение «проблемы» DI (dependency inversion). SL — это тоже решение «проблемы» DI.

Да, термины подобраны не очень удачно. Но вот тут Мартин Фаулер хорошо поясняет
UFO just landed and posted this here
UFO just landed and posted this here
Если это dependency inversion

Это DIP, а не DI.

Так, давайте один раз все проясним.


Последняя буковка в SOLID — это Dependency Inversion Principle, сокращается как DIP. Для реализации этого принципа нет никакой необходимости в библиотеке, ручная передача зависимостей в конструктор — это уже достаточно, это соблюдение принципа.


Dependency Injection (DI) — это одновременно и обобщенный паттерн (реализация DIP методом инъекций без указания, как конкретно это реализуется), и библиотека (или иное инфраструктурное решение) для автоматизации и упрощения реализации DIP (тут обычно есть уточнение, Dependency Injection Library/Framework/...). Контейнер там или нет — не важно, это уже детали реализации (скажем, вместо контейнера может быть декларативная конфигурация, или вообще какой-нибудь препроцессор, работающий в compile time, почему бы и нет).


Service Locator сюда прямого отношения не имеет, поскольку не обязан реализовывать DIP. Service Locator, реализующий DIP — это уже DI.

UFO just landed and posted this here

SL, не реализующий SIP — это практически любой SL (иначе бы его называли DI). Как Yii::app() в Yii, скажем.

UFO just landed and posted this here

Мы, видимо, по-разному понимаем, что такое DIP.


По определению Роберта Мартина:


  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

В Yii я кругом вижу жесткие связки. (Хотя, допускаю, что на Yii можно писать более-менее нормально, просто этого никто не делает, можно тогда пример?)

UFO just landed and posted this here

Ни разу не видел, чтобы в yii типизировали по интерфейсу :) так то конечно ничего не мешает.

Service Locator сюда прямого отношения не имеет, поскольку не обязан реализовывать DIP

SL — это как бы одна из возможных реализаций DIP. Он в принципе не может "не реализовывать" DIP. Вот просто по определению.

Почему зависимости, в основном, передаются через конструктор, а не в setter'ах?
Потому что безопасно. В случае передачи в конструктор любая ошибка не позволит создать объект, а, значит, он не успеет начать выполнять действия с ошибками. В случае с сеттерами есть возможность начать выполнять работу без зависимостей.

Для меня лично, вся разница (со стороны «потребителя кода SL/DI») выражается разницей между этими двумя примерами использования:
$logger = $this->container->get('logger'); // #1
$logger = $this->logger;                   // #2
$logger->debug('Blah-blah');               // #3

В первом случае есть проблемы: можно опечататься в строке названия сервиса, сервис может быть недоступен, не работает автокомплит (в связке symfony + phpstorm + плагин для symfony он таки есть, но это ведь частный случай), отсутствие сервиса или некорректный возвращённый объект доставят много весёлого времени дебага фатала в рантайме в третьей строке примера.

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

Конкретно в Symfony есть удобство: достаточно в конструктор класса передать интерфейс/класс и магия автоматически заинжектит нужный сервис без нужды лезть в конфиги и описывать правила явно. Заодно сразу становятся видны архитектурные проблемы (ещё до релиза и даже до коммита правок) когда у конструктора 4+ параметра.

ps: было бы гораздо проще и понятнее следить за эволюцией мысли если бы выбрали примеры на одном языке (любом). Или был бы каждый пример оформлен в виде («вот так в пхп, а вот так в js»).
А почему сразу SL — антипаттерн? Если не мешает зависимость от контейнера — SL очень даже юзабельный паттерн, у него свои преимущества есть.

Сервис локатор это по сути глобальные переменные в проекте. Вот и все.

Sign up to leave a comment.

Articles