Queremos comenzar esta sección «Raona Tech Talks«, hablando sobre la inyección de dependencias en el lenguaje de programación C# y mostrándoos un caso práctico con el fin que podáis solucionar este problema.
Problemática
Tenemos una clase llamada Androide. Este androide puede realizar acciones de diferentes maneras. Por ejemplo, un Androide puede avanzar andando o corriendo.
Veamos la clase Androide sin inyección de dependencias:
public class Androide
{
public Androide()
{
}
public void Avanzar() { }
public void Retroceder() { }
public void Disparar() { }
}
Androide puede realizar las acciones de Avanzar, Retroceder y Disparar. Viendo este trozo de código ya podemos intuir que si queremos cambiar el comportamiento de cualquier acción nos implica modificar directamente el código de Androide. Esto no es ideal, ya que hemos dicho que Androide debería poder realizar las acciones de diferentes maneras.
Para solucionarlo utilizamos la inyección de dependencias.
¿Qué entendemos por inyección de dependencias?
Cuando tenemos un objeto que necesita de otro para funcionar correctamente, tenemos definida una dependencia. Esta dependencia puede ser altamente acoplada o levemente acoplada. Si el acoplamiento es bajo el objeto independiente es fácilmente cambiable; si por el contrario es altamente acoplado, el reemplazo no es fácil y dificulta el diseño de los tests.
La inyección de dependencias es una metodología utilizada en los patrones de diseño que consiste en especificar comportamientos a componentes.
Se trata de extraer responsabilidades a un componente para delegarlas a otros componentes, de tal manera que cada componente solo tiene una responsabilidad (Principio de Responsabilidad Única). Estas responsabilidades pueden cambiarse en tiempo de ejecución sin ver alterado el resto de comportamientos.
Retomando el ejemplo anterior vamos a desarrollar una inyección de dependencias para solucionar el problema visto anteriormente.
Tenemos que abstraer la responsabilidad de los comportamientos de Androide a un componente que de verdad se encargue de estas acciones y tenga la responsabilidad de realizarlas.
Para ello vamos a crear la clase CuerpoAndador que es la que tendrá la responsabilidad de realizar las acciones de Androide.
La clase CuerpoAndador tiene los siguientes métodos:
public class CuerpoAndador
{
public CuerpoAndador() { }
public void Avanzar() { }
public void Retroceder() { }
public void Disparar() { }
}
Ahora vamos a cambiar la implementación de Androide para que utilice una inyección de dependencias:
public class Androide
{
public CuerpoAndador Cuerpo { get; set; }
public Androide()
{
}
public void Avanzar()
{
if (Cuerpo != null)
Cuerpo.Avanzar();
}
public void Retroceder()
{
if (Cuerpo != null)
Cuerpo.Retroceder();
}
public void Disparar()
{
if (Cuerpo != null)
Cuerpo.Disparar();
}
}
Vemos ahora que la responsabilidad de las acciones recaen en CuerpoAndador y que Androide utiliza las acciones del CuerpoAndador que tiene asignado en ese momento.
Aun así, podemos comprobar que seguimos dependiendo de la implementación CuerpoAndador en vez de una abstracción.
Vamos a crear la interface ICuerpo de la siguiente manera:
public interface ICuerpo
{
void Avanzar();
void Retroceder();
void Disparar();
}
Ahora implementamos la interface:
public class CuerpoAndador : ICuerpo
{
public CuerpoAndador() { }
public void Avanzar()
{
//Doy un paso hacia adelante
}
public void Retroceder()
{
//Doy un paso hacia atrás
}
public void Disparar()
{
//Disparo
}
}
Modificamos la clase Androide para inyectarle la abstracción (Interface) en vez de la concreción (Implementación).
public class Androide
{
public ICuerpo Cuerpo { get; set; }
public Androide()
{
}
public void Avanzar()
{
if (Cuerpo != null)
Cuerpo.Avanzar();
}
public void Retroceder()
{
if (Cuerpo != null)
Cuerpo.Retroceder();
}
public void Disparar()
{
if (Cuerpo != null)
Cuerpo.Disparar();
}
}
Como podemos ver, ahora le podemos asignar a Androide cualquier implementación de ICuerpo para que realice las acciones concretas de cada implementación.
Por ejemplo, podríamos tener otra implementación de ICuerpo e inyectársela a Androide sin tener que modificar la clase Androide.
public class CuerpoSaltador : ICuerpo
{
public CuerpoSaltador() { }
public void Avanzar()
{
//Doy un salto hacia adelante
}
public void Retroceder()
{
//Doy un salto hacia atrás
}
public void Disparar()
{
//Disparo
}
}
A continuación vemos el diagrama de clases de la estructura explicada anteriormente:
En el diagrama vemos que Androide tiene una dependencia con la interfaz ICuerpo. Esta interfaz tiene dos implementaciones: CuerpoAndador y CuerpoSaltador. Se le puede inyectar cualquiera de las dos implementaciones a Androide dado que implementan la interfaz ICuerpo.
Tipos de Inyección de Dependencias
Hemos visto cómo podemos asignar comportamientos a una clase. Existen tres modelos de inyectar dependencias a una clase:
• Por Setter
• Por Interface
• Por Constructor
Inyección de Dependencias por Setter
public class Androide
{
public ICuerpo Cuerpo { get; set; }
public Androide()
{
}
public void Avanzar()
{
if (Cuerpo != null)
Cuerpo.Avanzar();
}
public void Retroceder()
{
if (Cuerpo != null)
Cuerpo.Retroceder();
}
public void Disparar()
{
if (Cuerpo != null)
Cuerpo.Disparar();
}
}
En esta clase se realiza la Inyección de Dependencias a través de una propiedad definida en la clase Androide.
Para inyectar la dependencia de un componente por setter de Androide realizamos:
ICuerpo cuerpoAndador = new CuerpoAndador();
Androide androide = new Androide();
androide.Cuerpo = cuerpoAndador;
androide.Avanzar();
En este caso, la clase Androide avanzará andando ya que se le ha inyectado la implementación de CuerpoAndador.
ICuerpo cuerpoSaltador = new CuerpoSaltador();
Androide androide2 = new Androide();
androide2.Cuerpo = cuerpoSaltador;
androide2.Avanzar();
En este caso, la clase Androide avanzará saltando ya que se le ha inyectado la implementación de CuerpoSaltador.
Inyección de Dependencias por Interfaz
public class Androide
{
private ICuerpo _cuerpo;
public Androide()
{
}
public void SetCuerpo(ICuerpo cuerpo)
{
_cuerpo = cuerpo;
}
public void Avanzar()
{
if (_cuerpo != null)
_cuerpo.Avanzar();
}
public void Retroceder()
{
if (_cuerpo != null)
_cuerpo.Retroceder();
}
public void Disparar()
{
if (_cuerpo != null)
_cuerpo.Disparar();
}
}
En esta clase la Inyección de Dependencias está implementada sobre un método que acepta un parámetro de tipo interfaz ICuerpo.
Para inyectar la dependencia de un componente por Interfaz de Androide realizamos:
ICuerpo cuerpoAndador = new CuerpoAndador();
Androide androide = new Androide();
androide.SetCuerpo(cuerpoAndador);
androide.Avanzar();
En este caso, la clase Androide avanzará andando ya que se le ha inyectado la implementación de CuerpoAndador.
ICuerpo cuerpoSaltador = new CuerpoSaltador();
Androide androide2 = new Androide();
androide2.SetCuerpo(cuerpoSaltador);
androide2.Avanzar();
En este caso, la clase Androide avanzará saltando ya que se le ha inyectado la implementación de CuerpoSaltador.
Inyección de Dependencias por Constructor
public class Androide
{
private ICuerpo _cuerpo;
public Androide(ICuerpo cuerpo)
{
_cuerpo = cuerpo;
}
public void Avanzar()
{
if (_cuerpo != null)
_cuerpo.Avanzar();
}
public void Retroceder()
{
if (_cuerpo != null)
_cuerpo.Retroceder();
}
public void Disparar()
{
if (_cuerpo != null)
_cuerpo.Disparar();
}
}
En esta clase se implementa la Inyección de Dependencias por medio del constructor. El constructor de Androide acepta un parámetro de tipo ICuerpo. El constructor acepta cualquier clase que implemente la interfaz.
Para inyectar la dependencia de un componente por constructor de Androide realizamos:
ICuerpo cuerpoAndador = new CuerpoAndador();
Androide androide = new Androide(cuerpoAndador);
androide.Avanzar();
En este caso, la clase Androide avanzará andando ya que se le ha inyectado la implementación de CuerpoAndador.
ICuerpo cuerpoSaltador = new CuerpoSaltador();
Androide androide2 = new Androide(cuerpoSaltador);
androide2.Avanzar();
En este caso, la clase Androide avanzará saltando ya que se le ha inyectado la implementación de CuerpoSaltador.
Conclusiones
Como hemos podido ver en este post, el uso de la Inyección de Dependencias nos ayuda a desacoplar el código. Es decir, cambios en las clases independientes no afectan al comportamiento del resto de clase. Además, nos permite intercambiar el comportamiento de las acciones (el caso de CuerpoAndador y CuerpoSaltador).
La Inyección de Dependencias va estrechamente ligada con el Principio de Responsabilidad Única. Cada clase debe tener una única responsabilidad. En nuestro ejemplo, las implementaciones de ICuerpo son las que se encargan de las acciones que puede realizar el cuerpo de un androide, liberando de esta tarea a la clase Androide. Podríamos llevar el ejemplo a un caso más extremo haciendo que la responsabilidad de Disparar recayera en una clase llamada Brazo. Con la Inyección de Dependencias facilitamos el uso de este principio de programación.
Normalmente, se utiliza la Inyección de Dependencias por constructor juntamente con algún framework de Inversión de Control (que detallaremos en un próximo post). De esta manera, se da el control de la resolución de las dependencias a una clase externa, en vez de tener que preocuparnos nosotros en saber cómo resolver las dependencias de una clase.
Unit Testing
También hemos comentado la mejoría en el diseño de los tests. Siguiendo el ejemplo anterior, diseñamos unos tests de la clase Androide. El objetivo de los tests que diseñamos es probar el código de Androide. Por lo tanto, no queremos depender de acciones de terceros (en este caso, las llamadas a la interfaz ICuerpo). Con la Inyección de Dependencias podemos “falsear” las llamadas realizadas a ICuerpo dependiendo del caso de uso que estemos probando en el test. Por ejemplo:
public void AvanzarTest()
{
ICuerpo fakeCuerpo = new FakeCuerpo();
Androide androide = new Androide();
androide.SetCuerpo(fakeCuerpo);
androide.Avanzar();
}
La implementación de FakeCuerpo se crea exclusivamente para la ejecución de tests:
public class FakeCuerpo : ICuerpo
{
public FakeCuerpo()
{
}
public void Avanzar()
{
//Realiza una acción falsa de avanzar
}
public void Retroceder()
{
//Realiza una acción falsa de retroceder
}
public void Disparar()
{
//Realiza una acción falsa de disparar
}
}
En el post de Unit Testing explicaremos con más detalle el diseño y ejecución de Unit Tests así como frameworks que facilitan su desarrollo.