Статьи

Прагматичный взгляд на метод инъекций

Намерение

Позволяет контейнеру внедрять методы вместо объектов и обеспечивает динамическое подклассирование.

Также известна как

декорация метода (или внедрение AOP).

Мотивация

Иногда случается, что нам нужен
фабричный метод в нашем классе, который создает новый объект каждый раз, когда мы обращаемся к классу. Например, у нас может быть RequestProcessor, у которого есть метод с именем process, который принимает запрос в качестве входных данных и возвращает ответ в качестве выходных данных. Но, прежде чем ответ будет сгенерирован, запрос должен быть проверен и затем передан в класс обслуживания, который обработает запрос и вернет ответ.

public class RequestProcessor implements Processor {

private Service service;

public Response process(Request request) {
Validator validator = getNewValidatorInstance();
List errorMessages = validator.validate(request);
if (!errorMessages.isEmpty()) {
throw new RuntimeException("Validation Error");
}
Response response = service.makeServiceCall(request);
return response;
}

protected ValidatorImpl getNewValidatorInstance() {
return new ValidatorImpl();
}

}

Как видно из приведенного выше фрагмента кода, мы создаем новый экземпляр ValidatorImpl каждый раз, когда вызывается метод процесса. RequestProcessor требует новый экземпляр каждый раз, потому что Validator может иметь некоторое состояние, которое должно быть разным для каждого запроса (например, список сообщений об ошибках).

Компонент RequestProcessor управляется контейнером внедрения зависимостей, таким как spring, в котором экземпляр Validator создается в RequestProcessor. Это решение выглядит идеально, но имеет несколько недостатков:

  1. RequestProcessor тесно связан с деталями реализации Validator.
  2. Если Validator имел какие-либо зависимости от конструктора, то RequestProcessor также должен знать их. Например, если Validator имеет зависимость от некоторого класса Helper, который внедряется в конструктор Validator, то RequestProcessor также должен знать о помощнике.

Существует также другой подход, который позволяет использовать контейнер для управления bean-компонентом Validator (прототипом) и информировать bean-компонент о контейнере, реализуя интерфейс ApplicationContextAware.

public class RequestProcessor implements Processor,ApplicationContextAware {

private Service service;
private ApplicationContext applicationContext;

public Response process(Request request) {
Validator validator = getNewValidatorInstance();
List errorMessages = validator.validate(request);
if (!errorMessages.isEmpty()) {
throw new RuntimeException("Validation Error");
}
Response response = getService().makeServiceCall(request);
return response;
}

protected Validator getNewValidatorInstance() {
return (Validator)applicationContext.getBean("validator");
}

public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
}

public void setService(Service service) {
this.service = service;
}

public Service getService() {
return service;
}

}

Этот подход также имеет свой недостаток, поскольку бизнес-логика приложения теперь связана со средой Spring.

Метод инъекций обеспечивает лучший способ обработки таких случаев. Ключ к внедрению метода заключается в том, что метод может быть переопределен для возврата другого компонента в контейнер. В методе инъекции Spring используется библиотека CGLIB для динамического переопределения класса.

Применимость

Использовать метод инъекций, когда

  • вы хотите избежать зависимости от контейнера, как мы видели во втором подходе, в котором вы должны внедрить не одноэлементный компонент внутри одноэлементного компонента.
  • Вы хотите избежать подклассов. Например, предположим, что RequestProcessor обрабатывает два типа ответа и в зависимости от типа отчета мы используем разные валидаторы. Таким образом, мы можем иметь подкласс RequestProcessor и иметь Report1RequestProcessor, который просто предоставляет Validator, необходимый для Report1.
public class Report1RequestProcessor extends RequestProcessor {

@Override
protected Validator getNewValidatorInstance() {
return new ValidatorImpl();
}

}

public abstract class RequestProcessor implements Processor {

private Service service;

public Response process(Request request) {
Validator validator = getNewValidatorInstance();
List errorMessages = validator.validate(request);
if (!errorMessages.isEmpty()) {
throw new RuntimeException("Validation Error");
}
Response response = getService().makeServiceCall(request);
return response;
}

protected abstract Validator getNewValidatorInstance();

public void setService(Service service) {
this.service = service;
}

public Service getService() {
return service;
}

}

Метод реализации инъекций обеспечивает более чистое решение. Контейнер внедрения зависимостей, такой как Spring, переопределит метод getNewValidatorInstance (), и ваш бизнес-код будет независимым как от кода инфраструктуры Spring Framework, так и от конкретной реализации интерфейса Validator. Таким образом, вы можете написать код для интерфейса.

public abstract class RequestProcessor implements Processor {

private Service service;

public Response process(Request request) {
Validator validator = getNewValidatorInstance();
List errorMessages = validator.validate(request);
if (!errorMessages.isEmpty()) {
throw new RuntimeException("Validation Error");
}
Response response = getService().makeServiceCall(request);
return response;
}

protected abstract Validator getNewValidatorInstance();

public void setService(Service service) {
this.service = service;
}

public Service getService() {
return service;
}

}

Метод требует следующую подпись

<public|protected> [abstract] <return-type> methodName(no-arguments);

Если класс не обеспечивает реализацию, как в нашем классе RequestProcessor, Spring динамически генерирует подкласс, который реализует метод, в противном случае он переопределяет метод. 

application-context.xml будет выглядеть так

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

<bean id="service" class="com.shekhar.methodinjection.ExampleService" />

<bean id="requestProcessor" class="com.shekhar.methodinjection.RequestProcessor">
<property name="service" ref="service"></property>
<lookup-method bean="validator" name="getNewValidatorInstance"/>
</bean>

<bean id="validator" class="com.shekhar.methodinjection.ValidatorImpl" scope="prototype">
</bean>
</beans>

Вот как метод инъекции может быть использован в наших приложениях.

Последствия

Внедрение метода имеет следующие преимущества:

1) Обеспечивает динамическое создание подклассов.

2) Избавление от кода инфраструктуры контейнера в сценариях, где компонент Singleton должен иметь компонент не синглтон или прототип.

Внедрение метода имеет следующие обязательства:

1) Модульное тестирование — Модульное тестирование станет трудным, так как мы должны тестировать абстрактный класс. Вы можете избежать этого, сделав метод, который предоставляет вам экземпляр как неабстрактный, но реализация этого метода будет избыточной, поскольку контейнер всегда будет переопределять его.

2) Добавляет магию в ваш код — любому, кто не знаком с внедрением метода, будет нелегко узнать, как работает код. Это может затруднить понимание вашего кода.