Inyección de dependencias
Explicaremos que es la inyección de dependencias y como se
aplica.
También veremos porque la inyección de dependencias ayuda a
tener un código más sencillo de mantener. Crearemos dos proyectos iguales, uno
con inyección de dependencias y el otro sin ellas. Veremos porque el código con
inyección de dependencias puede tener pruebas unitarias sólidas que solo
prueban pequeñas partes de ese código, mientras que en el código sin inyección
de dependencias las pruebas son frágiles, ya que dependerán de todo los
artefactos que componen el software.
Es un patrón con el cual se consigue que las cosas que cambian más (las implementaciones concretas) dependendan de las cosas que cambian menos (abstracciones o interfaces).
Uno de los principios de diseño de software más conocido es
el Principio de Responsabilidad única,
el cual dice: “Un objeto solo tiene que tener una razón para cambiar”. Para
conseguir que se cumpla este principio es necesario que el objeto delegue
responsabilidades a otros objetos.
Por ejemplo, un objeto PaySheet
tiene un método que calcula el total de la nómina. A primera vista esta
clase cumple con su cometido que es el cálculo de la nómina, pero veamos un
ejemplo muy básico de ese cálculo total:
public class Paysheet
{
public decimal Total { get; set; }
public decimal Irpf { get; set; }
public decimal Base { get; set; }
public decimal Ssg { get; set; }
public void CalculateTotalPaysheet()
{
var valueIrpf = (Base * Irpf) / 100;
var valueSsg = (Base * Ssg) / 100;
Total = Base - valueIrpf - valueSsg;
}
}
|
Como podemos ver el cálculo de la nómina depende de dos
descuentos, el Irpf y la seguridad social. Si mañana cambia la forma de
calcular el valor del irpf o el valor de la seguridad social, tendríamos dos
razones más para que este objeto cambie. Por lo tanto, esta clase tiene tres
razones por la que cambiar, calcular el valor del irpf, calcular el valor de la
seguridad social y calcular el total de la nómina.
Para evitar esto, tenemos que separar responsabilidades y lo
podríamos hacer de la siguiente forma:
public class Irpf
{
private readonly decimal _irpf = 18m;
public decimal Calculate(decimal paysheetBase)
{
return (paysheetBase * _irpf) / 100;
}
}
public class Ssg
{
private readonly decimal _ssg = 4.10m;
public decimal Calculate(decimal paysheetBase)
{
return (paysheetBase * _ssg) / 100;
}
}
public class Paysheet
{
private decimal _total;
private decimal _base;
public Paysheet()
{
_base = 2000m;
}
public void CalculateTotalPaysheet()
{
Irpf irpf = new Irpf();
var valueIrpf = irpf.Calculate(_base);
Ssg ssg = new Ssg();
var valueSsg = ssg.Calculate(_base);
_total = _base - valueIrpf - valueSsg;
}
}
|
Ahora si cambian los requisitos para calcular el valor del
descuento del irpf y de la seguridad social, la clase Paysheet no tiene que ser
modificada.
Este código tiene un problema y es que se están instanciando
dos objetos dentro del método CalculateTotalPaysheet.
¿Por qué esto es un problema? porque estos objetos instanciados no tienen
forma de poder ser sustituidos y evita que la clase se pueda heredar. Si
heredas de Paysheet, el método CalculateTotalPaysheet va a realizar siempre el
mismo cálculo. No se puede sustituir.
Vamos a ver qué pasa si heredamos:
public class Main
{
public Main()
{
PaysheetFirstChild paysheetFirstChild = new PaysheetFirstChild();
paysheetFirstChild.CalculateTotalPaysheet();
PaysheetSecondChild paysheetSecondChild = new PaysheetSecondChild();
paysheetSecondChild.CalculateTotalPaysheet();
}
}
public class PaysheetFirstChild: Paysheet
{
}
public class PaysheetSecondChild : Paysheet
{
}
public class Paysheet
{
private decimal _total;
private decimal _base;
public Paysheet()
{
_base = 2000m;
}
public void CalculateTotalPaysheet()
{
Irpf irpf = new Irpf();
var valueIrpf = irpf.Calculate(_base);
Ssg ssg = new Ssg();
var valueSsg = ssg.Calculate(_base);
_total = _base - valueIrpf - valueSsg;
}
}
|
Las dos sentencias en el cliente calcularán el mismo total.
No hay forma de hacer que el cálculo sea distinto.
Se propone utilizar el patrón de inyección de dependencias
para solucionar este problema.
public class Main
{
public Main()
{
IIrpf firstIrpf = new Irpf();
firstIrpf.Calculate(10000);
ISsg firstSsg = new Ssg();
firstSsg.Calculate(10000);
PaysheetFirstChild paysheetFirstChild = new PaysheetFirstChild(firstIrpf, firstSsg);
paysheetFirstChild.CalculateTotalPaysheet();
IIrpf secondIrpf = new Irpf();
secondIrpf.Calculate(2000);
ISsg secondSsg = new Ssg();
secondIrpf.Calculate(2000);
PaysheetSecondChild paysheetSecondChild = new PaysheetSecondChild(secondIrpf, secondSsg);
paysheetSecondChild.CalculateTotalPaysheet();
}
}
public class PaysheetFirstChild : Paysheet
{
public PaysheetFirstChild(IIrpf irpf, ISsg ssg)
: base(irpf, ssg)
{
}
}
public class PaysheetSecondChild : Paysheet
{
public PaysheetSecondChild(IIrpf irpf, ISsg ssg) : base(irpf, ssg)
{
}
}
public class Paysheet
{
public decimal Total { get; set; }
private decimal _base = 2000m;
private readonly IIrpf _irpf;
private readonly ISsg _ssg;
public Paysheet(IIrpf irpf, ISsg ssg)
{
_irpf = irpf;
_ssg = ssg;
}
public void CalculateTotalPaysheet()
{
var valueIrpf = _irpf.Calculate(_base);
var valueSsg = _ssg.Calculate(_base);
Total = _base - valueIrpf - valueSsg;
}
}
public interface IIrpf
{
decimal Calculate(decimal _paysheetBase);
}
public class Irpf:IIrpf
{
private readonly decimal _irpf = 18m;
public decimal Calculate(decimal _paysheetBase)
{
return (_paysheetBase * _irpf) / 100;
}
}
public interface ISsg
{
decimal Calculate(decimal _paysheetBase);
}
public class Ssg:ISsg
{
private readonly decimal _ssg = 4.10m;
public decimal Calculate(decimal _paysheetBase)
{
return (_paysheetBase * _ssg) / 100;
}
}
|
Está claro que tenemos que escribir más código, pero las
ventajas a posteriori son muchas. Si en la mitad del desarrollo de nuestra
aplicación nos cambian la forma de cálculo del Irpf solamente tengo que
modificar la clase Irpf. El resto de
mi código seguiría intacto. Sería fácil de modificar sin afectar a otras partes
de nuestra aplicación sólo al cálculo del irpf.
IIrpf firstIrpf = new Irpf();
ISsg firstSsg = new Ssg();
|
Ahora
podréis pensar que aún seguimos teniendo dos instancias en nuestro código, pero
hay una diferencia fundamental y es que el objeto Paysheet ha delegado su
instanciamiento a otro sitio, en este caso le ha dejado la labor al cliente.
Aquí es donde entre la inversión de control (IoC) que se encarga de hacer esta
instanciación por nosotros.
La
inversión de control no es más que delegar ciertos trabajos a “agentes
externos” a nuestro software. Por ejemplo, un agente externo a nuestro software
puede ser el Framework .NET. Dejamos que muchos trabajos los haga él, por
ejemplo:
string test = "Prueba-'1'";
test = test.Replace('-', '/');
|
En este
código estamos dejando que el Framework.Net se encargue de sustituir los
guiones por barras en la variable “test”.
Cuando
usamos el patrón de inyección de dependencias, para evitarnos la instanciación
utilizamos un framework que se encargue de ello por nosotros, como por ejemplo
Unity o Autofac.
En
nuestro proyecto estamos utilizando Autofac para resolver la inyección de dependencias. Un controlador común de
nuestras webapi puede ser como el siguiente:
[RoutePrefix("api/authentication")]
[ExceptionsHandler]
public class AuthenticationController : ApiController
{
private readonly IAuthorizationService _authorizationService;
public AuthenticationController(IAuthorizationService authorizationService)
{
Throw<ArgumentNullException>.WhenObject.IsNull(() => authorizationService);
_authorizationService = authorizationService;
}
[HttpGet]
[Route("getAuthenticatedUserAsync")]
[ExceptionsHandlerAttribute]
public async Task<HttpResponseMessage> GetAuthenticatedUserAsync()
{
dynamic user = await _authorizationService.GetAuthenticatedUserAsync();
if (user == null)
return null;
return Request.CreateResponse(HttpStatusCode.OK, new { user });
}
}
|
¿Quién
resuelve esta dependencia? Cuando se construye el controlador alguien tiene que
pasarle al constructor de esta clase un objeto que implemente la interfaz IAuthorizationService. El contenedor de
inversión de control (Autofac) se encarga de hacer esto por nosotros. Para que
Autofac funcione tenemos que configurarlo donde comienza nuestra aplicación.
public static void Configure()
{
var builder = new ContainerBuilder();
var config = GlobalConfiguration.Configuration;
var assemblies = new Assembly[]
{
typeof(Application.Authorization.AuthorizationService).Assembly,
typeof(PersonnelRepository).Assembly,
typeof(AuthorizationLogic).Assembly,
typeof(IUnitOfWork).Assembly,
typeof(DataFactory).Assembly,
typeof(IExceptionsManager).Assembly,
typeof(ClaimManagementFactory).Assembly,
typeof(ICoreService).Assembly
};
builder.RegisterAssemblyTypes(assemblies)
.AsImplementedInterfaces()
.InstancePerRequest();
//register web api controllers
builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
var container = builder.Build();
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
}
|
Resumiendo
este código, podríamos decir que el contenedor registra todos los ensamblados
de la aplicación, indicándole una clase o interfaz de cada uno de ellos. Cuando
la aplicación llame a AuthenticationController, Autofac construirá el objeto
que solicita el constructor buscando la clase que tenga implementada la
interfaz IAuthorizationService.
En el ejemplo mostraremos una solución con dos capas, la capa
de negocio y la capa de datos en su forma clásica de implementación y con
inyección de dependencias. Veamos el diagrama de uno y otro:
La solución con inyección de dependencias consta de cuatro
proyectos:
- ·
Business
- ·
Contracts
- ·
Entities
- ·
Data
La solución clásica consta de dos proyectos:
Como se puede ver en el diagrama la solución clásica tiene
una dependencia muy clara, el proyecto Business está fuertemente acoplado al
proyecto Data. La sustitución del proyecto data no se podrá realizar de forma sencilla.
La capa de negocio se verá afectada y también la tendríamos que modificar. En
cambio, en la solución con inyección de dependencias el proyecto Business no
conoce nada del proyecto Data. Los dos proyectos dependen débilmente del
proyecto Contracts. El proyecto Contracts contiene los contratos que debe
cumplir el proyecto Data, es decir, cualquier proyecto que cumpla esos
contratos podrá ser utilizado por el proyecto business.
En la solución de inyección de dependencias existe un
proyecto Entities que usa el proyecto Business y el proyecto Data. Este
proyecto evita el acoplamiento de Business con Data.
¿Se podría juntar lo que hay en el proyecto Contracts con lo
que hay en el proyecto Entities? La respuesta es sí. Las dos cosas tienen que ser utilizadas por el proyecto
Business y el proyecto Data. El motivo de separarlo es porque un proyecto
cliente que implemente la capa data y la capa business no tiene por qué conocer
las entidades. Si lo separamos en dos proyectos, el cliente no referenciará el
proyecto Entities, en cambio el proyecto Contracts si es necesario.
En el siguiente ejemplo vemos cómo se implementaría la
inyección de dependencias en la capa lógica de una aplicación y la diferencia
con otra que no implementa dicha inyección.
En el ejemplo el cliente utiliza la capa lógica de clientes
para obtener un listado de clientes. En el método convencional la capa lógica
inicializa en el constructor la capa de datos, en cambio con la inyección de
dependencias la capa lógica espera que le den un objeto que cumpla el contrato
IClientData.
Para usar la inyección de dependencias, a priori, tenemos
que escribir más código que del modo convencional, pero esto a largo plazo
terminamos agradeciéndolo.
Imaginar que mañana nos piden una modificación. Ahora
recibimos los clientes de dos proveedores de datos distintos. En el modo
convencional tendríamos que modificar la lógica de la clase clientLogic o crear
otra clase para el nuevo proveedor. En cambio con la inyección de dependencias
la lógica no hay que tocarla.