Внедрение и поиск зависимостей

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

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

Инверсия управления может быть использована для уменьшения связанности между компонентами кода повышения гибкости архитектуры следующими способами:

  1. Внедрение зависимостей
  2. Контекстный поиск
  3. Шаблон проектирования «Фабрика»
  4. Шаблон проектирования «Локатор служб»

Кроме этого инверсия управления может применяться для предоставления фреймворкам возможности использования нашего кода при помощи шаблонов проектирования «Шаблонный метод» или «Стратегия». Примеров такого поведения в контексте Spring Framework достаточно много: аннотированные контроллеры, функциональные обработчики HTTP-запросов, слушатели очередей сообщений JMS и т.д.

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

Внедрение зависимостей

Давайте вернёмся к интерфейсу FindTaskByIdSpi и его реализации FindTaskByIdMappingSqlQuery, которые были описаны в статье о шаблоне проектирования «Адаптер».

Допустим, в проекте появляется класс FindTaskUseCase, который использует FindTaskByIdSpi и FindTaskByIdMappingSqlQuery:

Конструктор класса FindTaskUseCase демонстрирует стандартный для процедурного программирования подход, когда все используемые зависимости определяются явно. Это создаёт жёсткую связь между FindTaskUseCase и FindTaskByIdSpiMappingSqlQuery, несмотря на то, что свойство findTaskByIdSpi объявлено с типом FindTaskByIdSpi — класс FindTaskUseCase всё равно знает и о существовании FindTaskByIdSpiMappingSqlQuery и о способе его создания, то же самое касается и HikariDataSource.

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

А что если в проекте интерфейс FindTaskByIdSpi используется в нескольких местах? Тогда вам придётся вносить изменения во все зависимые классы! Хорошей практикой это назвать сложно, конечно же. На помощь в данной ситуации приходит внедрение зависимостей.

Внедрение зависимостей через конструктор

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

А создан экземпляр класса может быть следующим образом:

Код, создающий экземпляр класса FindTaskUseCase, должен будет создать или получить откуда-то экземпляр FindTaskByIdSpi и передать его в аргументы конструктора.

У внедрения зависимостей через конструктор есть стилистическое ограничение: желательно, чтобы количество аргументов метода не превышало семи, в противном случае придётся обратиться к другим способам внедрения зависимостей. Альтернативным вариантом вполне может стать внедрение зависимостей через методы.

Внедрение зависимостей через set-метод

В этом варианте используются стандартные для спецификации Java Bean set-методы:

Процесс создания экземпляра FindTaskUseCase будет выглядеть следующим образом:

Методы, определённые в «интерфейсе внедрения»

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

Класс, в который требуется внедрять зависимость, должен реализовывать этот интерфейс:

Процесс создания экземпляра FindTaskUseCase будет выглядеть следующим образом:

Остальные методы

В целом внедрение зависимостей может быть реализовано при помощи любых методов:

Процесс создания экземпляра FindTaskUseCase будет выглядеть следующим образом:

Внедрение зависимостей через свойства

Последний способ внедрения зависимостей — при помощи прямых обращений к свойствам класса:

Этого способа внедрения лучше избегать, так как в этом случае вы открываете внутреннее устройство вашего класса для внешнего мира, как минимум в рамках пакета. Кстати, когда вы видите свойства классов, отмеченные аннотациями @Autowired в случае со Spring Framework или @Inject в случае с Java EE или Jakarta EE, знайте, что фактическое внедрение зависимостей происходит как раз через отмеченные свойства. Просто это делается при помощи рефлексии, и если вы выберете такой способ, то в модульных тестах вам придётся конфигурировать экземпляр тестируемого класса тоже при помощи рефлексии.

Поиск зависимостей

Ещё одним способом реализации инверсии управления является поиск зависимостей или контекстуализированный поиск (Contextualized Lookup). В этом случае в конфигурируемый объект может внедряться экземпляр класса, который предоставляет возможность поиска необходимых зависимостей. Примером такого класса может быть BeanFactory из Spring Framework.

Допустим, в нашем проекте для этого существует interface DependencyFactory:

Тогда создание экземпляра класса FindTaskUseCase будет выглядеть следующим образом, при условии, что DependencyFactory будет внедряться через конструктор:

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

Контейнеры инверсии управления

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

На помощь приходят контейнеры инверсии управления, которые могут порождать и предоставлять зависимости по мере необходимости, а так же хранить их для дальнейшего использования. Все фреймворки, реализующие внедрение зависимостей, такие как Spring Framework, Guice или Weld предоставляют собственные реализации таких контейнеров.

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