[DIP] Principio de Inversión de Dependencias

Descripción general

Principios del diseño orientado a objetos; Principios SOLID; [DIP] Principio de inversión de dependencias
Básicamente lo que nos dice este principio es que
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.

[TOC] Tabla de Contenidos


↑↑↑

[DIP] Principio de Inversión de Dependencias


↑↑↑

Los cinco principios SOLID


↑↑↑

[DIP] Principio de inversión de dependencias

Básicamente lo que nos dice este principio es que

En Informática, 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 quien cree el objeto. Es una herramienta comúnmente utilizada en varios patrones de diseño orientado a objetos, consiste en inyectar comportamientos a componentes. El término fue acuñado por primera vez por Martin Fowler.

Esto no es más que extraer responsabilidades a un componente para delegarlas en otro, estableciendo un mecanismo a través del cual el nuevo componente pueda ser cambiado en tiempo de ejecución. Es conveniente no confundir Inyección de dependencias (DIP) con Inversión de Control (IoC).


↑↑↑

Un ejemplo sencillito

Imaginemos que estamos haciendo un videojuego en cual tenemos un personaje que es un robot, este robot puede realizar acciones de diferentes maneras, por lo cual su cabeza puede ser conectada a infinidad de cuerpos distintos.

Figura 1

Figura 1

Las soluciones tradicionales desde el punto de vista de POO son diversas, ejemplo:

Esta sería una implementación de Robot que NO utiliza Inyección de Dependencias

Listado 1

class RobotInicial { public RobotInicial() { } public void Caminar() { //Caminando Ando... /*Paso 1 *Paso 2 *Paso N*/ } /*Hacer otras cosas chulas que hacen los robots*/ public void Disparar() { } public void Volar() { } public void BuscarAJhonConnor() { /*Paso 1 buscar en internet *Paso 2 correr *Paso 3 caminar para ahorrar bateria *Paso N*/ } }

Sin embargo, al final, la responsabilidad del cuerpo y de la cabeza sigue siendo confusa, además, hay que intentar respetar el principio SRP "Single Responsability" que señala: "Un objeto una responsabilidad"

Claramente las soluciones clásicas al problema planteado no ofrecen esa opción.

¿Qué hacer? Inyección de dependencias también puede llamarse inyección de comportamientos, en todo caso es un nombre muy profesional y estilizado, lo cual puede ser intimidante, pero es un nombre preciso. ¿Qué sucede cuando te inyectan algo, una vacuna por ejemplo? Depositan nuevos componentes en tu cuerpo, que pueden modificar el funcionamiento, sin que esto implique que vuelvas a nacer.

De eso mismo se trata la Inyección de Dependencias, colocar dentro de un objeto otros que puedan cambiar su comportamiento, sin que esto implique volver a crear el objeto.

La aplicación de esta idea nos permite diseñar un objeto que puede hacer un conjunto de tareas, (cada una de esas tareas es una responsabilidad), que pueden ser ejecutadas por otro objeto especialista y dedicado a ello [una responsabilidad], pero al ser un objeto… ¡¡puede establecerse su referencia en tiempo de ejecución!!

La implementación habitual de Inyección de dependencias es crear un método para establecer el comportamiento, es decir capaz de cambiar el valor de un atributo asignándole una instancia de objeto diferente.

Ahora vamos a cambiar la implementación del robot por una que SI utilice la Inyección de dependencias. En primer lugar creamos la clase [CuerpoRobot] que será la responsable de ejecutar las acciones del cuerpo:

Listado 2

public class CuerpoRobot { public void Caminar() { //Caminando ando... } public void Volar() { //Volando ando... } public void Disparar() { //Volando ando... } }

Seguidamente creamos un [Robot] que camina por medio de un [CuerpoRobot], delegando las responsabilidades a dicho objeto, además debemos establecer un mecanismo para asignar un [CuerpoRobot] a nuestro [Robot]

Listado 3

public class Robot { //Inyectar un cuerpo al Robot public CuerpoRobot Cuerpo { get; set; } public Robot() { } public void Caminar() { if (Cuerpo != null) Cuerpo.Caminar(); } public void Volar() { if (Cuerpo != null) Cuerpo.Volar(); } public void Disparar() { if (Cuerpo != null) Cuerpo.Disparar(); } /*Hacer otras cosas chulas que hacen los robots*/ public void BuscarAJhonConnor() { /*Paso 1 buscar en internet *Paso 2 Cuerpo.Caminar(); *Paso 3 Cuerpo.Disparar(); *Paso N*/ } }

Como se puede observar la clase [Robot] dispone de la acción caminar, pero la responsabilidad es de la clase [CuerpoRobot], también está claro que el valor de la propiedad [Cuerpo] se puede cambiar en tiempo de ejecución tan solo asignando una instancia diferente de la clase [CuerpoRobot ] a la propiedad [Cuerpo].

Esta es una primera aproximación a la inyección de dependencias, pero si nos ponemos mas "formales", muchos comportamientos como Volar y Caminar podrían llegar a estar en clases distintas.


↑↑↑

Ejemplo un poco más complejo

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

En este ejemplo, vamos a ver, de una forma práctica, la relación existente entre dependencias, detalles y abstracciones

Figura 2

Figura 2

En la figura siguiente 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 4.

Listado 4

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

↑↑↑

¿Qué problema tiene este código?

Podemos afirmar que nuestra estación meteorológica tiene un diseño rígido, porque cualquier cambio será difícil de llevarlo a cabo. El problema es que no conocemos el impacto que la modificación de una clase de bajo nivel (clase [Termómetro]) tendrá sobre la clase de alto nivel (clase EstacionMeteorologica).

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 4, la clase [EstacionMeteorologica] depende tanto de [Termómetro] 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 inmóvil lo utilizamos para medir el nivel de dependencia entre una parte del diseño y otros datos no directos. El ejemplo es inmóvil porque la clase [EstacionMeteorologica] depende de las clases [Termómetro] y [System.Console] para mostrar los datos. Dicho en otras palabras, no podríamos extraer la clase de mayor nivel y utilizarla con otras entidades. Lo mismo pasaría con la clase de bajo nivel por su dependencia de System.Console.

Planteemos un nuevo diseño a nuestro sistema. En primer lugar, eliminemos la dependencia que la clase [Termómetro] 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 5.

Listado 5

public class EstacionMeteorologica { 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 [Termómetro] ha quedado libre de dependencias, y por tanto es reutilizable. Sin embargo, [EstacionMeteorologica] aún depende tanto de [System.Console] como de [Termómetro]. Por otro lado, la clase [Termómetro] 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 6, y hacer que la clase [Termómetro] la implemente. Esto es un ejemplo de aplicación del patrón Fachada (Facade), mediante el cual simplificamos la firma de varias clases a través de una única interfaz.

Listado 6

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 [EstacionMeteorologica] de [Termómetro], tal y como muestra el listado 7.

Listado 7

public class EstacionMeteorologica { private IMeteoReferencia termometro; public EstacionMeteorologica() { 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 [Termómetro], y la solución pasa por inyectar la dependencia directamente a través del constructor, como se muestra en el listado 8.

Listado 8

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

Figura 3

A modo de comentario

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

Imaginemos por un momento la solución inicial de la estación meteorológica (listado 4). La clase de alto nivel [EstacionMeteorologica] depende de la clase de bajo nivel [Termómetro] (o Barómetro, Anemómetro, 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 [EstacionMeteorologica] no debe depender de la clase [Termómetro]; en todo caso, al contrario.


↑↑↑

Formas de implementar la Inyección de Dependencias

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

Por constructor

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 9 muestra una generalización.

Listado 9

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

Cualquier clase que implemente la interfaz [IMeteoReferencia] se acepta como parámetro en el constructor de la clase [EstacionMeteorologica]

Por setter

La inyección por setter se realiza a través de una propiedad de la clase (listado 10) en el código siguiente se supone que la clase [EstacionMeteorologica] dispone de una propiedad llamada [Referencia] que carga en la variable miembro de la clase la referencia al objeto (que se inyecta) que implementa la [IMeteoReferencia]

Listado 10

//IMeteoReferencia referencia = ObtenerReferencia(); EstacionMeteorologica estacion = new EstacionMeteorologica(); estacion.Referencia = ObtenerReferencia();

Por interfaz.

Por último, la inyección por interfaz se realiza a través de un método, que acepta un parámetro de tipo interface [IMeteoReferencia] (listado 11).

Listado 11

EstacionMeteorologica estacion = new EstacionMeteorologica(); estacion.LecturaContador(ObtenerReferencia());

Las tres implementaciones difieren sutilmente entre sí. La diferencia más importante es que, Por Constructor, nos obliga a inyectar al crear la clase, mientras que las otras modalidades, la difieren para más adelante.

La diferencia entre estas dos últimas está en si se prefiere inyectar la dependencia en el set de una propiedad, Por Setter, o como argumento de un parámetro de entrada a un método o función, Por Interface. Cuestión de gusto sintáctico.


↑↑↑

Inversión de control y contenedores

No podemos hablar de DIP 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".

Inversión de control (Inversion of Control en inglés, IoC) es un método de programación en el que el flujo de ejecución de un programa se invierte respecto a los métodos de programación tradicionales, en los que la interacción se expresa de forma imperativa haciendo llamadas a procedimientos (procedure calls) o funciones. Tradicionalmente el programador especifica la secuencia de decisiones y procedimientos que pueden darse durante el ciclo de vida de un programa mediante llamadas a funciones. En su lugar, en la inversión de control se especifican respuestas deseadas a sucesos o solicitudes de datos concretas, dejando que algún tipo de entidad o arquitectura externa lleve a cabo las acciones de control que se requieran en el orden necesario y para el conjunto de sucesos que tengan que ocurrir.

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 las que hemos visto. 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.

Figura 4

Figura 4


↑↑↑

Referencia Bibliográfica


↑↑↑

A.2.Enlaces

[Para saber mas]
[Grupo de documentos]
[Documento Index]
[Apuntes sobre principios de diseño]
[Documento Start]
[Imprimir el Documento]