Шаблон проектирования Java Dependency Injection позволяет нам избавиться от жестко закодированных зависимостей и сделать наше приложение слабосвязанным, расширяемым и поддерживаемым. Мы можем реализовать внедрение зависимостей в Java, чтобы переместить разрешение зависимости из времени компиляции во время выполнения.
Внедрение зависимостей в Java
Внедрение зависимостей в Java кажется сложным с теоретической точки зрения, поэтому давайте рассмотрим простой пример, а затем посмотрим, как использовать шаблон внедрения зависимостей для достижения слабой связности и расширяемости в приложении. Допустим, у нас есть приложение, в котором мы используем EmailService
для отправки электронных писем. Обычно мы бы реализовали это следующим образом.
package com.journaldev.java.legacy;
public class EmailService {
public void sendEmail(String message, String receiver){
//логика отправки электронной почты
System.out.println("Email sent to "+receiver+ " with Message="+message);
}
}
Класс EmailService
содержит логику отправки сообщения по электронной почте на адрес получателя. Наш код приложения будет выглядеть следующим образом.
package com.journaldev.java.legacy;
public class MyApplication {
private EmailService email = new EmailService();
public void processMessages(String msg, String rec){
//некоторая логика проверки сообщения, манипуляции и т.д.
this.email.sendEmail(msg, rec);
}
}
Наш клиентский код, который будет использовать класс MyApplication
для отправки электронных сообщений, будет выглядеть следующим образом.
package com.journaldev.java.legacy;
public class MyLegacyTest {
public static void main(String[] args) {
MyApplication app = new MyApplication();
app.processMessages("Hi Pankaj", "[email protected]");
}
}
С первого взгляда кажется, что в вышеуказанной реализации нет ничего плохого. Однако логика выше имеет определенные ограничения.
Класс MyApplication
отвечает за инициализацию службы электронной почты и её последующее использование. Это приводит к зашитой зависимости в коде. Если мы захотим переключиться на другую продвинутую службу электронной почты в будущем, это потребует изменений кода в классе MyApplication. Это делает наше приложение сложным для расширения, особенно если служба электронной почты используется в нескольких классах.- Если мы захотим расширить наше приложение, чтобы предоставить дополнительную функцию отправки сообщений, такую как SMS или сообщение в Facebook, нам придется написать ещё одно приложение. Это потребует изменений кода в классах приложения и в классах клиентов.
- Тестирование приложения будет затруднено, поскольку наше приложение напрямую создает экземпляр службы электронной почты. Нет способа использовать имитации этих объектов в наших тестовых классах.
Можно возразить, что мы можем удалить создание экземпляра службы электронной почты из класса MyApplication
, имея конструктор, который требует службу электронной почты в качестве аргумента.
package com.journaldev.java.legacy;
public class MyApplication {
private EmailService email = null;
public MyApplication(EmailService svc){
this.email=svc;
}
public void processMessages(String msg, String rec){
// выполнение проверки сообщения, логики манипуляции и т.д.
this.email.sendEmail(msg, rec);
}
}
Но в этом случае мы просим клиентские приложения или тестовые классы инициализировать службу электронной почты, что не является хорошим проектным решением. Теперь давайте посмотрим, как мы можем применить паттерн внедрения зависимостей (dependency injection) в Java, чтобы решить все проблемы с вышеуказанной реализацией. Для внедрения зависимостей в Java требуется как минимум следующее:
- Службовые компоненты должны быть разработаны с базовым классом или интерфейсом. Желательно использовать интерфейсы или абстрактные классы, которые определяют контракт для служб.
- Классы-потребители должны быть написаны в терминах интерфейса службы.
- Injector-классы, которые будут инициализировать службы, а затем классы-потребители.
Внедрение зависимостей в Java – компоненты служб
Для нашего случая мы можем использовать MessageService
, который будет объявлять контракт для реализации служб.
package com.journaldev.java.dependencyinjection.service;
public interface MessageService {
void sendMessage(String msg, String rec);
}
Предположим, у нас есть службы электронной почты и SMS, реализующие указанные интерфейсы.
package com.journaldev.java.dependencyinjection.service;
public class EmailServiceImpl implements MessageService {
@Override
public void sendMessage(String msg, String rec) {
//логика отправки электронной почты
System.out.println("Email sent to "+rec+ " with Message="+msg);
}
}
package com.journaldev.java.dependencyinjection.service;
public class SMSServiceImpl implements MessageService {
@Override
public void sendMessage(String msg, String rec) {
//логика отправки SMS
System.out.println("SMS sent to "+rec+ " with Message="+msg);
}
}
Наши службы внедрения зависимостей в Java готовы, и теперь мы можем написать наш класс-потребитель.
Внедрение зависимостей в Java – Потребитель служб
Не требуется иметь базовые интерфейсы для классов-потребителей, но у меня есть интерфейс Consumer
, который объявляет контракт для классов-потребителей.
package com.journaldev.java.dependencyinjection.consumer;
public interface Consumer {
void processMessages(String msg, String rec);
}
Реализация моего класса-потребителя выглядит следующим образом.
package com.journaldev.java.dependencyinjection.consumer;
import com.journaldev.java.dependencyinjection.service.MessageService;
public class MyDIApplication implements Consumer{
private MessageService service;
public MyDIApplication(MessageService svc){
this.service=svc;
}
@Override
public void processMessages(String msg, String rec){
//выполнение некоторой валидации сообщения, логики манипуляции и т. д.
this.service.sendMessage(msg, rec);
}
}
Обратите внимание, что наш класс приложения просто использует сервис. Он не инициализирует сервис, что приводит к лучшему “разделению задач“. Также использование интерфейса сервиса позволяет нам легко тестировать приложение, подменяя MessageService, и привязывать службы во время выполнения, а не во время компиляции. Теперь мы готовы написать классы инъекции зависимостей на Java, которые будут инициализировать сервис, а также классы-потребители.
Внедрение зависимостей в Java – Классы инжекторы
Давайте создадим интерфейс MessageServiceInjector
с объявлением метода, возвращающего класс Consumer
.
package com.journaldev.java.dependencyinjection.injector;
import com.journaldev.java.dependencyinjection.consumer.Consumer;
public interface MessageServiceInjector {
public Consumer getConsumer();
}
Теперь для каждого сервиса нам придется создавать классы инжекторов, как показано ниже.
package com.journaldev.java.dependencyinjection.injector;
import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.consumer.MyDIApplication;
import com.journaldev.java.dependencyinjection.service.EmailServiceImpl;
public class EmailServiceInjector implements MessageServiceInjector {
@Override
public Consumer getConsumer() {
return new MyDIApplication(new EmailServiceImpl());
}
}
package com.journaldev.java.dependencyinjection.injector;
import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.consumer.MyDIApplication;
import com.journaldev.java.dependencyinjection.service.SMSServiceImpl;
public class SMSServiceInjector implements MessageServiceInjector {
@Override
public Consumer getConsumer() {
return new MyDIApplication(new SMSServiceImpl());
}
}
Теперь посмотрим, как наши клиентские приложения будут использовать приложение с помощью простой программы.
package com.journaldev.java.dependencyinjection.test;
import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.injector.EmailServiceInjector;
import com.journaldev.java.dependencyinjection.injector.MessageServiceInjector;
import com.journaldev.java.dependencyinjection.injector.SMSServiceInjector;
public class MyMessageDITest {
public static void main(String[] args) {
String msg = "Hi Pankaj";
String email = "[email protected]";
String phone = "4088888888";
MessageServiceInjector injector = null;
Consumer app = null;
//Отправить электронное письмо
injector = new EmailServiceInjector();
app = injector.getConsumer();
app.processMessages(msg, email);
//Отправить SMS
injector = new SMSServiceInjector();
app = injector.getConsumer();
app.processMessages(msg, phone);
}
}
Как видите, наши классы приложений отвечают только за использование сервиса. Классы сервисов создаются в инжекторах. Также, если нам нужно дополнительно расширить наше приложение для возможности отправки сообщений в Facebook, нам нужно будет написать только классы сервисов и инжекторов. Таким образом, реализация внедрения зависимостей решает проблему с жесткой зависимостью и помогает сделать наше приложение гибким и легко расширяемым. Теперь посмотрим, насколько легко мы можем тестировать наш класс приложения, подменяя инжекторы и классы сервисов.
Java Dependency Injection – JUnit Тестовый Кейс с Мок Инжектором и Сервисом
package com.journaldev.java.dependencyinjection.test;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.consumer.MyDIApplication;
import com.journaldev.java.dependencyinjection.injector.MessageServiceInjector;
import com.journaldev.java.dependencyinjection.service.MessageService;
public class MyDIApplicationJUnitTest {
private MessageServiceInjector injector;
@Before
public void setUp(){
//мок инжектор с анонимным классом
injector = new MessageServiceInjector() {
@Override
public Consumer getConsumer() {
//мок сервиса сообщений
return new MyDIApplication(new MessageService() {
@Override
public void sendMessage(String msg, String rec) {
System.out.println("Mock Message Service implementation");
}
});
}
};
}
@Test
public void test() {
Consumer consumer = injector.getConsumer();
consumer.processMessages("Hi Pankaj", "[email protected]");
}
@After
public void tear(){
injector = null;
}
}
Как видите, я использую анонимные классы, чтобы мокировать инжектор и классы сервисов, и легко тестирую методы приложения. Я использую JUnit 4 для вышеприведенного тестового класса, так что убедитесь, что он находится в пути сборки вашего проекта, если вы запускаете вышеприведенный тестовый класс. Мы использовали конструкторы для внедрения зависимостей в классы приложения, другой способ – использовать метод установки для внедрения зависимостей в классы приложения. Для внедрения зависимостей методом установки наш класс приложения будет реализован следующим образом.
package com.journaldev.java.dependencyinjection.consumer;
import com.journaldev.java.dependencyinjection.service.MessageService;
public class MyDIApplication implements Consumer{
private MessageService service;
public MyDIApplication(){}
//внедрение зависимостей методом установки
public void setService(MessageService service) {
this.service = service;
}
@Override
public void processMessages(String msg, String rec){
//выполнить некоторую валидацию сообщения, логику манипуляции и т. д.
this.service.sendMessage(msg, rec);
}
}
package com.journaldev.java.dependencyinjection.injector;
import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.consumer.MyDIApplication;
import com.journaldev.java.dependencyinjection.service.EmailServiceImpl;
public class EmailServiceInjector implements MessageServiceInjector {
@Override
public Consumer getConsumer() {
MyDIApplication app = new MyDIApplication();
app.setService(new EmailServiceImpl());
return app;
}
}
Один из лучших примеров внедрения зависимостей с использованием сеттера – это Интерфейсы Struts2 Servlet API Aware. Решение о том, использовать ли внедрение зависимостей на основе конструктора или на основе сеттера, является решением по дизайну и зависит от ваших требований. Например, если мое приложение вообще не может работать без класса сервиса, то я бы предпочел внедрение зависимостей на основе конструктора, в противном случае я бы выбрал внедрение зависимостей на основе метода сеттера, чтобы использовать его только тогда, когда это действительно необходимо. Внедрение зависимостей в Java – это способ достижения Инверсии управления (IoC) в нашем приложении путем перемещения привязки объектов с момента компиляции до времени выполнения. Мы можем добиться IoC с помощью Шаблона Фабрики, Шаблона Метода Шаблона, Шаблона Стратегии и шаблона Местоположителя службы. Внедрение зависимостей в Spring, Google Guice и фреймворки Java EE CDI облегчают процесс внедрения зависимостей с помощью использования API Java Reflection и аннотаций Java. Все, что нам нужно сделать, это проаннотировать поле, конструктор или метод сеттера и настроить их в конфигурационных файлах xml или классах.
Преимущества инъекции зависимостей в Java
Некоторые из преимуществ использования инъекции зависимостей в Java:
- Разделение забот
- Снижение количества шаблонного кода в классах приложения, потому что всю работу по инициализации зависимостей обрабатывает компонент-инжектор
- Конфигурируемые компоненты делают приложение легко расширяемым
- Модульное тестирование легко с использованием заглушек
Недостатки инъекции зависимостей в Java
Инъекция зависимостей в Java также имеет некоторые недостатки:
- При чрезмерном использовании может привести к проблемам с обслуживанием, потому что эффект изменений известен только во время выполнения.
- Инъекция зависимостей в Java скрывает зависимости служебного класса, что может привести к ошибкам времени выполнения, которые могли бы быть выявлены на этапе компиляции.
Загрузить проект инъекции зависимостей
Вот и все, что касается шаблона инъекции зависимостей в Java. Хорошо знать и использовать его, когда мы контролируем услуги.