El patrón de diseño de inyección de dependencias de Java nos permite eliminar las dependencias codificadas y hacer que nuestra aplicación sea flexible, ampliable y mantenible. Podemos implementar la inyección de dependencias en Java para trasladar la resolución de dependencias del tiempo de compilación al tiempo de ejecución.
Inyección de dependencia de Java
La inyección de dependencias en Java parece difícil de comprender en teoría, por lo que tomaré un ejemplo simple y luego veremos cómo usar el patrón de inyección de dependencias para lograr un acoplamiento flexible y capacidad de extensión en la aplicación. Supongamos que tenemos una aplicación en la que consumimos EmailServicepara enviar correos electrónicos. Normalmente, lo implementaríamos como se muestra a continuación.
package com.journaldev.java.legacy;public class EmailService {public void sendEmail(String message, String receiver){//logic to send emailSystem.out.println("Email sent to "+receiver+ " with Message="+message);}}
EmailServiceLa clase contiene la lógica para enviar un mensaje de correo electrónico a la dirección de correo electrónico del destinatario. El código de nuestra aplicación será el siguiente.
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 etcthis.email.sendEmail(msg, rec);}}
Nuestro código de cliente que usará MyApplicationla clase para enviar mensajes de correo electrónico será como el que se muestra a continuación.
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");}}
A primera vista, no parece que haya nada malo en la implementación anterior, pero la lógica del código anterior tiene ciertas limitaciones.
MyApplicationLa clase MyApplication es responsable de inicializar el servicio de correo electrónico y luego usarlo. Esto genera dependencias codificadas. Si queremos cambiar a otro servicio de correo electrónico avanzado en el futuro, será necesario realizar cambios en el código de la clase MyApplication. Esto hace que sea difícil ampliar nuestra aplicación y, si el servicio de correo electrónico se usa en varias clases, será aún más difícil.- Si queremos ampliar nuestra aplicación para ofrecer una función de mensajería adicional, como SMS o mensajes de Facebook, entonces necesitaremos escribir otra aplicación para ello. 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 crea directamente la instancia del servicio de correo electrónico. No hay forma de que podamos simular estos objetos en nuestras clases de prueba.
Se puede argumentar que podemos eliminar la creación de la instancia del servicio de correo electrónico de MyApplicationla clase al tener 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){//do some msg validation, manipulation logic etcthis.email.sendEmail(msg, rec);}}
Pero en este caso, estamos pidiendo a las aplicaciones cliente o clases de prueba que inicialicen el servicio de correo electrónico, lo que no es una buena decisión de diseño. Ahora veamos cómo podemos aplicar el patrón de inyección de dependencia de Java para resolver todos los problemas con la implementación anterior. La inyección de dependencia en Java requiere al menos lo siguiente:
- Los componentes de servicio deben diseñarse con una clase base o una interfaz. Es mejor preferir interfaces o clases abstractas que definan el contrato para los servicios.
- Las clases de consumidor deben escribirse en términos de interfaz de servicio.
- Clases inyectoras que inicializarán los servicios y luego las clases consumidoras.
Inyección de dependencias de Java: componentes de servicio
Para nuestro caso podemos tener MessageServiceque declarar el contrato para 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 {@Overridepublic void sendMessage(String msg, String rec) {//logic to send emailSystem.out.println("Email sent to "+rec+ " with Message="+msg);}}
package com.journaldev.java.dependencyinjection.service;public class SMSServiceImpl implements MessageService {@Overridepublic void sendMessage(String msg, String rec) {//logic to send SMSSystem.out.println("SMS sent to "+rec+ " with Message="+msg);}}
Nuestros servicios de inyección de dependencia Java están listos y ahora podemos escribir nuestra clase de consumidor.
Inyección de dependencias de Java: consumidor de servicios
No estamos obligados a tener interfaces base para las clases de consumidores, pero tendré una Consumerinterfaz que declare el contrato para las clases de consumidores.
package com.journaldev.java.dependencyinjection.consumer;public interface Consumer {void processMessages(String msg, String rec);}
Mi implementación de clase de consumidor es como se muestra a continuación.
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;}@Overridepublic void processMessages(String msg, String rec){//do some msg validation, manipulation logic etcthis.service.sendMessage(msg, rec);}}
Tenga en cuenta que nuestra clase de aplicación solo utiliza el servicio. No inicializa el servicio, lo que conduce a una mejor ” separación de preocupaciones “. Además, el uso de la interfaz de servicio nos permite probar fácilmente la aplicación simulando MessageService y vinculando los servicios en tiempo de ejecución en lugar de en tiempo de compilación. Ahora estamos listos para escribir clases de inyectores de dependencia de Java que inicializarán el servicio y también las clases de consumidor.
Inyección de dependencias en Java: clases inyectoras
Tengamos una interfaz MessageServiceInjectorcon declaración de método que devuelva la Consumerclase.
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 inyector 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 {@Overridepublic 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 {@Overridepublic 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 = "pankaj@abc.com";String phone = "4088888888";MessageServiceInjector injector = null;Consumer app = null;//Send emailinjector = new EmailServiceInjector();app = injector.getConsumer();app.processMessages(msg, email);//Send SMSinjector = new SMSServiceInjector();app = injector.getConsumer();app.processMessages(msg, phone);}}
Como puede ver, nuestras clases de aplicación son responsables únicamente de usar el servicio. Las clases de servicio se crean en inyectores. Además, si tenemos que ampliar aún más nuestra aplicación para permitir la mensajería de Facebook, tendremos que escribir solo clases de servicio y clases de inyectores. Por lo tanto, la implementación de la inyección de dependencia resolvió el problema con la dependencia codificada y nos ayudó a hacer que nuestra aplicación sea flexible y fácil de extender. Ahora veamos con qué facilidad podemos probar nuestra clase de aplicación simulando las clases de inyector y de servicio.
Inyección de dependencias en Java: caso de prueba JUnit con inyector y servicio simulados
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;@Beforepublic void setUp(){//mock the injector with anonymous classinjector = new MessageServiceInjector() {@Overridepublic Consumer getConsumer() {//mock the message servicereturn new MyDIApplication(new MessageService() {@Overridepublic void sendMessage(String msg, String rec) {System.out.println("Mock Message Service implementation");}});}};}@Testpublic void test() {Consumer consumer = injector.getConsumer();consumer.processMessages("Hi Pankaj", "pankaj@abc.com");}@Afterpublic void tear(){injector = null;}}
Como puedes ver, estoy usando clases anónimas para simular las clases de inyector y servicio y puedo probar fácilmente mis métodos de aplicación. Estoy usando JUnit 4 para la clase de prueba anterior, así que asegúrate de que esté en la ruta de compilación de tu proyecto si estás ejecutando la clase de prueba anterior. Hemos usado constructores para inyectar las dependencias en las clases de aplicación, otra forma es usar un método setter para inyectar dependencias en las clases de aplicación. Para la inyección de dependencia del método setter, nuestra clase de aplicación se implementará como se muestra a continuación.
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 injectionpublic void setService(MessageService service) {this.service = service;}@Overridepublic void processMessages(String msg, String rec){//do some msg validation, manipulation logic etcthis.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 {@Overridepublic Consumer getConsumer() {MyDIApplication app = new MyDIApplication();app.setService(new EmailServiceImpl());return app;}}
Uno de los mejores ejemplos de inyección de dependencia de setter son las interfaces Struts2 Servlet API Aware . Si se utiliza la inyección de dependencia basada en el constructor o basada en setter es una decisión de diseño y depende de sus requisitos. Por ejemplo, si mi aplicación no puede funcionar en absoluto sin la clase de servicio, entonces preferiría DI basada en el constructor o bien optaría por DI basada en el método setter para utilizarla solo cuando sea realmente necesaria. La inyección de dependencia en Java es una forma de lograr la inversión de control ( IoC ) en nuestra aplicación moviendo la vinculación de objetos del tiempo de compilación al tiempo de ejecución. También podemos lograr la IoC a través del patrón Factory , el patrón de diseño de método de plantilla , el patrón de estrategia y el patrón Service Locator. Los marcos Spring Dependency Injection , Google Guice y Java EE CDI facilitan el proceso de inyección de dependencia mediante el uso de Java Reflection API y anotaciones de Java . Todo lo que necesitamos es anotar el campo, el constructor o el 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 usar la inyección de dependencia en Java son:
- Separación de preocupaciones
- Reducción de código repetitivo en las clases de aplicación porque todo el trabajo para inicializar dependencias lo maneja el componente inyector
- Los componentes configurables hacen que la aplicación sea fácilmente ampliable
- Las pruebas unitarias son fáciles con objetos simulados
Desventajas de la inyección de dependencias en Java
La inyección de dependencia de Java también tiene algunas desventajas:
- Si se usa en exceso, puede generar problemas de mantenimiento porque el efecto de los cambios se conoce en tiempo de ejecución.
- La inyección de dependencia en Java oculta las dependencias de la clase de servicio que pueden provocar errores de tiempo de ejecución que se habrían detectado en el momento de la compilación.
Descargar Proyecto de Inyección de Dependencias
Eso es todo sobre el patrón de inyección de dependencia en Java . Es bueno conocerlo y usarlo cuando tenemos el control de los servicios.