Tutorial: Diseño de Patrón con Inyección de Dependencias DI con Java

El Diseño de Patrón con Inyección de Dependencias (DI Dependency Injection por sus siglas en inglés) nos permite eliminar las dependencias no modificables, hacemos nuestra aplicación extensible y un mantenimiento más simple. Podemos implementar el patrón de inyección de independencias en tiempo de ejecución en vez de la manera tradicional, que es en tiempo de compilación.

El Diseño de Patrón con Inyección de Dependencias parece difícil de entender con la teoría, así que tomamos un ejemplo sencillo y luego vamos a ver cómo utilizar el patrón de inyección de dependencias para lograr la articulación flexible y posibilidad de ampliación en la aplicación.

Digamos que tenemos una aplicación en la que consultamos desde el mostrador de una tienda de cómputo un stock de equipos de cómputo (servidores, laptops, tabletas, teléfonos inteligentes). Al ejecutar una consulta, el vendedor obtendrá las características que el cliente le pide. Normalmente un desarrollo es como a continuación.

package com.iod.consultas.legados;

public class TabletConsulta {
   private int IdArticulo;
   private int CantidadDD;
   private String CapacidadDD;
   private String SistemaO;
   private double Precio;
   private String Memoria;
   private String TMonitor;

   public void DatoTabletConsulta(String Marca) {

   //.. setter getter
   //.. consulta a la de datos, consulta por web services, rest, jms, etc.

   // Aquí lo datos datos
   IdArticulo = 4934;
   CantidadDD = 4;
   CapacidadDD = "500G";
   SistemaO = "Android";
   Precio = 6999.34;
   Memoria = "16G";
   TMonitor = "7\"";

   System.out.println( "Categoria Tablet. Marca " +
   " Artículo " + IdArticulo + " Cantidad " + CantidadDD +
   " Capacidad " + CapacidadDD + "Sistem Operativo " + SistemaO +
   " Precio " + Precio + " Memoria " + Memoria +
   " Tamaño de Monitor " + TMonitor );
   }
}

La aplicación para  la consulta es la siguiente:

package com.iod.consultas.legados;

public class AplicacionConsulta {
    private TabletConsulta consulta = new TabletConsulta();

    public void ProcesaConsulta( String marca) {
       this.consulta.DatoTabletConsulta(marca);
    }
}

Una aplicación en el cliente que para usar la consulta, puede ser la siguiente:

package com.iod.consultas.legados;

public class ConsultaMostrador {

   public static void main(String[] args) {
   TabletConsulta consulta = new TabletConsulta();
   AplicacionConsulta app = new AplicacionConsulta();
   app.ProcesaConsulta("Lenovo");
   }
}

A primera vista, no parece nada malo la aplicación anterior. Pero la lógica de código anterior tiene ciertas limitaciones.

La clase AplicacionConsulta es responsable de inicializar el servicio de consultas y luego lo usa. Esto conduce a la que dependencia no es modificable. Si queremos cambiar a algún otro tipo de consulta en el futuro, laptops, servidores, teléfonos inteligentes, se requerirá de cambios en el código de la clase AplicacionConsulta. Esto hace que nuestra aplicación sea difícil de extender,  y si el servicio de consultas utiliza en múltiples clases entonces eso sería aún más difícil.

Si queremos ampliar nuestra aplicación para proporcionar la función de mensajería a más consultas adicionales de otros artículos de cómputo con otros atributos, entonces tendríamos que escribir otra aplicación para eso. Esto implicará cambios en el código de las clases de la aplicación y en las clases de cliente también.

Para probar la aplicación va a ser muy difícil, ya que nuestra aplicación está creando directamente la instancia de consulta de las características de una Tableta. No hay manera de que podamos omitir estos objetos en nuestras clases de prueba.

Se puede argumentar que podemos quitar la creación de la instancia de la consulta de la tableta de la clase AplicacionConsulta para  tener un constructor que requiere el servicio de consulta de tableta como argumento:

package com.iod.consultas.legados;

public class AplicacionConsultaArgumento {

   private TabletConsulta ConsultaTab = null;

   public AplicacionConsultaArgumento( TabletConsulta consulta ) {
      this.ConsultaTab = consulta;
   }
   public void ProcesaConsulta( String marca ) {
     this.ConsultaTab.DatoTabletConsulta(marca);
   }
}
package com.iod.consultas.legados;

public class ConsultaMostrador {
   public static void main(String[] args) {
      TabletConsulta consulta = new TabletConsulta();
      AplicacionConsultaArgumento app = new AplicacionConsultaArgumento(consulta);
      app.ProcesaConsulta("Lenovo");
   }
}

Pero en este caso, estamos pidiendo a las aplicaciones del cliente ConsultaMostrador que inicie el servicio de consulta de la Tableta y no es una buena decisión de diseño.

Ahora vamos a ver cómo podemos aplicar la dependencia patrón de inyección de resolver todos los problemas con la aplicación anterior. El Patrón de Inyección de Dependencia (DI por sus siglas en inglés) requiere por lo menos lo siguiente:

  • Componentesde servicios debenser diseñadoscon la clasebase oEsmejor preferirinterfaces oclases abstractasque definirían elcontrato paralosservicios.
  • Clasesde consumodeben ser escritas entérminos de la interfaz de servicio.
  • Clases deinyectoresqueinicializarlos servicios yluego lasclasesde consumidores.

Componentes de servicio

Para nuestro caso, podemos tener la interfaz ConsultaServicio en donde se declara  las implementaciones de servicios.

package com.io.inyection.service;

public interface ConsultaServicio {
   void sendConsultaEquipo(String marca);
   }

Ahora vamos a decir que tenemos consultas de equipos Laptops, Servidores, Tabletas, Teléfonos Inteligentes de servicios que implementan las interfaces anteriores. Suponemos que cada implementación vienen de diferentes fuentes de información: Bases de datos, web cervices, JMS, REST, streamings, etc. (En este tutorial no importa la fuente de datos).

LaptopServicioImpl

package com.io.inyection.service;

public class LaptopServicioImpl implements ConsultaServicio {

private int IdArticulo;
private String CapacidadDD;
private String SistemaO;
private double Precio;
private String Memoria;
private String TMonitor;

public void sendConsultaEquipo( String Marca) {

   //.. setter & getter
   //.. consulta a al base de datos

   // Aquí lo datos datos
   IdArticulo = 5611;
   CapacidadDD = "1Tb";
   SistemaO = "WIndows 7 Home Premium";
   Precio = 13456.12;
   Memoria = "8G";
   TMonitor = "15\"";

   System.out.println( "Categoria Laptop. Marca " + Marca +
   " Artículo " + IdArticulo +
   " Capacidad " + CapacidadDD + "Sistem Operativo " + SistemaO +
   " Precio " + Precio + " Memoria " + Memoria +
   " Tamaño de Monitor " + TMonitor + "\n" );
   }
}

ServidorServicioImpl

package com.io.inyection.service;

public class ServidorServicioImpl implements ConsultaServicio {

private int IdArticulo;
private int CantidadDD;
private String CapacidadDD;
private String SistemaO;
private double Precio;
private String Memoria;

public void sendConsultaEquipo(  String Marca) {

   //.. setter getter
   //.. consulta a al base de datos

   // Aquí lo datos datos
   IdArticulo = 5402;
   CantidadDD = 4;
   CapacidadDD = "2Tb";
   SistemaO = "RedHat Linux 6.5";
   Precio = 28635.45;
   Memoria = "8G";

   System.out.println( "Categoria Servidor. Marca " + Marca +
   " Artículo " + IdArticulo + " Cantidad de Discos Duros " + CantidadDD +
   " Capacidad " + CapacidadDD + "Sistem Operativo " + SistemaO +
   " Precio " + Precio + " Memoria " + Memoria + "\n" );
   }
}

TabletServicioImpl

package com.io.inyection.service;

public class TabletServicioImpl implements ConsultaServicio {
private int IdArticulo;
private String SistemaO;
private double Precio;
private String Memoria;
private String TMonitor;

public void sendConsultaEquipo(  String Marca) {

   //.. setter & getter
   //.. consulta a al base de datos

   // Aquí lo datos datos
   IdArticulo = 4934;
   SistemaO = "Android";
   Precio = 6999.34;
   Memoria = "16G";
   TMonitor = "7\"";

   System.out.println( "Categoria Tablet. Marca " + Marca +
   " Artículo " + IdArticulo +
   " Sistem Operativo " + SistemaO +
   " Precio " + Precio + " Memoria " + Memoria +
   " Tamaño de Monitor " + TMonitor + "\n" );      
   }
}

TelInteligenteServicioImpl

package com.io.inyection.service;

public class TelInteligenteServicioImpl implements ConsultaServicio {

private int IdArticulo;
private String SistemaO;
private double Precio;
private String Memoria;
private String TMonitor;

public void sendConsultaEquipo(String Marca) {

   //.. setter & getter
   //.. consulta a al base de datos

   // Aquí lo datos datos
   IdArticulo = 4598;
   SistemaO = "Andriod Kitkat 2.0";
   Precio = 8959.54;
   Memoria = "16G";
   TMonitor = "10\"";

   System.out.println( "Categoria Telefono Inteligente. Marca " + Marca +
   " Artículo " + IdArticulo +
   " Sistem Operativo " + SistemaO +
   " Precio " + Precio + " Memoria " + Memoria +
   " Tamaño de Monitor " + TMonitor + "\n" );

   }
}

Consumidores de Servicios

No estamos obligados a tener interfaces base para las clases de consumo, pero se  tiene un método declarando en la interfaz. El Consumidor para las clases consumidoras. (clase consumer).

package com.io.inyection.consumer;

public interface Consumer {
   void processMessages(String marca);
}

Aplicacion

package com.io.inyection.consumer;

import com.io.inyection.service.ConsultaServicio;

public class Aplicacion implements Consumer {

   private ConsultaServicio service;

   Aplicacion( ConsultaServicio svc ) {
      this.service = svc;
   }

   public void processMessages(String marca) {
      // posiblemente se puedan hacer aqui algunas validaciones para obtener la valor a consultar
      this.service.sendConsultaEquipo(marca);
   }
}

Tomar en cuenta que nuestra clase Aplicación es sólo con el servicio. Al no inicializar el servicio, conduce a una mejor “separación de la modularidad de la aplicación”. También utilizamos la interfaz de servicio, que nos permite probar fácilmente la aplicación y también probar por separado Consulta Servicio,  atamos los servicios en tiempo de ejecución en lugar de tiempo de compilación.

Ahora estamos listos para escribir clases de inyección que inicializan el servicio y también clases de consumidoras.

El acto de crear asociaciones entre las aplicaciones comúnmente se refieren como wiring

Inyección

Pongamos una interfaz ConsultaServiceInjectorcon declaración de método que devuelve la clase de los consumidores.

package com.io.inyection.inyector;

import com.io.inyection.consumer.Consumer;

public interface ConsultaServiceInyector {
 public Consumer getConsumer();
 }

Ahora, para cada servicio, tendremos que crear clases de inyectores

ServidorServicioInyector

package com.io.inyection.inyector;

import com.io.inyection.consumer.Consumer;
import com.io.inyection.consumer.Aplicacion;
import com.io.inyection.service.ServidorServicioImpl;

public class ServidorServicioInyector implements ConsultaServiceInyector {

   public Consumer getConsumer() {
      return new Aplicacion( new ServidorServicioImpl());
   }
}

LaptopServicioInyector

package com.io.inyection.inyector;

import com.io.inyection.consumer.Aplicacion;
import com.io.inyection.consumer.Consumer;
import com.io.inyection.service.LaptopServicioImpl;

public class LaptopServicioInyector implements ConsultaServiceInyector {

   public Consumer getConsumer() {
      return new Aplicacion( new LaptopServicioImpl());
   }
}

TabletServicioInyector

package com.io.inyection.inyector;

import com.io.inyection.consumer.Aplicacion;
import com.io.inyection.consumer.Consumer;
import com.io.inyection.service.TabletServicioImpl;

public class TabletServicioInyector implements ConsultaServiceInyector {

   public Consumer getConsumer() {
      return new Aplicacion( new TabletServicioImpl());
   }
}

TelInteligenteServicioInyector

package com.io.inyection.inyector;
import com.io.inyection.consumer.Aplicacion;
import com.io.inyection.consumer.Consumer;
import com.io.inyection.service.TelInteligenteServicioImpl;
public class TelInteligenteServicioInyector implements ConsultaServiceInyector {
   public Consumer getConsumer() {
      return new Aplicacion( new TelInteligenteServicioImpl() );
   }
}

Ahora vamos a ver cómo nuestras aplicaciones cliente utilizarán la aplicación con un programa sencillo.

package com.io.inyection.consultas;

import com.io.inyection.consumer.Consumer;
import com.io.inyection.inyector.ConsultaServiceInyector;
import com.io.inyection.inyector.LaptopServicioInyector;
import com.io.inyection.inyector.ServidorServicioInyector;
import com.io.inyection.inyector.TabletServicioInyector;
import com.io.inyection.inyector.TelInteligenteServicioInyector;

public class ConsultaEquipoMostrador {

   public static void main(String[] args) {
      String marca = "Lenovo";
      ConsultaServiceInyector inyector = null;
      Consumer app = null;

      //consulta laptop
     inyector = new LaptopServicioInyector();
     app = inyector.getConsumer();
     app.processMessages(marca);

     //consulta servidor
     inyector = new ServidorServicioInyector();
     app = inyector.getConsumer();
     app.processMessages(marca);

     //consulta tableta
     inyector = new TabletServicioInyector();
     app = inyector.getConsumer();
     app.processMessages(marca);

     //consulta telefono inteligenre
     inyector = new TelInteligenteServicioInyector();
     app = inyector.getConsumer();
     app.processMessages(marca);
     }
}

Como se puede ver nuestras clases de la aplicación son responsables sólo para usar el servicio. Las clases del servicio se crean en los inyectores. Además, si tenemos que ampliar aún más nuestra aplicación para permitir más consultas de equipos de cómputo o accesorios, vamos a tener que escribir las clases de servicios y clases de inyector solamente.

Así que la dependencia de la aplicación de la inyección nos resuelve el problema con la dependencia no modificable y nos ayudó a hacer nuestra aplicación flexible y fácil de extender. Ahora vamos a ver la facilidad con que podemos probar nuestra clase de aplicación al burlarse de las clases de los inyectores y de servicios.

Caso de prueba JUnit con Inyector y Servicio

package com.iod.consultas.computo;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import com.io.inyection.consumer.Aplicacion;
import com.io.inyection.consumer.Consumer;
import com.io.inyection.inyector.ConsultaServiceInyector;
import com.io.inyection.service.ConsultaServicio;

public class AplicacionTest {

private ConsultaServiceInyector injector;

@Before
public void setUp(){
   injector = new ConsultaServiceInyector() {
   public Consumer getConsumer() {
      return new Aplicacion( new ConsultaServicio() {
         public void sendConsultaEquipo(String marca) {
         System.out.println("Simulando un mensaje de una consulta");
         }
      });
   }
};

}

@Test
public void test() {
   Consumer consumer = injector.getConsumer();
   consumer.processMessages("Lenovo");
}

@After
public void tear(){
   injector = null;
   }
}

Como se puede ver se está usando clases anónimas para la simulación de las clases de los inyectores y de servicio y se  puede probar fácilmente los métodos de aplicación.

Nota: Se usa JUnit 4 para la clase de prueba anterior.
Se ha utilizado los constructores para inyectar las dependencias en las clases de la aplicación, otra manera el usar el método origen  es inyectar las dependencias en las clases en aplicación. El uso de origen  de inyección método de dependencia, de nuestra clase de la aplicación se lleva a cabo como se muestra continuación a continuación.

package com.io.inyection.consumer;

import com.io.inyection.consumer.Consumer;
import com.io.inyection.service.ConsultaServicio;

public class DIApplication implements Consumer{
   private ConsultaServicio service;
   public DIApplication() {}
   public void setService(ConsultaServicio service) {
   this.service = service;
   }

   public void processMessages(String marca){
    // posiblemente se puedan hacer aqui algunas validaciones para obtener la valor a consultar
    this.service.sendConsultaEquipo(marca);
   }
}

Y el inyector de una implementación queda como:

package com.io.inyection.inyector;

import com.io.inyection.consumer.Consumer;
import com.io.inyection.consumer.DIApplication;
import com.io.inyection.service.LaptopServicioImpl;

public class DILaptopServicioInyector implements ConsultaServiceInyector {

   public Consumer getConsumer() {
   DIApplication app = new DIApplication();
   app.setService(new LaptopServicioImpl() );
      return app;
   }
}

Ya sea para usar Constructor inyección basado de dependencias o por medio de hacer un “setter” es una decisión de diseño y depende de sus necesidades. Por ejemplo, si la aplicación no puede funcionar en absoluto sin una clase de servicio entonces preferiríamos una DI basado en un constructor o de lo contrario sería un método setter basada DI usarlo sólo cuando es realmente necesario.

La Inyección de Dependencia es una manera de lograr la Inversión de Control (IoC) en una  aplicación para los objetos de enlace de tiempo de compilación en vez de ser en tiempo de ejecución.

Spring, Google Guice y Java EE CDI facilitan el proceso de inyección de dependencia a través del uso de Java API de Java Reflexión y sus anotaciones. Todo lo que necesitamos es para anotar el campo, método constructor o colocador y configurarlos en los archivos XML de configuración y de clases.

Beneficios de la inyección de dependencias

Algunos de los beneficios del uso de la inyección de dependencias son:

  • Separación de las implementaciones y sus casos derivados de uso.
  • Reducción decódigo repetitivoenclases de la aplicacióndebido atodo el trabajopara inicializardependenciases manejado porel componentedel inyector.
  • Componentesconfigurableshace quela aplicaciónfácilmente se pueda ampliar y mantener.
  • Las pruebas unitariasson fácil con simulación objetos.

Desventajas de Inyección de Dependencia

La inyección de dependencia tiene algunas desventajas también:

  • Sise abusa, puede conducir aproblemas de mantenimientodebido al efecto de los cambios en tiempo de ejecución
  • La inyección de dependenciaoculta lasdependenciasde la clasede serviciosque pueden conducir aerrores de ejecuciónquehan sido capturadosen tiempo de compilación.

El diagrama de clases es el siguiente:

IoD

Código fuente en GitHub

https://github.com/vemarquez/InyeccionDependenciasJava

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s