¿Qué es IoC - Inversion of Control - Inversión de Control? ¿Y la Inyección de Dependencias - Dependency Injection - DI?

Existen por Internet definiciones muy aburridas y crípticas de lo que es la Inversión de Control, también conocida por la expresión inglesa Inversion of Control, o sus siglas IoC (que usaré a partir de ahora para abreviar).

Antes de meternos en faena, tengo que hacer un inciso: se tiende a confundir los términos Inversión de Control e Inyección de Dependencias (Dependency Injection, o DI). Aunque los términos están relacionados, en realidad no son lo mismo ni son intercambiables. Como veremos más adelante, la Inyección de Dependencias es un efecto derivado de la Inversión de Control.

IoC no es más que una metodología de programación que pretende evitar la dependencia directa entre clases mediante el uso de interfaces. De esta forma, en vez de hacer una llamada directa a una clase concreta que me dé una funcionalidad, realizo una llamada a "algo" que me dé esa funcionalidad.

Me explicaré mejor.

Supongamos que tengo una aplicación de gestión de una biblioteca, que contiene un módulo de comunicaciones para el envío de mensajes a los usuarios:

public class Comunicaciones
{
   public Boolean EnviaMensaje(Usuario usuariodestino)
   {
      // lógica para enviar email al usuario afectado
   }
}

public class Biblioteca
{
   public void ComprobarUsuario(Usuario usuarioactual)
   {
      if (se_ha_pasado_de_fecha)
         var resultado = new Comunicaciones().EnviaMensaje(usuarioactual);
   }
}

Como se puede observar, el código funcionará, cumplirá su misión, pero hay un problema: EnviaMensaje envía un email al usuario. Pero... ¿qué ocurre si el día de mañana, en vez de enviar un email, la biblioteca decide enviar un SMS? O si decide añadir más complejidad, y además de enviar el mensaje, crear un log que recoja los intentos de aviso al usuario. Nos obligaría a cambiar parte del código, ya sea del procedimiento de envío de mensajes (deshaciéndonos del código anterior) o del procedimiento ComprobarUsuario, para que utilice la nueva clase, adaptándonos a las nuevas necesidades.

Para esto se creó la Inversión de Control. En este caso, lo que haremos será que nuestro código no dependa directamente de una clase, sino de una definición genérica de una clase que implemente la función EnviaMensaje:

public interface IComunicaciones
{
	Boolean EnviaMensaje(Usuario usuariodestino);
}

public class enviaEmail:IComunicaciones
{
	public Boolean EnviaMensaje(Usuario usuariodestino)
	{
		// lógica para enviar email al usuario afectado
	}
}

public class Biblioteca
{
	private IComunicaciones moduloComunicaciones;

	public Biblioteca(IComunicaciones comunicaciones)
	{
		moduloComunicaciones = comunicaciones;
	}

	public void ComprobarUsuario(Usuario usuarioactual)
	{
		if (true)
		{
	       var resultado = moduloComunicaciones.EnviaMensaje(usuarioactual);
		}
	}
}

¿Qué está ocurriendo aquí? en este caso, ComprobarUsuario está llamando a "alguna clase que sea capaz de implementar la función EnviaMensaje", puesto que moduloComunicaciones pertenece al tipo interfaz IComunicaciones, que obliga a implementar dicha función.

Cuando se instancia la clase Biblioteca, se le debe indicar qué clase dependiente de IComunicaciones debe utilizar (observar que la clase enviaEmail hereda directamente de IComunicaciones). Para ello, cuando arranque la aplicación, tenemos que hacer una asociación que indique "cuando se necesite un componente de tipo IComunicaciones, entonces utilizas la clase enviaEmail". A este proceso, se le llama "Inyección de Dependencias", y para ello se utiliza lo que se llama un Contenedor de Dependencias, que no es más que un almacén que se encarga de comprobar en qué momento se solicita un componente de un tipo concreto, para indicar qué componente de dicho tipo tiene que utilizar. Al ejecutarse el constructor Biblioteca, recibe un objeto instanciado de la clase enviaEmail, que será el que use como módulo de comunicaciones.

Ahora, ¿qué ocurre si, como antes dijimos, en vez de usar emails, queremos enviar sms a los usuarios? Pues es tan fácil como crear una nueva clase que herede de IComunicaciones, pero cuyo método EnviaMensaje envíe SMS:

public class enviaSMS:IComunicaciones
{
	public Boolean EnviaMensaje(Usuario usuariodestino)
	{
		// lógica para enviar sms al usuario afectado
	}
}

Posteriormente, en el arranque del programa, le decimos "cuando se necesite un componente de tipo IComunicaciones, entonces utilizas la clase enviaSMS". El constructor Biblioteca recibirá un objeto de la clase enviaSMS, que utilizará como módulo de comunicaciones. Así de fácil. No es necesario cambiar la implementación de la clase enviaEmail, ni la implementación de la clase Biblioteca.

¿Qué ventajas obtenemos con la Inversión de Control?

  • 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. Sólo sabemos que necesitamos algo que nos envíe un mensaje y que nos devuelva un boolean que diga si se ha enviado o no. Podemos usar un módulo que envíe emails, que envíe sms, o que envíe un mensaje por twitter. Eso no cambiará la codificación de la clase Biblioteca.
  • Se favorece la reutilización de código, ya que permite la creación de módulos independientes del resto de la aplicación. En nuestro caso, podemos utilizar los módulos de envío en otras aplicaciones que usen la misma interfaz IComunicaciones, sin falta de cambiar código en ninguno de los dos sitios.
  • 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. En este caso, al crear las pruebas unitarias para ComprobarUsuario, hacemos una llamada a "algo" que "manda un mensaje", y nos da igual cómo lo haga, lo que necesitamos es que lo envíe y nos devuelva un valor booleano que diga si se ha enviado o no.