Inyección de dependencias

  • Por
  • y  
Qué es la Inyección de dependencias. Que es el contenedor de dependencias, elementos de un patrón de diseño de software usado en la mayoría de los grandes frameworks.

En este artículo vamos a fundir dos textos de DesarrolloWeb.com dedicados a la inyección de dependencias. Ambos te explican el patrón de diseño de software orientada a objetos, sobre dos enfoques diferentes.

1.- La primera parte del artículo es más nueva, escrita en 2015 por Miguel Angel Alvarez. Relata en rasgos generales el patrón de inyección de dependencias y contenedor de dependencias. Aunque está inspirada en el uso que se le da al patrón en la comunidad de desarrolladores de PHP y en numerosos frameworks de desarrollo, pretende abarcar el conocimiento básico en cualquier lenguaje de programación orientado a objetos.

2.- La segunda parte es un artículo más antiguo, publicado en 2011, pero que sigue de actualidad porque el patrón en sí no ha sufrido cambios conceptuales. La segunda parte la escribe José Miguel Torres y relata la Inyección de dependencias enfocada en la programación en .NET.

Introducción

La inyección de dependencias es un patrón de diseño de software usado en la Programación Orientada a Objetos, que trata de solucionar las necesidades de creación de los objetos de una manera práctica, útil, escalable y con una alta versatilidad del código.

En la mayoría de los frameworks actuales se aplica la Inyección de dependencias como parte de las herramientas y modelos que facilitan al programador. Como cualquier patrón de diseño de software trata de solucionar de una manera elegante un problema habitual en el desarrollo de software, por lo que también es idóneo utilizar este patrón en el desarrollo de proyectos a pequeña escala.

Qué es la inyección de dependencias

Aparte de un patrón de diseño de software, vamos a explicar qué idea hay detrás de ese nombre. Este patrón, como muchos otros, nos ayuda a separar nuestro código por responsabilidades, siendo que en esta ocasión sólo se dedica a organizar el código que tiene que ver con la creación de los objetos.

Como ya sabemos, uno de los principios básicos de la programación, y de las buenas prácticas, es la separación del código por responsabilidades. Pues la inyección de dependencias parte de ahí.

En el código de una aplicación con OOP (Programación Orientada a Objetos) tenemos una posible separación del código en dos partes, una en la que creamos los objetos y otra en la que los usamos. Existen patrones como las factorías que tratan esa parte, pero la inyección de dependencias va un poco más allá. Lo que dice es que los objetos nunca deben construir aquellos otros objetos que necesitan para funcionar. Esa parte de creación de los objetos se debe hacer en otro lugar diferente a la inicialización de un objeto.

Por ejemplo, este código no sería el mejor:

Nota: Voy a escribir con pseudocódigo porque la inyección de dependencias realmente sirve para cualquier lenguaje y verdaderamente no importa aquí el código sino entender el concepto.

class programador{
	ordenador
	lenguaje

	constructor(){
		this.ordenador = new Mac()
		this.lenguaje = new ObjectiveC()
	}
}

miguel = new Programador()

El problema que nos encontramos es que la clase Programador está fuertemente acoplada con la el ordenador Mac o el lenguaje ObjectiveC. Si mañana queremos tener programadores de C que usan Windows, tal como está el código, tendríamos que crear una nueva clase Programador, porque esta no nos valdría.

Ahora veamos esta alternativa de código mucho más versátil.

class programador{
	ordenador
	lenguaje

	constructor(ordenador, lenguaje){
		this.ordenador = ordenador
		this.lenguaje = lenguaje
	}
}

miguel = new Programador( new Mac(), new ObjectiveC() )
carlos = new Programador( new Windows(), new Java() )

Ahora nuestro programador es capaz de adaptarse a cualquier tipo de ordenador y cualquier tipo de lenguaje. De hecho observarás que hemos podido crear un segundo programador llamado "carlos" que es capaz de programar en Java bajo Windows.

Claro que esto es solo un ejemplo ridículo pero si has podido apreciar la diferencia, podrás entender el resto que viene detrás del concepto de inyección de dependencias. En realidad es tan sencillo como apreciar que al constructor de los objetos se les están pasando aquellas dependencias que ellos tienen para poder realizar sus tareas.

El hecho en sí, de enviar por parámetros los objetos que son necesarios para que otro objeto funcione, es la inyección de dependencias.

Contenedor de dependencias

El código que has visto anteriormente es muy sencillo, pero en estructuras más complejas observarás que hay muchos objetos que dependen de otros objetos. Esos otros objetos a su vez dependen de otros, que dependen de otros. Como has observado, dado el patrón de inyección de dependencias, necesito tener listos todos los objetos de los que depende el que voy a construir, porque se los tengo que enviar por parámetro al constructor.

Por ejemplo, el programador depende del ordenador y los lenguajes, pero el ordenador depende del sistema operativo y el teclado y ratón. A su vez el teclado depende de una conexión USB y de un conjunto de teclas, la conexión USB depende de las líneas de comunicación de datos y de la electricidad, la electricidad depende de que hayas pagado tu factura el mes anterior y de que así haya tensión en la red. Así podríamos continuar hasta donde nos lleve la imaginación.

Todo eso nos indica que, para conseguir instanciar un programador necesito haber instanciado antes la red eléctrica y las líneas de comunicación del USB, la conexión USB, cada una de las teclas del teclado, el teclado, el ratón, el sistema operativo, el ordenador, los lenguajes… y cuando tengo todo eso, por fin puedo invocar al constructor de la clase Programador para obtener el objeto que quería.

¿Complicado? No. Pero sí es laborioso. Quizás tengas una docena de líneas de código, o más, para poder hacer lo que tú querías, que era instanciar un programador al que necesitas inyectarle todas las dependencias, conforme nos dicta el patrón.

La solución a esta problemática nos la trae el contenedor de dependencias, también llamado inyector de dependencias, contenedor de servicios, etc.

Básicamente es como una caja a la que le pido construir las cosas. Esa caja sabe qué tiene que hacer para construir cada uno de los objetos de la aplicación. Si queremos un programador, simplemente le pedimos al contenedor de dependencias que nos cree un objeto de esa clase y él se dedica a crearlo. Finalmente el "dependency container" lo devuelve listo para ser usado.

miguel = contenedorDependencias.crear("Programador");

El contenedor de dependencias nos permite que la instanciación de un objeto, por muchas dependencias que tenga, vuelva a ser tan simple como una llamada a un método. ¿Dónde está la magia? el contenedor de dependencias simplemente tiene todos los objetos que puedas necesitar para crear cualquier objeto complejo y si no cuenta en ese instante con las dependencias necesarias, sabe cómo conseguirlas en el acto.

El uso de este contenedor de dependencias ya depende del lenguaje que estés usando y la librería o framework con la que trabajes. Además habitualmente hay que hacer algunas configuraciones básicas para que funcione. Si te fijas, en la línea anterior estoy diciendo que me cree un programador, pero en algún lugar le tendré que decir cómo puedo obtener ese objeto deseado y si tal como está ese programador será experto en Java, ObjectiveC o PHP.

Nota: Si nuestro código está correctamente escrito es muy probable que las configuraciones sean mínimas, porque podrá decidirlo a través del análisis de los tipos de datos indicados en las cabeceras de los métodos constructores, pero generalmente hay que configurar algunas cosas básicas.

En futuros artículos experimentaremos con alguna librería para implementar de manera sencilla un contenedor con el que poner en marcha este patrón de inyección de dependencias.

Inyección de dependencias enfocada en el desarrollo en .NET

A continuación te ofrecemos la segunda parte de este artículo sobre la inyección de dependencias, en la que vamos a conocer más detalles sobre este patrón de diseño de software, particularmente enfocada en el mundo del .NET.

Introducción

Si nos remontamos a los primeros años de la programación, nos encontraremos con programas rígidos repletos de código monolítico y lineal. La propia evolución hizo aparecer conceptos hoy por hoy imprescindibles como la modularidad y la reutilización de componentes, conceptos fundamentales en el paradigma de la Programación Orientada a Objetos.

La modularidad y reutilización de clases conlleva un flujo de comunicación entre instancias cuyo mal uso deriva en un hándicap que limita la flexibilidad, robustez y reusabilidad del código debido a la dependencia o alto acoplamiento entre las clases.

En la figura 1 podemos ver un sencillo diagrama de clases de un sistema de adquisición y control de datos meteorológicos. Existen dos clases participantes: una para la captura de la temperatura, y otra que representa a la estación meteorológica. Ambas tienen una responsabilidad a la hora de mostrar los datos, como puede apreciarse en el listado 1.


public class EstacioMeteorologica
{
public void MostrarDatos()
{
Console.WriteLine(
string.Format("Datos a {0} n", DateTime.Now));
Termometro termometro = new Termometro();
termometro.MostrarTemperaturaActual();
}
}
public class Termometro
{
public int Valor { get; set; }
public void MostrarTemperaturaActual ()
{
Console.WriteLine(
string.Format("Temperatura: {0} º", Valor));
}
}

Identificando el problema

Cuando hablamos en términos de calidad, solemos utilizar los adjetivos "bueno" o "malo" para definir la calidad de un diseño. Sin embargo, no siempre utilizamos los argumentos o criterios que sustentan la afirmación "éste es un mal diseño". Existe un conjunto de criterios más allá del siempre subjetivo TNTWIWHDI (That’s Not The Way I Would Have Done It, "Yo no lo habría hecho así") acuñado por Robert C. Martin, y son los que miden el nivel de rigidez, la fragilidad y la inmovilidad del sistema.

En nuestro ejemplo de la estación meteorológica, podemos afirmar que el diseño es rígido, porque cualquier cambio será difícil de llevar a cabo, ya que no conocemos el impacto que la modificación de una clase de bajo nivel (clase Termometro) tendrá sobre la clase de alto nivel (clase EstacioMeteorologica).

Cuando los cambios tienen una repercusión en otras entidades, no necesariamente dependientes, se dice que un sistema o aplicación es frágil. Si nos fijamos en el listado 1, la clase EstacioMeteorologica depende tanto de Termometro como de System.Console. Un cambio del flujo de salida de datos del programa (por ejemplo, a una impresora en lugar de System.Console) repercutiría en las clases de bajo nivel.

El termino inmovil lo utilizamos para medir el nivel de dependencia entre una parte del diseno y otros datos no directos. El ejemplo es inmovil porque la clase Estacio] Meteorologica depende de las clases Termometro y System. Console para mostrar los datos. Dicho en otras palabras, no podriamos extraer la clase de mayor nivel y utilizarla con otras entidades. Lo mismo pasaria con la clase de bajo nivel por su dependencia de System.Console.

Nota: Entre los criterios que permiten determinar si un diseño es bueno o malo están los que miden su nivel de rigidez, fragilidad e inmovilidad.

Planteemos un nuevo diseño a nuestro sistema. En primer lugar, eliminemos la dependencia que la clase Termometro tiene de System.Console, ya le que estamos otorgando la responsabilidad de salida por pantalla cuando realmente no le corresponde. El resultado sería el que se muestra en el listado 2.


public class EstacioMeteorologica
{
public void MostrarDatos()
{
Termometro termometro = new Termometro();
string temperatura =
termometro.MostrarTemperaturaActual();
Console.WriteLine(
string.Format("Datos a {0} n{1}",
DateTime.Now, temperatura));
}
}
public class Termometro
{
public int Valor { get; set; }
public string MostrarTemperaturaActual ()
{
return string.Format("Temperatura:{0} º", Valor);
}
}

Ahora la clase Termometro ha quedado libre de dependencias, y por tanto es reutilizable. Sin embargo, aún EstacioMeteorologica depende tanto de System.Console como de Termometro. Por otro lado, la clase Termometro no es más que una representación de un valor referencial meteorológico cualquiera; por tanto, podríamos abstraer la interfaz IMeteoReferencia, tal y como se muestra en el listado 3, y hacer que la clase Termometro la implemente. Esto es un ejemplo de aplicación del patrón Fachada (Façade), mediante el cual simplificamos la firma de varias clases a través de una única interfaz.


public interface IMeteoReferencia
{
int Valor { get; set; }
string Mostrar();
}
public class Termometro : IMeteoReferencia
{
public int Valor { get; set; }
public string Mostrar()
{
return string.Format("Temperatura:{0} º", Valor);
}
}

Ahora que hemos abstraído la interfaz, ésta nos servirá como contrato para las clases que quieran utilizarla. Esto nos permitirá desacoplar la clase EstacioMeteorologica de Termometro, tal y como muestra el listado 4.


public class EstacioMeteorologica
{
private IMeteoReferencia termometro;
public EstacioMeteorologica()
{
termometro = new Termometro();
}
public void MostrarDatos()
{
Console.WriteLine(
string.Format("Datos a {0}", DateTime.Now));
Console.WriteLine(termometro.Mostrar());
}
}

Sin embargo, aún no hemos solucionado el problema, pese a que estamos más cerca. Lo que pretendemos es eliminar completamente la instanciación de la clase Termometro, y la solución pasa por inyectar la dependencia directamente a través del constructor, como se muestra en el listado 5.


public class EstacioMeteorologica
{
private IMeteoReferencia termometro;
public EstacioMeteorologica(
IMeteoReferencia termometro)
{
this.termometro = termometro;
}
public void MostrarDatos()
{
Console.WriteLine(
string.Format("Datos a {0}", DateTime.Now));
Console.WriteLine(termometro.Mostrar());
}
}

El Principio de Inyección de Dependencias

Robert C. Martin afirma en el Principio de Inyección de Dependencias:

    A. Las clases de alto nivel no deberían depender de las clases de bajo nivel. Ambas deberían depender de las abstracciones.
    B. Las abstracciones no deberían depender de los detalles. Los detalles deberían depender de las abstracciones.

Imaginemos por un momento la solución inicial de la estación meteorológica (listado 1). La clase de alto nivel EstacioMeteorologica depende de la clase de bajo nivel Termometro (o Barometro, Anemometro, etc.). Toda la lógica de la solución se implementaría en la clase de alto nivel, y cualquier modificación en las clases de bajo nivel tendría repercusión no únicamente sobre la definición de la clase de alto nivel, sino sobre la propia lógica de la aplicación, llegando incluso a forzar cambios en la misma, cuando debería ser la clase de alto nivel la que debería forzar el cambio a las clases de bajo nivel sin comprometer la lógica de la aplicación; es decir, justamente lo contrario. Además, la clase de alto nivel sería difícilmente reusable debido a este acoplamiento. Sencillamente, y resumiendo, la clase EstacioMeteorologica no debe depender de la clase Termometro; en todo caso, al contrario.

Existen tres formas de implementación de la Inyección de Dependencias:

  • por constructor
  • por setter
  • por interfaz.
El primer caso lo hemos visto en la sección anterior, donde hemos inyectado la dependencia a través del constructor de la clase; el listado 6 muestra una generalización. La inyección por setter se realiza a través de una propiedad de la clase (listado 7); y por último, la inyección por interfaz se realiza a través de un método, recibiendo como parámetro el objeto a inyectar (listado 8).


IMeteoReferencia referencia = ObtenerReferencia();
EstacioMeteorologica estacion =
new EstacioMeteorologica(referencia);

EstacioMeteorologica estacion = new EstacioMeteorologica();
estacioMeteorologica.Referencia = ObtenerReferencia();

EstacioMeteorologica estacion = new EstacioMeteorologica();
estacioMeteorologica.LecturaContador(ObtenerReferencia());

Inversión de control y contenedores

No podemos hablar de DI sin dejar de hablar de la Inversión de control (Inversion of Control, IoC). IoC también es conocido como Principio de Hollywood, nombre derivado de las típicas respuestas de los productores de cine a los actores noveles: "no nos llames; nosotros lo haremos".

IoC invierte el flujo de control de un sistema en comparación con la programación estructurada y modular. En el fondo, DI es una implementación de IoC. Aún hoy existe la discusión acerca de si IoC es un principio, un patrón o ambas cosas a la vez. IoC, en definitiva, es una característica fundamental de un framework, y de hecho lo que lo hace realmente diferente a una librería de funciones.

En escenarios de producción, las clases no son tan triviales como la que hemos presentado en este artículo. Imagine por un momento que la interfaz IMeteoReferencia tiene una implementación de IEntradaDatos e IVerificador, y éstas a su vez implementan otras interfaces. En realidad, obtendremos una jerarquía de dependencias (figura 3), cuyo manejo en tiempo de diseño es imposible de gestionar "manualmente"; es aquí donde entra a jugar el término contenedor IoC (IoC Container).

El principal cometido de un contenedor IoC, a diferencia de una factoría, es el de gestionar el ciclo de vida de los objetos. El contenedor IoC registra una implementación específica para cada tipo de interfaz y retorna una instancia de objeto. Esta resolución de objetos tiene lugar en un único punto de las aplicaciones; normalmente, a nivel de infraestructura.

Conclusión

Con este artículo, hemos tratado de mostrar de una forma práctica la relación existente entre dependencias, detalles y abstracciones. Con el Principio de Inyección de Dependencias, ponemos fin a la última de las siglas que componen SOLID. Existen libros íntegros que hablan de este principio, y podrá encontrar en Internet una gran cantidad de recursos relacionados.

A lo largo de esta serie sobre los principios SOLID, hemos presentado aspectos muy importantes que debemos tener en cuenta ante cualquier nuevo desarrollo, y hemos visto cómo muchas de las problemáticas lógicas del diseño pueden ser reducidas mediante la aplicación de estos principios. Trate de entender cada uno de los principios desde un punto de vista práctico. Algunos de ellos (y lo digo por experiencia) son realmente complejos de llevar a la práctica; recuerde además que son principios, no reglas.

Para finalizar, agradecer a Hadi Hariri, quien me ha servido de "enciclopedia de consulta" para esta serie, por su apoyo y ayuda en todo momento.