¿Qué es la Inyección de Dependencias?

  • Time to read 4 minutes
Inyeccion de dependencias

Existen por Internet decenas de definiciones aburridas que hablan sobre la Inyección de Dependencias, también conocida como Dependency Injection para los angloparlantes, o DI en su versión abreviada. En este artículo intentaré explicar de qué va, y la relación que tiene con el concepto de Inversión de Control, Inversion of Control en lengua de Shakespeare, o IOC para quienes les gusta hablar con abreviaturas. Ambos conceptos suelen confundirse habitualmente y, aunque están relacionados porque uno es consecuencia del otro, no son exactamente lo mismo.

Vamos a analizar primeramente cómo se instancia un objeto en un lenguaje como Java. Imaginemos que tenemos un proceso de comunicación que utiliza un algoritmo de cifrado en algún punto de su trabajo:

public class ComApplication {

  public static void main(String[] args) {		
    CommunicationService service = new CommunicationService();
    service.CypheredSend("Esto es una prueba");		
  }

}
public class CommunicationService {
	
  public void CypheredSend(String source) {		
    DesCypher cypher = new DesCypher();
    String cyphered = cypher.Cypher(source);
    // Proceso de envío de información		
  }

}
public class DesCypher {
	
  public String Cypher(String source) {		
    String response = null;		
    // Mi proceso de cifrado Des	
    return response;		
  }

}

Nuestra aplicación ComApplication utiliza un servicio de comunicaciones llamado CommunicationService, que tiene un procedimiento llamado CypheredSend. Este servicio instancia un objeto que contiene nuestro excelente algoritmo de cifrado DES, y que se encarga de cifrar la cadena que le pasemos como parámetro a su método Cypher.

Pero resulta que pasa el tiempo y aparece el algoritmo AES, que parece mucho más seguro, y queremos utilizarlo en nuestra aplicación. ¿Qué ocurre? Tenemos que crear nuestra nueva suite de cifrado, y tendremos que modificar la implementación de CommunicationService para que la utilice:

public class AesCypher {
	
  public String Cypher(String source) {		
    String response = null;		
    // Mi proceso de cifrado Aes	
    return response;		
  }

}
public class CommunicationService {
	
  public void CypheredSend(String source) {		
    AesCypher cypher = new AesCypher();
    String cyphered = cypher.Cypher(source);
    // Proceso de envío de información		
  }

}

¿Ves el problema? Nuestra suite de cifrado es una dependencia de nuestro servicio de comunicaciones. Cambiar una dependencia de una clase obliga a cambiar la implementación de la misma. En estos casos, se dice que ambos están fuertemente acoplados. Este es un pequeño ejemplo con 2 clases. ¿Pero qué ocurre en aplicaciones con 1000 clases? Un cambio de este tipo puede suponer horas y horas de trabajo. ¿La solución? La inyección de dependencias.

Vamos a ver que dice Wikipedia:

(...) inyección de dependencias (en inglés Dependency Injection, DI) es un patrón de diseño orientado a objetos, en el que se suministran objetos a una clase en lugar de ser la propia clase la que cree dichos objetos. Esos objetos cumplen contratos que necesitan nuestras clases para poder funcionar (de ahí el concepto de dependencia). Nuestras clases no crean los objetos que necesitan, sino que se los suministra otra clase 'contenedora' que inyectará la implementación deseada a nuestro contrato.

En resumen, cuando se usa el patrón Dependency Injection, le decimos al objeto qué implementación concreta de sus dependencias tiene que usar. Normalmente esto se suele hacer mediante su constructor. Transformemos nuestro ejemplo para adaptarse a este patrón. Lo primero que debemos hacer, es crear una interfaz que nos ayude a definir el contrato que deben de cumplir todas las implementaciones del objeto a inyectar:

public interface CypherSuite {

  String Cypher(String source);
  
}

Y hacemos que nuestras clases la implementen:

public class DesCypher implements CypherSuite {
	
  public String Cypher(String source) {		
    String response = null;		
    // Mi proceso de cifrado Des	
    return response;		
  }

}
public class AesCypher implements CypherSuite {
	
  public String Cypher(String source) {		
    String response = null;		
    // Mi proceso de cifrado Aes	
    return response;		
  }

}

Ahora modificamos nuestro servicio de comunicaciones. Lo que debemos pensar para ello es que no nos importa cómo se vaya a realizar el proceso de cifrado. Nosotros simplemente necesitamos "algo" que nos cifre los datos que vamos a enviar:

public class CommunicationService {
	
  private final CypherSuite suite;
  
  public CommunicationService(CypherSuite suite) {
    this.suite = suite;
  }
  
  public void CypheredSend(String source) {		
    String cyphered = this.suite.Cypher(source);
    // Proceso de envío de información		
  }

}

Ahora recibimos en el constructor de CommunicationService la instancia de la suite de cifrado que queremos utilizar. Vamos a modificar nuestro método main para utilizar primeramente el cifrado DES:

public class ComApplication {

  public static void main(String[] args) {
    CommunicationService service = new CommunicationService(new DesCypher());
    service.CypheredSend("Esto es una prueba");
  }

}

¿Qué pasaría si quisiésemos cambiar el algoritmo de cifrado por AES? Únicamente tenemos que cambiar una línea de código, sin necesidad de cambiar la implementación de ninguna clase:

CommunicationService service = new CommunicationService(new AesCypher());

Cuando arrancamos la aplicación, escogemos qué implementación queremos para las dependencias de las clases que participan en ella. De esta forma, se genera código débilmente acoplado, o código desacoplado. 

Esto tiene como consecuencia un efecto llamado Inversión de Control. La Inversión de Control es un principio de diseño de software en el que el flujo de ejecución se invierte con respecto al flujo tradicional. En el flujo tradicional, cada objeto es el encargado de instanciar sus propias dependencias. En nuestro ejemplo, antes de aplicar la Inyección de Dependencias, CommunicationService decía "vale, tengo que usar un algoritmo de cifrado mediante DES. Voy a instanciar DesCypher y utilizo su método de cifrado". Después de aplicar la Inyección de Dependencias, la cosa cambia. En ese caso, cuando inicializamos CommunicationService, le decimos "cuando durante el proceso de comunicación vayas a necesitar cifrado, vas a coger esta instancia de DesCypher, que te estoy pasando y que te permitirá realizar cifrados mediante el algoritmo que me interesa".

En aplicaciones grandes, se delega esta funcionalidad en los llamados Contenedores IOC o Contenedores de Dependencias. Este tipo de complementos, se encargan de gestionar el instanciado e inyeccion de objetos a medida que se van necesitando. Algunos ejemplos de este tipo de herramientas son Spring Framework o Castle Windsor.

¿Qué ventajas nos aporta esta metodología de desarrollo?

  • La primera y más importante, se favorece la modularización del código al evitar la dependencia directa entre unos componentes y otros, reduciendo la cantidad de código a modificar en caso de ampliar funcionalidades o cambiar comportamientos de clases. 
  • Se favorece la reutilización de código, ya que permite la creación de módulos independientes del resto de la aplicación. 
  • Se facilita el testeo y las pruebas unitarias del código, puesto que las pruebas no dependen de la implementación concreta de una clase, sino que dependen de cómo esa clase debe comportarse, independientemente de su implementación.

Imagen: Kelly Sikkema en UnSplash