Java Dependency Injection – Ejemplo de Patrón de Diseño DI Tutorial

Inyección de Dependencias en Java permite eliminar las dependencias codificadas de forma rígida y hacer que nuestra aplicación sea débilmente acoplada, extensible y mantenible. Podemos implementar inyección de dependencias en Java para trasladar la resolución de dependencias desde el tiempo de compilación hasta el tiempo de ejecución.

Inyección de Dependencias en Java

La inyección de dependencias en Java parece difícil de entender con la teoría, así que tomaré un ejemplo sencillo y luego veremos cómo utilizar el patrón de inyección de dependencias para lograr un acoplamiento débil y una mayor capacidad de extensión en la aplicación. Supongamos que tenemos una aplicación donde consumimos EmailService para enviar correos electrónicos. Normalmente, lo implementaríamos de la siguiente manera.

package com.journaldev.java.legacy;

public class EmailService {

	public void sendEmail(String message, String receiver){
		// lógica para enviar correo electrónico
		System.out.println("Email sent to "+receiver+ " with Message="+message);
	}
}

La clase EmailService contiene la lógica para enviar un mensaje de correo electrónico a la dirección de correo del destinatario. Nuestro código de aplicación se verá así.

package com.journaldev.java.legacy;

public class MyApplication {

	private EmailService email = new EmailService();
	
	public void processMessages(String msg, String rec){
		// realizar alguna validación de mensajes, lógica de manipulación, etc.
		this.email.sendEmail(msg, rec);
	}
}

Nuestro código de cliente que utilizará la clase MyApplication para enviar mensajes de correo electrónico se verá así.

package com.journaldev.java.legacy;

public class MyLegacyTest {

	public static void main(String[] args) {
		MyApplication app = new MyApplication();
		app.processMessages("Hi Pankaj", "[email protected]");
	}

}

A primera vista, no parece haber nada malo con la implementación anterior. Pero el código tiene ciertas limitaciones.

  • La clase MyApplication es responsable de inicializar el servicio de correo electrónico y luego usarlo. Esto conduce a una dependencia codificada en duro. Si deseamos cambiar a algún otro servicio de correo electrónico avanzado en el futuro, requerirá cambios de código en la clase MyApplication. Esto hace que nuestra aplicación sea difícil de ampliar y si el servicio de correo electrónico se utiliza en varias clases, sería aún más difícil.
  • Si queremos ampliar nuestra aplicación para proporcionar una función adicional de mensajería, como SMS o mensajes de Facebook, entonces necesitaríamos escribir otra aplicación para eso. Esto implicará cambios de código en las clases de la aplicación y también en las clases del cliente.
  • Probar la aplicación será muy difícil, ya que nuestra aplicación está creando directamente la instancia del servicio de correo electrónico. No hay forma de simular estos objetos en nuestras clases de prueba.

Alguien podría argumentar que podemos eliminar la creación de la instancia del servicio de correo electrónico de la clase MyApplication teniendo un constructor que requiera el servicio de correo electrónico como argumento.

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){
		//realizar alguna validación de mensajes, lógica de manipulación, etc.
		this.email.sendEmail(msg, rec);
	}
}

Pero en este caso, estamos pidiendo a las aplicaciones cliente o a las clases de prueba que inicialicen el servicio de correo electrónico, lo cual no es una buena decisión de diseño. Ahora veamos cómo podemos aplicar el patrón de inyección de dependencias en Java para resolver todos los problemas con la implementación anterior. La inyección de dependencias en Java requiere al menos lo siguiente:

  1. Los componentes de servicio deben estar diseñados con una clase base o interfaz. Es mejor preferir interfaces o clases abstractas que definan el contrato para los servicios.
  2. Las clases consumidoras deben estar escritas en términos de la interfaz del servicio.
  3. Clases de inyección que inicializarán los servicios y luego las clases consumidoras.

Inyección de Dependencias en Java – Componentes de Servicio

Para nuestro caso, podemos tener MessageService que declarará el contrato para las implementaciones de servicios.

package com.journaldev.java.dependencyinjection.service;

public interface MessageService {

	void sendMessage(String msg, String rec);
}

Ahora digamos que tenemos servicios de correo electrónico y SMS que implementan las interfaces anteriores.

package com.journaldev.java.dependencyinjection.service;

public class EmailServiceImpl implements MessageService {

	@Override
	public void sendMessage(String msg, String rec) {
		// lógica para enviar correo electrónico
		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) {
		// lógica para enviar SMS
		System.out.println("SMS sent to "+rec+ " with Message="+msg);
	}

}

Nuestros servicios de inyección de dependencias en Java están listos y ahora podemos escribir nuestra clase consumidora.

Inyección de Dependencias en Java – Consumidor de Servicio

No es necesario tener interfaces base para las clases consumidoras, pero tendré una Consumer interfaz que declara el contrato para las clases consumidoras.

package com.journaldev.java.dependencyinjection.consumer;

public interface Consumer {

	void processMessages(String msg, String rec);
}

Mi implementación de clase consumidora es como sigue.

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){
		// hacer alguna validación de mensaje, lógica de manipulación, etc.
		this.service.sendMessage(msg, rec);
	}

}

Notamos que nuestra clase de aplicación solo está utilizando el servicio. No inicializa el servicio, lo que lleva a una mejor “separación de preocupaciones“. Además, el uso de la interfaz de servicio nos permite probar fácilmente la aplicación mediante la simulación del MessageService y vincular los servicios en tiempo de ejecución en lugar de en tiempo de compilación. Ahora estamos listos para escribir clases de inyección de dependencias en Java que inicializarán el servicio y también las clases consumidoras.

Inyección de Dependencias en Java – Clases de Injectores

Creemos una interfaz MessageServiceInjector con una declaración de método que devuelve la clase Consumer.

package com.journaldev.java.dependencyinjection.injector;

import com.journaldev.java.dependencyinjection.consumer.Consumer;

public interface MessageServiceInjector {

	public Consumer getConsumer();
}

Ahora, para cada servicio, tendremos que crear clases de inyección como se muestra a continuación.

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());
	}

}

Ahora veamos cómo nuestras aplicaciones cliente utilizarán la aplicación con un programa simple.

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;
		
		//Enviar correo electrónico
		injector = new EmailServiceInjector();
		app = injector.getConsumer();
		app.processMessages(msg, email);
		
		//Enviar SMS
		injector = new SMSServiceInjector();
		app = injector.getConsumer();
		app.processMessages(msg, phone);
	}

}

Como puedes ver, nuestras clases de aplicación son responsables solo de usar el servicio. Las clases de servicio se crean en los injectores. Además, si tenemos que ampliar aún más nuestra aplicación para permitir el envío de mensajes de Facebook, solo tendremos que escribir clases de servicio e injectores. Así que la implementación de la inyección de dependencias resolvió el problema con la dependencia codificada y nos ayudó a hacer nuestra aplicación flexible y fácil de ampliar. Ahora veamos lo fácil que es probar nuestra clase de aplicación simulando los injectores y las clases de servicio.

Inyección de Dependencias en Java – Caso de Prueba de JUnit con Inyector y Servicio Falsos

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(){
		//falsificar el inyector con una clase anónima
		injector = new MessageServiceInjector() {
			
			@Override
			public Consumer getConsumer() {
				//falsificar el servicio de mensajes
				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;
	}

}

Como puedes ver, estoy utilizando clases anónimas para falsificar las clases de inyector y servicio y puedo probar fácilmente los métodos de mi aplicación. Estoy utilizando JUnit 4 para la clase de prueba anterior, así que asegúrate de que esté en la ruta de construcción de tu proyecto si estás ejecutando la clase de prueba anterior. Hemos utilizado constructores para inyectar las dependencias en las clases de la aplicación; otra forma es utilizar un método setter para inyectar dependencias en las clases de la aplicación. Para la inyección de dependencias mediante el método setter, nuestra clase de aplicación se implementará de la siguiente manera.

package com.journaldev.java.dependencyinjection.consumer;

import com.journaldev.java.dependencyinjection.service.MessageService;

public class MyDIApplication implements Consumer{

	private MessageService service;
	
	public MyDIApplication(){}

	//inyección de dependencias mediante setters	
	public void setService(MessageService service) {
		this.service = service;
	}

	@Override
	public void processMessages(String msg, String rec){
		//realizar alguna validación de mensajes, lógica de manipulación, 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;
	}

}

Uno de los mejores ejemplos de inyección de dependencias mediante setter es Interfaces Servlet API Aware de Struts2. Si usar la inyección de dependencias basada en constructor o en setter es una decisión de diseño que depende de sus requisitos. Por ejemplo, si mi aplicación no puede funcionar en absoluto sin la clase de servicio, entonces preferiría la inyección basada en constructor; de lo contrario, optaría por la inyección basada en métodos setter para usarla solo cuando realmente sea necesario. La inyección de dependencias en Java es una forma de lograr la Inversión de control (IoC) en nuestra aplicación al mover la vinculación de objetos desde el tiempo de compilación hasta el tiempo de ejecución. Podemos lograr IoC a través del Patrón Factory, el Patrón de diseño Template Method, el Patrón de diseño Strategy y también el patrón Service Locator. Los frameworks de Spring Dependency Injection, Google Guice y Java EE CDI facilitan el proceso de inyección de dependencias mediante el uso de Java Reflection API y anotaciones de Java. Todo lo que necesitamos es anotar el campo, constructor o método setter y configurarlos en archivos XML de configuración o clases.

Beneficios de la Inyección de Dependencias en Java

Algunos de los beneficios de utilizar la Inyección de Dependencias en Java son:

  • Separación de preocupaciones
  • Reducción del código redundante en las clases de la aplicación, ya que todo el trabajo de inicialización de las dependencias es manejado por el componente de inyección
  • Los componentes configurables hacen que la aplicación sea fácilmente ampliable
  • Las pruebas unitarias son fáciles de realizar con objetos simulados

Desventajas de la Inyección de Dependencias en Java

La Inyección de Dependencias en Java también tiene algunas desventajas:

  • Si se abusa de ella, puede generar problemas de mantenimiento porque los efectos de los cambios se conocen en tiempo de ejecución.
  • La inyección de dependencias en Java oculta las dependencias de la clase de servicio, lo que puede provocar errores en tiempo de ejecución que podrían haberse detectado en tiempo de compilación.

Descarga del Proyecto de Inyección de Dependencias

Eso es todo sobre el patrón de inyección de dependencias en Java. Es bueno conocerlo y usarlo cuando tenemos control sobre los servicios.

Source:
https://www.digitalocean.com/community/tutorials/java-dependency-injection-design-pattern-example-tutorial