Pull to refresh

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();
Ну ок, для первого уровня вы решили, а если зависимость нужна на 5 уровне? Передавать вниз по цепочке все зависимости которые когда то понадобятся? Так как раз от этого и хочется сбежать.
а для пятого — я сам не знаю классного подхода. а автор предлагает группировать эти зависимости по смыслу в один групповой объект (он назвал его config), и передавать их разом. тогда при добавлении/удалении очередной вложенной зависимости сигнатура метода не меняется (т.к. эта зависимость находится внутри группового объекта). по сути, это некий аналог service locator'а, только очень локальный, маленький, под конкретную предметную микро-область. я предпочитаю в таких «config»-ах делать заранее объявленные именованные геттеры, а не единый геттер с доступом по (условно) произвольной строке (getSomething() vs get(«something»)). чтобы синтаксический анализатор видел явную зависимость при обращении к данному геттеру где-то на пятом уровне вложенности. и чтобы было удобно искать и изменять эти зависимости, в том числе сигнатуру метода, автоматическими средствами рефакторинга, встроенными в ide. (напр., изменить getSomething() на getSomethingTasty(appleTaste)). но в целом, проблема, конечно, решена не полностью красиво. и всё же, по-моему, это лучше глобального сервис-локатора. (неважно, передаётся он в виде параметра или синглтоном)
Вы что-то понимаете неверно, неважно на каком уровне понадобилась зависимость способ её получения уже записан в контейнере, посмотрите мой пример в комментариях чуть ниже.
Я пока еще прочитал не все комментарии, и детально не вник, (да и тяжело идет, в работе использую процедурный язык с парой намеков на ООП), но кажется основную идею понял. Правда неясно что делать если в процессе работы нужно поменять фабрику на другую, да и в целом понимание к сожалению смутным осталось, мало того что почти незнакомое ООП, так еще и совсем незнакомый C#, интуитивно вроде ясно, но непривычно.
Есть случаи, когда крайне сложно или крайне неэффективно передавать зависимости параметрами, а не контейнер с ними. Например Url-роутер или иной диспетчер может иметь сотни и тысячи зависимостей и выбирать одну из них для исполнения в рантайме. Даже с какими-то прокси-объектами для ленивой загрузки, придётся инстанцировать сотни и тысячи этих прокси. Не говоря о ситуациях, когда прокси не работают. В таких случаях передать контейнер вполне оправданно.
Нет, нужно передать фабрику зависимостей. А объект с тысячей зависимостей это явный косяк архитектуры.
А фабрике контейнер, да? Иначе как она будет собирать зависимости этих зависимостей?
Посмотрите мой пример ниже. Все объекты регистрируются на контейнере в виде функтора описывающего как произвести данный объект, внутри этого функтора можно получать другие объекты зарегистрированные на контейнере.
Вот есть у нас контейнер, абстрактный от хранящихся в нём типов. Есть у нас роутер или диспетчер, который по какому-то рантайм ключу или их набору типа url должен вернуть инстанс какого-то интерфейса типа HttpRequestHandler. Все имплементации этого интерфейса, сотни и тысячи, зарегистрированы в контейнере как сервисы со сложными инфраструктурными зависимостями, нужно вернуть один из них, определяя нужный класс в рантайме. Можно не делать контейнер явно зависимостью роутера, можно сделать его зависимостью какой-то фабрики, отвечающей исключительно за получение инстансов, реализующих этот интерфейс, но или мы делаем контейнер зависимостью этой фабрики, или изобретаем велосипед, отвечающий за заполнение зависимостей нужных инстансов тем, что уже есть в контейнере.
В таком случае предлагается немного покодить в регистрации:
container.Register<Func<string, HttpRequestHandler>>(c => (url) =>
{
     Type type = GetHandlerForURL(url);
     return c.Resolve<HttpRequestHandler>(type); 
     //или c.Resolve(type) as HttpRequestHandler;
}


Да, в данном случае можно заюзать контейнер как словарь объектов, но это всё-таки нестандартный способ использования. И я убежден, что в рабочие классы контейнер передавать нельзя никогда, обязательно нужно обернуть в функтор или объект, имплементация которого живет там же где живет регистрация.
Хочу добавить, что пример вы привели хороший и да у меня такие случаи в работе встречаются(не сотни и тысячи классов в одном месте, конечно, но десятки) в таких случаях я поступаю как описал в первом ответе.
Думаю, это хороший повод для дискуссии. Допустим, в начале проекта мы делаем DI вручную. В какой-то момент код, занимающийся созданием объектов, станет настолько обширным, что работать с проектом станет неудобно. Тогда мы встанем перед выбором: продолжать накручивать самодельные несистематические костыли или использовать для управления зависимостями контейнер.

Допустим, мы выбрали контейнер. И даже то, что мы получим зависимость от контейнера, будет плюсом. Даже двумя (как минимум). Мы получим: 1) единый «центр управления» — контейнер упростит создание экземпляров и их внедрение (это очевидное преимущество), 2) мы будем использовать «хорошую» зависимость.

Под «хорошей» зависимостью я в данном случае понимаю то, что контейнер поставляется в виде внешней библиотеки. Код контейнера, очень вероятно, будет построен по принципам открытого ПО. Это точно верно для всех контейнеров, упомянутых в статье. Это значит, что этот код будет доступен и многократно перепроверен сообществом.
C контейнером сложнее реализовывать модульность.

Например, если в коде есть несколько слоев, то можно легко их и непринужденно нарушать.

Для больших проектов, есть резонный повод использовать готовую реализацию, а вот для библиотек контейнер вреден, на мой взгляд, у тебя возникнет протест, когда у тебя в проекте есть уже одна библиотека, которая реализует контейнер, а в зависимостях другая

Зависеть от контейнера должен только и только конечный проект, библиотеки просто пишутся с использованием DI и тогда нет проблемы их зарегистрировать на любой реализации контейнера

Например, если в коде есть несколько слоев, то можно легко их и непринужденно нарушать.
Как? И зачем?

для библиотек контейнер вреден
согласен. библиотека не должна зависеть от фреймворков.
Поскольку из контейнера можно получить любой объект, то, например, можно достать соединение с БД из слоя представления, хотя помещено оно туда было для слоя модели. В ситуации «нужно вчера» это может оказаться самым эффективным решением какой-то проблемы в краткосрочной перспективе.
И защиты от такого поведения в контейнере не может быть предусмотрено?

Возможность запросить подключение к БД из слоя представления говорит о том, что Вы и без контейнера сможете создать его вручную, если действительно захотите.


Но, например, в c# если подключение к БД и слой представления разнести по разным проектам, то можно ограничить область видимости и без хаков в виде рефлексии Вы на слое представления не сможете получить БД вообще никак

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

Для этих целей в контейнерах реализованы Nested Scopes/Nested Lifetimes, которые могут в том числе и ограничивать область видимости отдельных регистраций.

Способ изложения очень не нравится.
Напоминает стиль, принятый в современном матане или там теоретической физике.
Куча ненужных терминов, рассчитанных на людей, которые и так давно в теме. Примеры на тему «хочу зайти за умного». Если читатель уже набил шишек на синглтонах, ему не надо рассказывать про плюсы и минусы DI. Если нет — половина статьи для него набор баззвордов.
В матане за таким стилем стоит хотя бы идея — формальная корректность от аксиом и вот это всё — то здесь тупо понты, «смотрите какие умные слова я знаю».
Способ изложения очень не нравится.
Не нравится стиль статьи или моих комментариев?
Куча ненужных терминов, рассчитанных на людей, которые и так давно в теме.
Я немного удивлен. Думаю, здесь терминов не больше десятка. И избыточных из них может быть, разве «коллаборатор». Какие термины Вы считаете лишними?
В матане за таким стилем стоит хотя бы идея — формальная корректность от аксиом и вот это всё — то здесь тупо понты, «смотрите какие умные слова я знаю».
Ок, преамбулу Вы сделали. Давайте к конкретике.

Как-то спутаны плюсы DI и DI-контейнера. DI-контейнер и зависмости от него — это цена, которую нужно платить за удобное использование DI, прежде всего за решение проблемы переноса зависимостей. Причём проблема решается лишь частично — сам контейнер становится зависимостью которую нужно пробрасывать вместо реальных зависимостей. При этом реальные зависимости становятся менее явными, особенно в языках с утиной динамической типизацией, где реальный тип может вообще не появляться в коде, только this.container.get('someservice').run()
Контейнер, использумый таким образом является не DI-контейнером, а service-locator'ом. При нормальном использовании DI-container'а код проекта вообще не зависит от DI-container'а.
Эм… А нормальный тогда вариант какой?

Классы и их фабрики и всё-всё-всё регистрируются на контейнере в "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 могут это всё сделать гораздо более лаконично.

Наглядный пример получился и интересный подход, что все классы оборачиваются в функцию и прокидываются в таком виде в контейнер. Но возник вопрос, как это будет выглядеть, когда у нас 100, 200, 300 классов? Создаем множество таких входных точек?

Используя контейнер из моего примера, для каждого класса будет соответствующая строчка в Main вида:
container.Register<MyClass>(c => new MyClass());,
а в серьезных контейнерах достаточно
container.Register<MyClass>(),
а иногда и вообще без этого можно обойтись, если использовать принцип Convention over Configuration.


Если продолжить рассматривать мой случай, то блоки регистраций связанных друг с другом объектов часто удобно выделить в отдельную функцию регистрации этого блока объектов для повышения читаемости регистрации.

Как-то спутаны плюсы DI и DI-контейнера.
Серия идет в таком духе, что из статьи в статью переносятся основные мысли с добавлением нового. Да, автор снова перечисляет плюсы DI и добавляет к этому еще плюс от применения DI-контейнера (меньше перенос зависимостей). Думаю, такой подход оправдан и полезен для тех, кто с темой только начинает знакомиться.
Среди приверженцев принципов DI, есть и альтернативный взгляд на DI-container'ы, который сводится к тому что DI-container это промежуточный шаг между Poor mans DI и Convention over configuration, который сам по себе не очень-то нужен
Вот есть небольшая статья на эту тему:
blog.ploeh.dk/2012/11/06/WhentouseaDIContainer

В случае с Convention over Configuration, описанном в этой статье, DI-container все еще присутствует, он никуда не делся. Просто объекты в нем регистрируются автоматически, а не вручную.
Так что говорить, что DI-container это ненужный промежуточный шаг — некорректно.

Согласен, сформулировал неправильно.
Sign up to leave a comment.

Articles