Поиск по сайту:

Внедрение зависимостей Java — пример учебного шаблона проектирования DI


Шаблон проектирования Java Dependency Injection позволяет нам удалить жестко закодированные зависимости и сделать наше приложение слабо связанным, расширяемым и удобным в сопровождении. Мы можем реализовать внедрение зависимостей в java, чтобы перенести разрешение зависимостей из времени компиляции во время выполнения.

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

Внедрение зависимостей в Java кажется сложным для понимания с точки зрения теории, поэтому я возьму простой пример, а затем мы увидим, как использовать шаблон внедрения зависимостей для достижения слабой связи и расширяемости в приложении. Допустим, у нас есть приложение, в котором мы используем EmailService для отправки электронных писем. Обычно мы реализуем это, как показано ниже.

package com.journaldev.java.legacy;

public class EmailService {

	public void sendEmail(String message, String receiver){
		//logic to send email
		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){
		//do some msg validation, manipulation logic etc
		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", "pankaj@abc.com");
	}

}

На первый взгляд кажется, что в приведенной выше реализации нет ничего плохого. Но приведенная выше логика кода имеет определенные ограничения.

    Класс
  • 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){
		//do some msg validation, manipulation logic etc
		this.email.sendEmail(msg, rec);
	}
}

Но в этом случае мы просим клиентские приложения или тестовые классы инициализировать службу электронной почты, что не является хорошим дизайнерским решением. Теперь давайте посмотрим, как мы можем применить шаблон внедрения зависимостей Java для решения всех проблем с приведенной выше реализацией. Для внедрения зависимостей в java требуется как минимум следующее:

  1. Компоненты службы должны разрабатываться с использованием базового класса или интерфейса. Лучше предпочесть интерфейсы или абстрактные классы, которые определят контракт на услуги.
  2. Потребительские классы должны быть написаны с точки зрения интерфейса службы.
  3. Класс-инжектор, который будет инициализировать службы, а затем классы-потребители.

Внедрение зависимостей 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) {
		//logic to send email
		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) {
		//logic to send 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){
		//do some msg validation, manipulation logic etc
		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 = "pankaj@abc.com";
		String phone = "4088888888";
		MessageServiceInjector injector = null;
		Consumer app = null;
		
		//Send email
		injector = new EmailServiceInjector();
		app = injector.getConsumer();
		app.processMessages(msg, email);
		
		//Send SMS
		injector = new SMSServiceInjector();
		app = injector.getConsumer();
		app.processMessages(msg, phone);
	}

}

Как видите, наши классы приложений отвечают только за использование сервиса. Сервисные классы создаются в инжекторах. Кроме того, если нам нужно еще больше расширить наше приложение, чтобы разрешить обмен сообщениями в Facebook, нам придется писать только классы обслуживания и классы инжектора. Таким образом, реализация внедрения зависимостей решила проблему с жестко закодированной зависимостью и помогла нам сделать наше приложение гибким и простым в расширении. Теперь давайте посмотрим, как легко мы можем протестировать наш класс приложения, смоделировав классы инжектора и сервиса.

Внедрение зависимостей Java — тестовый пример 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(){
		//mock the injector with anonymous class
		injector = new MessageServiceInjector() {
			
			@Override
			public Consumer getConsumer() {
				//mock the message service
				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", "pankaj@abc.com");
	}
	
	@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(){}

	//setter dependency injection	
	public void setService(MessageService service) {
		this.service = service;
	}

	@Override
	public void processMessages(String msg, String rec){
		//do some msg validation, manipulation logic etc
		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;
	}

}

Одним из лучших примеров внедрения зависимостей сеттера являются аннотации Java. Все, что нам нужно, это аннотировать поле, конструктор или метод установки и настроить их в XML-файлах или классах конфигурации.

Преимущества внедрения зависимостей Java

Некоторые из преимуществ использования внедрения зависимостей в Java:

  • Разделение проблем
  • Сокращение стандартного кода в классах приложений, поскольку вся работа по инициализации зависимостей выполняется компонентом инжектора
  • Настраиваемые компоненты позволяют легко расширять приложение
  • Моделирование объектов упрощает модульное тестирование

Недостатки внедрения зависимостей Java

Внедрение зависимостей Java также имеет некоторые недостатки:

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

Скачать проект внедрения зависимостей

Это все, что касается шаблона внедрения зависимостей в java. Хорошо знать и использовать его, когда мы контролируем услуги.