Контейнеры внедрения зависимостей и выгоды от их использования

http://tutorials.jenkov.com/dependency-injection/dependency-injection-benefits.html
  • Перевод

От переводчика


Всем привет! Я продолжаю серию переводов, в которой мы по косточкам разбираем, что такое Dependency Injection.

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

В сегодняшнем переводе речь пойдет о том, что собой представляет DI-контейнер, его функциях, преимуществах использования и отличии от фабрик.


Серия включает в себя следующие статьи



  1. Dependency Injection
  2. Dependency Injection Containers
  3. Dependency Injection Benefits
  4. When to use Dependency Injection
  5. Is Dependency Injection Replacing the Factory Patterns?

Контейнеры внедрения зависимостей


Основные термины: контейнер, управление жизненным циклом компонента

Если в вашей системе все компоненты имеют свои зависимости, то где-то в системе какой-то класс или фабрика должны знать, что внедрять во все эти компоненты. Вот что делает DI-контейнер. Причина, по которой это называется «контейнер», а не «фабрика» в том, что контейнер обычно берет на себя ответственность не только за создание экземпляров и внедрение зависимостей.

Когда вы конфигурируете DI-контейнер, вы определяете, экземпляры каких компонентов он должен быть способен создать, и какие зависимости внедрить в каждый компонент. Также вы обычно можете настроить режим создания экземпляра для каждого компонента. Например, должен ли новый экземпляр создаваться каждый раз? Или один и тот же экземпляр компонента должен быть переиспользован (синглтон) везде, куда он внедряется?

Если некоторые компоненты настроены как синглтоны, то некоторые контейнеры имеют возможность вызывать методы синглтона тогда, когда контейнер выключается. Таким образом синглтон может освободить любые ресурсы, которые он использует, такие как подключение к БД или сетевое соединение. Это обычно называют «управлением жизненным циклом объекта». Это значит, что контейнер способен управлять компонентом на различных стадиях жизненного цикла компонента. Например, создание, конфигурирование и удаление.

Управление жизненным циклом — это одна из обязанностей, которую DI контейнеры принимают в дополнение к созданию экземпляров и их внедрению. Тот факт, что контейнер иногда сохраняет ссылку на компоненты после создания экземпляра, и есть та причина, по которой он называется «контейнером», а не фабрикой. DI-контейнеры обычно сохраняют ссылки на объекты, чьим жизненным циклом им предстоит управлять или которые будут переиспользованы для будущих внедрений, такие как синглтон или приспособленец. Когда контейнер настроен на создание новых экземпляров компонентов при каждом вызове, контейнер обычно «забывает» о созданных объектах. В противном случае у сборщика мусора будет горячая пора, когда придет время собирать все эти объекты.

На данный момент доступно несколько DI-контейнеров. Для Java существуют Butterfly Container, Spring, Pico Container (прим. ред. в его разработке уаствовал Мартин Фаулер), Guice (прим. ред. разработка Google) и другие (прим. ред. например, есть еще Dagger, также разработка Google. Jakob Jenkov, автор переводимой статьи, разработал Butterfly Container. Его исходный код доступен на github, а документация содержится в отдельной серии постов).

Выгоды от использования DI и DI-контейнеров

Основные термины: перенос зависимостей, коллабораторы

Существует несколько преимуществ от использования DI-контейнеров по сравнению с тем, что компонентам приходится самостоятельно разрешать свои зависимости (прим. ред. «самостоятельно разрешать зависимости» в данном контексте означает «создавать объекты, необходимые для работы компонента, внутри самого компонента»).

Некоторые из этих преимуществ:

Меньше зависимостей
Меньше «перенос» зависимостей
Код проще переиспользовать
Код удобнее тестировать
Код удобнее читать

Эти преимущества более детально объяснены далее.

Меньше зависимостей


DI делает возможным устранить или, по крайней мере, уменьшить необязательные зависимости компонента. Компонент уязвим перед изменением его зависимостей. Если зависимость изменится, компоненту, возможно, придется адаптироваться к этим изменениям. Например, если сигнатура метода зависимости изменится, компоненту придется изменять вызов этого метода. Когда зависимости компонента сведены к минимуму, он в меньшей степени подвержен необходимости изменений.

Код проще переиспользовать


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

Код удобнее тестировать


DI также повышает возможности тестирования компонентов. Когда зависимости могут быть внедрены в компонент, возможно также и внедрение mock-ов этих объектов. Mock-объекты используются для тестирования как замена настоящей имплементации. Поведение mock-объекта может быть сконфигурировано. Таким образом, все возможное поведение компонента при использовании mock-объекта может быть протестировано на корректность. Например, обработка ситуации, когда mock возвращает корректный объект, когда возвращает null и когда выбрасывается исключение. Кроме того, mock-объекты обычно записывают, какие их методы были вызваны и, таким образом, тест может проверить, что компонент, использующий mock, использовал их (прим. ред. методы) как ожидалось.

Код удобнее читать


DI переносит зависимости в интерфейс компонентов. Это делает нагляднее то, какие зависимости есть у компонента, делая код более удобным для чтения. Вам не придется просматривать весь код для того, чтобы увидеть то, какие зависимости вам нужно будет предоставить для данного компонента. Они все видны в интерфейсе.

Меньше «перенос» зависимостей


Еще один приятный бонус от DI — избавляет от того, что я называю «перенос зависимостей». Перенос зависимостей проявляется в том, что объект получает параметр в одном из своих методов, который сам по себе объекту не нужен, а нужен одному из объектов, которые он вызывает для своей работы. Это может звучать немного абстрактно, так что давайте приведем простой пример.

Компонент A загружает приложение и создает объект конфигурации, Config, который нужен какому-то из объектов приложения, но не всем компонентам в системе. Затем А вызывает B, B вызывает C, С вызывает D. Ни B, ни C не нуждаются в объекте типа Config, но D нуждается. Вот цепочка вызовов.

  A создает Config
  A --> B --> C --> D --> Config

Стрелки символизируют вызовы методов. Если A создает B, и B создает C, и C создает D, и D нуждается в Config, то объект Config должен быть передан через всю цепочку: от A к B, от B к C, и, наконец, от C к D. Тем не менее, ни C, ни D для выполнения работы объект Config не нужен. Все, что они делают — это «переносят» Config к D, который и зависит от Config. Отсюда и название «перенос зависимостей».

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

Перенос зависимостей создает много «шума» в коде, делая труднее его чтение и поддержку. К тому же, это затрудняет тестирование компонентов. Если вызов метода компонента A требует некоторого объекта OX только потому, что он нужен его «коллаборатору» CY (прим. ред. в оригинале используется слово collaborator. По определению, коллабораторы — это классы, которые либо зависят от других, либо предоставляют что-либо другому классу. Получается, что категория «коллаборатор» объединяет в себе понятия «зависимый класс» и «зависимость», является их надмножеством), вам все равно нужно предоставить экземпляр OX при тестировании метода объекта A, даже если он его не использует. Даже если вы используете mock-реализацию коллаборатора CY, который может не использовать объект OX. Вы можете обойти это, передав null вместо OX, если в тестируемом методе нет проверки на null. Иногда в ходе теста может быть сложно создать объект OX. Если конструктор OX зависит от множества других объектов или значений, вашему тесту также придется передавать осмысленные объекты/значения для этих параметров. И если OX зависит от OY, который зависит от OZ, это становится настоящим безумием.

Когда стеки вызова глубоки, перенос зависимостей — это настоящая боль. Особенно, если вы обнаруживаете, что компонент со дна стека нуждается в другом объекте, доступном выше по стеку. Затем вам придется добавить этот объект как параметр во все вызовы методов вниз по стеку, начиная тем, откуда требуемый объект доступен и заканчивая тем, где он необходим.

Общее решение для проблемы «переноса зависимостей» — сделать необходимые объекты статическими синглтонами. Таким образом любой компонент системы сможет получить доступ к синглтону через его статический фабричный метод (прим. ред. не путать с паттерном Фабричный Метод). К сожалению, статические синглтоны тянут за собой целый ворох других проблем, в которые я здесь не буду погружаться. Статические синглтоны — зло. Не используйте их, если вам удастся избежать этого.

Когда вы используете DI-контейнер, вы можете снизить «перенос зависимостей» и сократить использование статических синглтонов. Контейнер знает обо всех компонентах в приложении. Следовательно, он может идеально связать компоненты, без необходимости передавать зависимости одному компоненту через другой. Пример с компонентами при использовании контейнера будет выглядеть следующим образом:

Контейнер создает Config
Контейнер создает D и внедряет Config
Контейнер создает C и внедряет D
Контейнер создает B и внедряет C
Контейнер создает A и внедряет B

A --> B --> C --> D --> Config

Когда A вызывает B, ему не нужно передавать объект Config в B. D уже знает об объекте Config.

Однако, все еще могут быть ситуации, когда нельзя избежать переноса зависимостей. Например, если ваше приложение обрабатывает запросы (это веб-приложение или веб-сервис), и компоненты, обрабатывающие запросы это — синглтоны. Тогда объект запроса может быть необходимо передавать вниз по цепочке вызовов каждый раз, когда доступ к нему понадобится компоненту более низкоуровневого слоя.

В следующей статье, «When to use dependency injection», Jakob Jenkov приводит практические примеры применения DI. Как написать код, если вам нужно: внедрить конфигурационную информацию в один или несколько компонентов, внедрить одну и ту же зависимость в один или несколько компонентов, внедрить разные реализации одной зависимости, внедрить одну и ту же реализацию в разных конфигурациях, получить какие-либо данные из контейнера. Также автор рассказывает о том, в каких случаях DI вам не понадобится. Stay tuned!

К началу
Поделиться публикацией
Похожие публикации
Ой, у вас баннер убежал!

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


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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

                          • +1

                            Контейнер остаётся контейнером. В качестве зависимости в отдельных компонентах он пробрасывается, чтобы частично решить проблему переноса зависимости. Скажем, модулю на 3-м уровне нужно 9 зависимостей. Проще пробросить контейнер, чем эти 9 зависимостей тащить из контейнера от точки входа.


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


                            А как код проекта может не зависеть от DI-контейнера я вообще не представляю. Вернее представляю, но только в случае широкого использования метапрограммирования, заменяющего, например, new UserManager на вызов Container::getInstance().get('UserManager'). Как-то нам нужно получать из контейнера то, что мы хотим хотя бы на уровне точки входа в приложение, если пробрасывание десятков зависимостей нас не страшит.

                            • +1
                              А как код проекта может не зависеть от 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 могут это всё сделать гораздо более лаконично.

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

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


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

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

                            • +1

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

                              • 0
                                Согласен, сформулировал неправильно.

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                            Самое читаемое