Comments 36
Одна из проблем контейнера — он сам по себе внешняя зависимостьзначит, вы его НЕПРАВИЛЬНО готовите! нельзя путать шаблон «dependency injection container» с (анти-)шаблоном «service locator». далее привожу три примера на php:
1. как правильно готовить di container,
2. как неправильно использовать его в качестве service locator,
3. как совсем неправильно использовать его в качестве статического метода
в правильном способе (№1) от контейнера зависит только код самого контейнера, плюс ЕДИНСТВЕННАЯ точка входа в приложение, которая создаёт экземпляр контейнера и получает из него ваше приложение. это единственное использование контейнера за пределами самого контейнера. в DI-терминологии такое место в коде называется «composition root».
Таким образом, контейнер ни в коем случае не должен сам являться зависимостью! если он у вас является зависимостью, значит, вы его НЕПРАВИЛЬНО используете, и вам нужно внимательнее изучить тему DI. надеюсь, мои примеры помогут понять разницу.
простите, не нашёл тэга cut/spoiler в редакторе комментов на хабре!
<?php
// DI container pattern:
class SomeComponent
{
public function doSomething()
{
}
}
class App
{
public function __construct(SomeComponent $theComponent)
{
$this->theComponent = $theComponent;
}
public function run()
{
$this->theComponent->doSomething();
}
}
class DIContainer
{
public function get($className)
{
switch ($className) {
case SomeComponent::class:
return new SomeComponent;
case App::class:
return new App($this->get(SomeComponent::class));
default:
throw new Exception;
}
}
}
$DIContainer = new DIContainer();
$app = $DIContainer->get(App::class);
$app->run();
<?php
// service locator anti-pattern:
class SomeComponent
{
public function doSomething()
{
}
}
class App
{
// зависит от самого контейнера зависимостей -- service locator -- плохо!
public function __construct(DIContainer $DIContainer)
{
$this->theComponent = $DIContainer->get(SomeComponent::class);
}
public function run()
{
$this->theComponent->doSomething();
}
}
class DIContainer
{
public function get($className)
{
switch ($className) {
case SomeComponent::class:
return new SomeComponent;
case App::class:
return new App($this);
default:
throw new Exception;
}
}
}
$DIContainer = new DIContainer();
$app = $DIContainer->get(App::class);
$app->run();
<?php
// static methods call:
class SomeComponent
{
public function doSomething()
{
}
}
class App
{
// зависит от самого контейнера зависимостей -- но зависимость не явная,
// а через вызов статического метода -- ужас!
public function __construct()
{
$this->theComponent = DIContainer::get(SomeComponent::class);
}
public function run()
{
$this->theComponent->doSomething();
}
}
class DIContainer
{
public static function get($className)
{
switch ($className) {
case SomeComponent::class:
return new SomeComponent;
case App::class:
return new App;
default:
throw new Exception;
}
}
}
$app = DIContainer::get(App::class);
$app->run();
container.Register<Func<string, HttpRequestHandler>>(c => (url) =>
{
Type type = GetHandlerForURL(url);
return c.Resolve<HttpRequestHandler>(type);
//или c.Resolve(type) as HttpRequestHandler;
}
Да, в данном случае можно заюзать контейнер как словарь объектов, но это всё-таки нестандартный способ использования. И я убежден, что в рабочие классы контейнер передавать нельзя никогда, обязательно нужно обернуть в функтор или объект, имплементация которого живет там же где живет регистрация.
Допустим, мы выбрали контейнер. И даже то, что мы получим зависимость от контейнера, будет плюсом. Даже двумя (как минимум). Мы получим: 1) единый «центр управления» — контейнер упростит создание экземпляров и их внедрение (это очевидное преимущество), 2) мы будем использовать «хорошую» зависимость.
Под «хорошей» зависимостью я в данном случае понимаю то, что контейнер поставляется в виде внешней библиотеки. Код контейнера, очень вероятно, будет построен по принципам открытого ПО. Это точно верно для всех контейнеров, упомянутых в статье. Это значит, что этот код будет доступен и многократно перепроверен сообществом.
Например, если в коде есть несколько слоев, то можно легко их и непринужденно нарушать.
Для больших проектов, есть резонный повод использовать готовую реализацию, а вот для библиотек контейнер вреден, на мой взгляд, у тебя возникнет протест, когда у тебя в проекте есть уже одна библиотека, которая реализует контейнер, а в зависимостях другая
Например, если в коде есть несколько слоев, то можно легко их и непринужденно нарушать.Как? И зачем?
для библиотек контейнер вреденсогласен. библиотека не должна зависеть от фреймворков.
Возможность запросить подключение к БД из слоя представления говорит о том, что Вы и без контейнера сможете создать его вручную, если действительно захотите.
Но, например, в c# если подключение к БД и слой представления разнести по разным проектам, то можно ограничить область видимости и без хаков в виде рефлексии Вы на слое представления не сможете получить БД вообще никак
Для этих целей в контейнерах реализованы Nested Scopes/Nested Lifetimes, которые могут в том числе и ограничивать область видимости отдельных регистраций.
Напоминает стиль, принятый в современном матане или там теоретической физике.
Куча ненужных терминов, рассчитанных на людей, которые и так давно в теме. Примеры на тему «хочу зайти за умного». Если читатель уже набил шишек на синглтонах, ему не надо рассказывать про плюсы и минусы DI. Если нет — половина статьи для него набор баззвордов.
В матане за таким стилем стоит хотя бы идея — формальная корректность от аксиом и вот это всё — то здесь тупо понты, «смотрите какие умные слова я знаю».
Способ изложения очень не нравится.Не нравится стиль статьи или моих комментариев?
Куча ненужных терминов, рассчитанных на людей, которые и так давно в теме.Я немного удивлен. Думаю, здесь терминов не больше десятка. И избыточных из них может быть, разве «коллаборатор». Какие термины Вы считаете лишними?
В матане за таким стилем стоит хотя бы идея — формальная корректность от аксиом и вот это всё — то здесь тупо понты, «смотрите какие умные слова я знаю».Ок, преамбулу Вы сделали. Давайте к конкретике.
Классы и их фабрики и всё-всё-всё регистрируются на контейнере в "main"(ну или другая точка входа в зависимости от языка и окружения), с контейнера resolve'ится рутовый объект приложения и у него вызывается метод. В примере я буду использовать c# и некий условный DI-container
public void Main()
{
Container container = new Container();
container.Register<B>(c => new B());
container.Register<A>(c => new A(c.Resolve<B>()));
using(Scope scope = Container.CreateScope())
{
scope.Resolve<A>().ExecuteApplication();
}
}
Вообще рекомендую для понимания принципов использования контейнеров почитать документацию к autofac — достаточно популярному DI-container'у для С#
http://autofac.readthedocs.io/en/latest/getting-started/index.html
и в частности:
http://autofac.readthedocs.io/en/latest/best-practices/index.html
там даже есть отдельный пункт с рекомендацией не использовать DI-container как service locator.
Контейнер остаётся контейнером. В качестве зависимости в отдельных компонентах он пробрасывается, чтобы частично решить проблему переноса зависимости. Скажем, модулю на 3-м уровне нужно 9 зависимостей. Проще пробросить контейнер, чем эти 9 зависимостей тащить из контейнера от точки входа.
Да, сценарий использования похож на типичное использование сервис локатора со стороны зависимого модуля, но решение другое, сервис локатор не управляет жизненным циклом сервисов в общем случае, сервисы регистрируются в нём сами или их регистрируют их создатели, контейнер же создаёт их имея какое-то описание как создавать.
А как код проекта может не зависеть от DI-контейнера я вообще не представляю. Вернее представляю, но только в случае широкого использования метапрограммирования, заменяющего, например, new UserManager на вызов Container::getInstance().get('UserManager'). Как-то нам нужно получать из контейнера то, что мы хотим хотя бы на уровне точки входа в приложение, если пробрасывание десятков зависимостей нас не страшит.
А как код проекта может не зависеть от DI-контейнера я вообще не представляю.
Код проекта зависит от кода DI-контенера, но только в файлах регистрации.
Использовать DI-container как service-locator это антипаттерн(это не моё утверждение выше я уже давал ссылку на документацию к autofac, отговаривающую использовать контейнер таким образом), исходящий из неполного понимания зачем вообще городится огород. Для того чтобы пробросить зависимости не нужно тащить через весь стек 100500 зависимостей и не нужно тащить с собой service-locator — нужно использовать фабрики. Рассмотрим модельную ситуацию: класс A в процессе работы должен производить экземпляры класса B, но классу B для этого нужен инстанс класса С(всем общий), который классу A никак не нужен, тогда вместо того чтобы в класс A тащить инстанс класса С или контейнер, чтобы его передать в конструктор B, нужно передать в A функтор создания B, который уже знает как создать B и зарезловить для него зависимости на контейнере:
(Снова С#, уж простите что я с ним лезу в тред с тегом Java)
//Примитивный контейнер
public interface IContainer
{
void Register<T>(Func<Container, T> a);
void RegisterSingleton<T>(Func<Container, T> a);
T Resolve<T>();
}
public class C
{
public void Do() { }
}
public class B
{
public B(C c)
{
// сделаем что-нибудь с C в конструкторе, чтобы обозначить, что B зависит от C
c.Do();
}
}
public class A
{
private readonly Func<B> _bFactory;
private List<B> _bList;
public A(Func<B> bFactory)
{
_bFactory = bFactory;
}
public void DoStuff()
{
//Создадим инстанс B с помощью фабрики и положим его в список
_bList.Add(_bFactory());
}
}
public class App
{
public void Main()
{
//Cоздадим контейнер для приложения
IContainer container = new Container();
//Регистрация классов на контейнере
//регистрируем фабрику С и сообщаем, что инстанс C должен быть один
container.RegisterSingleton<C>(c => new C());
//Регистрируем фабрику B на контейнере
//(заметь что инстанс С в конструктор разрешается с контейнера
container.Register<Func<B>>(c => () => new B(c.Resolve<C>()));
//Регистрируем А, разрешая фабрику В с контейнера
container.Register<A>(c => new A(c.Resolve<Func<B>>()));
//Забираем А из контейнера и выполняем код приложения
A a = container.Resolve<A>();
a.DoStuff();
}
}
Как видишь, классы проекта не зависит от контейнера, только Main
создающий контейнер и регистрирующий на нем классы знает о его существовании.
Отмечу, что в примере использовался примитивный контейнер только с базовым функционалом, крутые контейнеры типо Autofac могут это всё сделать гораздо более лаконично.
Используя контейнер из моего примера, для каждого класса будет соответствующая строчка в Main
вида:
container.Register<MyClass>(c => new MyClass());
,
а в серьезных контейнерах достаточно
container.Register<MyClass>()
,
а иногда и вообще без этого можно обойтись, если использовать принцип Convention over Configuration.
Если продолжить рассматривать мой случай, то блоки регистраций связанных друг с другом объектов часто удобно выделить в отдельную функцию регистрации этого блока объектов для повышения читаемости регистрации.
Как-то спутаны плюсы DI и DI-контейнера.Серия идет в таком духе, что из статьи в статью переносятся основные мысли с добавлением нового. Да, автор снова перечисляет плюсы DI и добавляет к этому еще плюс от применения DI-контейнера (меньше перенос зависимостей). Думаю, такой подход оправдан и полезен для тех, кто с темой только начинает знакомиться.
Вот есть небольшая статья на эту тему:
blog.ploeh.dk/2012/11/06/WhentouseaDIContainer
В случае с Convention over Configuration, описанном в этой статье, DI-container все еще присутствует, он никуда не делся. Просто объекты в нем регистрируются автоматически, а не вручную.
Так что говорить, что DI-container это ненужный промежуточный шаг — некорректно.
Контейнеры внедрения зависимостей и выгоды от их использования