Хабрахабр

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

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

Всем привет! Я продолжаю серию переводов, в которой мы по косточкам разбираем, что такое 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, от D к 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!

К началу

Показать больше

Похожие публикации

Кнопка «Наверх»