Прежде чем браться за материалы, посвящённые Spring Framework, мне хотелось бы повторить тему инверсии управления, так как этот принцип и его реализации в виде внедрения и поиска зависимостей являются важной составляющей ядра Spring Framework.
Под инверсией управления понимается подход к написанию кода, при котором элементы кода получают поток управления неявно от некоторого фреймворка. Иными словами, если сравнивать с традиционным процедурным программированием, не код обращается к фреймворку или библиотекам для выполнения какого-либо действия, а наоборот, фреймворк обращается к нашему коду.
Инверсия управления может быть использована для уменьшения связанности между компонентами кода повышения гибкости архитектуры следующими способами:
- Внедрение зависимостей
- Контекстный поиск
- Шаблон проектирования «Фабрика»
- Шаблон проектирования «Локатор служб»
Кроме этого инверсия управления может применяться для предоставления фреймворкам возможности использования нашего кода при помощи шаблонов проектирования «Шаблонный метод» или «Стратегия». Примеров такого поведения в контексте 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
можно назвать минимальной заготовкой для контейнера инверсии управления.