Внедрение и поиск зависимостей
Прежде чем браться за материалы, посвящённые Spring Framework, мне хотелось бы повторить тему инверсии управления, так как этот принцип и его реализации в виде внедрения и поиска зависимостей являются важной составляющей ядра Spring Framework.
Под инверсией управления понимается подход к написанию кода, при котором элементы кода получают поток управления неявно от некоторого фреймворка. Иными словами, если сравнивать с традиционным процедурным программированием, не код обращается к фреймворку или библиотекам для выполнения какого-либо действия, а наоборот, фреймворк обращается к нашему коду.
Инверсия управления может быть использована для уменьшения связанности между компонентами кода повышения гибкости архитектуры следующими способами:
- Внедрение зависимостей
- Контекстный поиск
- Шаблон проектирования "Фабрика"
- Шаблон проектирования "Локатор служб"
Кроме этого инверсия управления может применяться для предоставления фреймворкам возможности использования нашего кода при помощи шаблонов проектирования "Шаблонный метод" или "Стратегия". Примеров такого поведения в контексте Spring Framework достаточно много: аннотированные контроллеры, функциональные обработчики HTTP-запросов, слушатели очередей сообщений JMS и т.д.
В этой статье я хочу подробно остановиться на внедрении и поиске зависимостей.
Внедрение зависимостей
Давайте вернёмся к интерфейсу FindTaskByIdSpi и его реализации FindTaskByIdMappingSqlQuery, которые были описаны в статье о шаблоне проектирования "Адаптер".
interface FindTaskByIdSpi {
Optional<TaskData> findTaskById(UUID id);
}
class FindTaskByIdSpiMappingSqlQuery extends MappingSqlQuery<TaskData>
implements FindTaskByIdSpi {
FindTaskByIdSpiMappingSqlQuery(DataSource dataSource) {
super(dataSource, """
select * from t_task where id = :id
""");
this.declareParameter(new SqlParameter("id", Types.OTHER));
this.compile();
}
@Override
Optional<TaskData> findTaskById(UUID id) {
return Optional.ofNullable(
this.findObjectByNamedParam(Map.of("id", id)));
}
}Допустим, в проекте появляется класс FindTaskUseCase, который использует FindTaskByIdSpi и FindTaskByIdMappingSqlQuery:
class FindTaskUseCase {
FindTaskByIdSpi findTaskByIdSpi;
FindTaskUseCase() {
var dataSource = new HikariDataSource();
dataSource.setJdbcUrl("...");
dataSource.setUsername("...");
dataSource.setPassword("...");
this.findTaskByIdSpi = new FindTaskByIdSpiMappingSqlQuery(dataSource);
}
}Конструктор класса FindTaskUseCase демонстрирует стандартный для процедурного программирования подход, когда все используемые зависимости определяются явно. Это создаёт жёсткую связь между FindTaskUseCase и FindTaskByIdSpiMappingSqlQuery, несмотря на то, что свойство findTaskByIdSpi объявлено с типом FindTaskByIdSpi - класс FindTaskUseCase всё равно знает и о существовании FindTaskByIdSpiMappingSqlQuery и о способе его создания, то же самое касается и HikariDataSource.
Приведённый пример кода нарушает принцип инверсии зависимостей, а так же ведёт к нарушению принципа открытости/закрытости, если в какой-то момент вы захотите использовать вместо HikariCP другой пул соединений с базой данных или захотите внедрить использование кэширования, так как в обоих случаях вам придётся вносить изменения в корректно работающий код.
А что если в проекте интерфейс FindTaskByIdSpi используется в нескольких местах? Тогда вам придётся вносить изменения во все зависимые классы! Хорошей практикой это назвать сложно, конечно же. На помощь в данной ситуации приходит внедрение зависимостей.
Внедрение зависимостей через конструктор
Если мы говорим о критически важных зависимостях, без которых зависимый класс не может работать корректно, то их лучше всего внедрять через конструктор. В результате этого конструктор FindTaskUseCase примет следующий вид:
class FindTaskUseCase {
FindTaskByIdSpi findTaskByIdSpi;
FindTaskUseCase(FindTaskByIdSpi findTaskByIdSpi) {
this.findTaskByIdSpi = findTaskByIdSpi;
}
}А создан экземпляр класса может быть следующим образом:
class FindTaskUseCaseCreator {
// Некоторый фабричный метод для поиска и создания зависимостей по типу
abstract <T> T getInstanceOf(Class<T> type);
FindTaskUseCase createWithConstructorInjection() {
// Получаем экземпляр FindTaskByIdSpi
var findTaskByIdSpi = this.getInstanceOf(FindTaskByIdSpi.class);
// Создаём и возвращаем экземпляр FindTaskUseCase
return new FindTaskUseCase(findTaskByIdSpi);
}
}Код, создающий экземпляр класса FindTaskUseCase, должен будет создать или получить откуда-то экземпляр FindTaskByIdSpi и передать его в аргументы конструктора.
У внедрения зависимостей через конструктор есть стилистическое ограничение: желательно, чтобы количество аргументов метода не превышало семи, в противном случае придётся обратиться к другим способам внедрения зависимостей. Альтернативным вариантом вполне может стать внедрение зависимостей через методы.
Внедрение зависимостей через set-метод
В этом варианте используются стандартные для спецификации Java Bean set-методы:
class FindTaskUseCase {
FindTaskByIdSpi findTaskByIdSpi;
void setFindTaskByIdSpi(FindTaskByIdSpi findTaskByIdSpi) {
this.findTaskByIdSpi = findTaskByIdSpi;
}
}Процесс создания экземпляра FindTaskUseCase будет выглядеть следующим образом:
class FindTaskUseCaseCreator {
// Некоторый фабричный метод для поиска и создания зависимостей по типу
abstract <T> T getInstanceOf(Class<T> type);
FindTaskUseCase createWithSetterInjection() {
// Создаём экземпляр FindTaskUseCase
var useCase = new FindTaskUseCase();
// Получаем экземпляр FindTaskByIdSpi
var findTaskByIdSpi = this.getInstanceOf(FindTaskByIdSpi.class);
// Внедряем зависимость
useCase.setFindTaskByIdSpi(findTaskByIdSpi);
return useCase;
}
}Методы, определённые в "интерфейсе внедрения"
В этом случае появляется интерфейс, декларирующий методы, при помощи которых зависимости могут быть внедрены, как это показано ниже:
interface FindTaskByIdSpiAware {
void findTaskByIdSpi(FindTaskByIdSpi findTaskByIdSpi);
}Класс, в который требуется внедрять зависимость, должен реализовывать этот интерфейс:
class FindTaskUseCase implements FindTaskByIdSpiAware {
FindTaskByIdSpi findTaskByIdSpi;
@Override
void findTaskByIdSpi(FindTaskByIdSpi findTaskByIdSpi) {
this.findTaskByIdSpi = findTaskByIdSpi;
}
}Процесс создания экземпляра FindTaskUseCase будет выглядеть следующим образом:
class FindTaskUseCaseCreator {
// Некоторый фабричный метод для поиска и создания зависимостей по типу
abstract <T> T getInstanceOf(Class<T> type);
FindTaskUseCase createWithInterfaceInjection() {
// Создаём экземпляр FindTaskUseCase
var useCase = new FindTaskUseCase();
if (useCase instanceof FindTaskByIdSpiAware findTaskByIdSpiAware) {
// Получаем экземпляр FindTaskByIdSpi
var findTaskByIdSpi = this.getInstanceOf(FindTaskByIdSpi.class);
// Внедряем зависимость
findTaskByIdSpiAware.findTaskByIdSpi(findTaskByIdSpi);
}
return useCase;
}
}Остальные методы
В целом внедрение зависимостей может быть реализовано при помощи любых методов:
class FindTaskUseCase {
FindTaskByIdSpi findTaskByIdSpi;
FindTaskUseCase withFindTaskByIdSpi(FindTaskByIdSpi findTaskByIdSpi) {
this.findTaskByIdSpi = findTaskByIdSpi;
return this;
}
}Процесс создания экземпляра FindTaskUseCase будет выглядеть следующим образом:
class FindTaskUseCaseCreator {
// Некоторый фабричный метод для поиска и создания зависимостей по типу
abstract <T> T getInstanceOf(Class<T> type);
FindTaskUseCase createWithMethodInjection() {
// Создаём экземпляр FindTaskUseCase
var useCase = new FindTaskUseCase();
// Получаем экземпляр FindTaskByIdSpi
var findTaskByIdSpi = this.getInstanceOf(FindTaskByIdSpi.class);
// Внедряем зависимость и возвращаем объект
return useCase.withFindTaskByIdSpi(findTaskByIdSpi);
}
}Внедрение зависимостей через свойства
Последний способ внедрения зависимостей - при помощи прямых обращений к свойствам класса:
class FindTaskUseCaseCreator {
// Некоторый фабричный метод для поиска и создания зависимостей по типу
abstract <T> T getInstanceOf(Class<T> type);
FindTaskUseCase createWithParameterInjection() {
// Создаём экземпляр FindTaskUseCase
var useCase = new FindTaskUseCase();
// Получаем экземпляр FindTaskByIdSpi и внедряем через свойство
useCase.findTaskByIdSpi = this.getInstanceOf(FindTaskByIdSpi.class);
return useCase;
}
}Этого способа внедрения лучше избегать, так как в этом случае вы открываете внутреннее устройство вашего класса для внешнего мира, как минимум в рамках пакета. Кстати, когда вы видите свойства классов, отмеченные аннотациями @Autowired в случае со Spring Framework или @Inject в случае с Java EE или Jakarta EE, знайте, что фактическое внедрение зависимостей происходит как раз через отмеченные свойства. Просто это делается при помощи рефлексии, и если вы выберете такой способ, то в модульных тестах вам придётся конфигурировать экземпляр тестируемого класса тоже при помощи рефлексии.
Поиск зависимостей
Ещё одним способом реализации инверсии управления является поиск зависимостей или контекстуализированный поиск (Contextualized Lookup). В этом случае в конфигурируемый объект может внедряться экземпляр класса, который предоставляет возможность поиска необходимых зависимостей. Примером такого класса может быть BeanFactory из Spring Framework.
Допустим, в нашем проекте для этого существует interface DependencyFactory:
interface DependencyFactory {
<T> T getInstanceOf(Class<T> type);
}Тогда создание экземпляра класса FindTaskUseCase будет выглядеть следующим образом, при условии, что DependencyFactory будет внедряться через конструктор:
class FindTaskUseCase {
FindTaskByIdSpi findTaskByIdSpi;
FindTaskUseCase(DependencyFactory dependencyFactory) {
this.findTaskByIdSpi = dependencyFactory.getInstanceOf(FindTaskByIdSpi.class);
}
}class FindTaskUseCaseCreator {
DependencyFactory dependencyFactory;
FindTaskUseCase createWithParameterInjection() {
// Создаём и возвращаем экземпляр FindTaskUseCase
return new FindTaskUseCase(this.dependencyFactory);
}
}В целом поиск зависимостей может упростить процесс конфигурирования экземпляров классов, которые имеют большое количество зависимостей.
Контейнеры инверсии управления
Все необходимые зависимости безусловно можно создать вручную, например, в main-методе вашего приложения и затем внедрять их в экземпляры целевых классов. В целом такое решение выглядит логичным до тех пор, пока проект имеет размеры, с которыми удобно работать. Но любой активно развивающийся проект в какой-то момент приобретает такие масштабы, при которых ручное управление зависимостями становится неудобным.
На помощь приходят контейнеры инверсии управления, которые могут порождать и предоставлять зависимости по мере необходимости, а так же хранить их для дальнейшего использования. Все фреймворки, реализующие внедрение зависимостей, такие как Spring Framework, Guice или Weld предоставляют собственные реализации таких контейнеров.
Среди классов, продемонстрированных в рамках этой статьи, DependencyFactory можно назвать минимальной заготовкой для контейнера инверсии управления.
