Inyección de Dependencias en .Net Framework

JorTurFer       No hay comentarios en Inyección de Dependencias en .Net Framework

Inyeccion de dependencias

Un mes ya… y hoy vengo a hablaros de la «Inyección de Dependencias» (DI). La inyección de dependencias consiste de manera resumida en evitar el acoplamiento entre clases utilizando interfaces. Gracias esto, conseguimos que cada clase tenga una función única, facilitando así el mantenimiento y el soporte de nuestro código.

Esto que puede parecer un poco lioso a simple vista, y puede parecer mucho trabajo, se puede conseguir fácilmente cambiando levemente nuestro patrón de diseño. Vamos a imaginar que tenemos una clase que lee unos datos desde una base de datos con Entity Framework («Database First» o «Code First«), y los envía por email:

 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using PostInyeccion.Models;

namespace PostInyeccion
{
    class Program
    {
        static void Main(string[] args)
        {
            GeneradorInformes generador = new GeneradorInformes();
            generador.GenerarInforme();
        }
    }

    internal class GeneradorInformes
    {
        internal void GenerarInforme()
        {
            //Obtenemos los datos desde el contexto
            using (PostDbContext contexto = new PostDbContext())
            {
                var profesores = contexto.Profesores.Include(x => x.Cursos)
                                                    .ThenInclude(x => x.Alumnos).ToList();
                
                //Trabajo de maquetacion
                //.......                           

                //Enviamos el correo
                var emailSender = new EmailSender();
                var envioOK = emailSender.Enviar(profesores);
                if(!envioOK)
                {
                    //Registramos el fallo en un log
                }
            }
        }
    }

    public class EmailSender
    {
        public bool Enviar(List profesores)
        {
            //Enviar correo
            return true;
        }
    }
}

En primer lugar, vamos a analizar los posibles problemas de esta clase:

  1. Baja mantenibilidad por el acoplamiento en entre clases: Es la propia clase la que instancia las que va necesitando, provocando que cualquier cambio en las clases "auxiliares" pueda cambiar el comportamiento.
  2. Dificultad para realizar pruebas unitarias: En el caso en el que queramos realizar pruebas unitarias, es requisito que todos los servicios estén activos. Desde el punto de vista de las pruebas, esto es nefasto, podemos no tener acceso al servidor de correo, o a la base de datos... Con este patrón, es muy difícil testear nuestra clase.

Inyección de Dependencias

Vamos a ver que pasa si refactorizamos la clase para inyectar las dependencias, de manera que no tengamos dependencias:

 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using PostInyeccion.Models;

namespace PostInyeccion
{
    class Program
    {
        static void Main(string[] args)
        {
            //Generamos la clase que hara el trabajo
            GeneradorInformes generador = new GeneradorInformes();

            //Obtenemos los datos desde el contexto
            using (PostDbContext contexto = new PostDbContext())
            {
                var profesores = contexto.Profesores.Include(x => x.Cursos)
                                                    .ThenInclude(x => x.Alumnos).ToList();

                //Instanciamos la clase de envio de emails
                var emailSender = new EmailSender();
                generador.GenerarInforme(profesores, emailSender);
            }
        }
    }

    public class GeneradorInformes
    {
        public  void GenerarInforme(List profesores, IEmailSender emailSender)
        {
            //Trabajo de maquetacion
            //.......
            
            //Enviamos el correo
            var envioOK = emailSender.Enviar(profesores); 
            if(!envioOK) 
            { 
                //Registramos el fallo en un log 
            }
        }
    }
}

interface IEmailSender
{
    bool Enviar(List profesores);
}

public class EmailSender : IEmailSender
{
    public bool Enviar(List profesores)
    {
        //Enviar correo
    }
}

Hemos conseguido una mayor mantenibilidad al abstraer totalmente a la clase "GeneradorInforme" de las labores propias de envío de correo y de obtención de datos. Y ahora es cuando me dices, "Pero no hace falta utilizar inyección de dependencias para conseguir lo que has conseguido...". Efectivamente, para simplemente desacoplar las clases, no sería necesario. Podríamos haberle pasado al método una instancia de EmailSender en vez de su interfaz pero... Si queremos hacer pruebas unitarias, eso no es suficiente. Seguiríamos con el problema de que si el servidor de correo esta caído, nuestra prueba fallaría. Ahí radica la potencia de trabajar inyectando dependencias. En nuestro proyecto de pruebas unitarias, simplemente tendríamos que añadir una nueva clase que implemente "IEmailSender" y que no se conecte a ningún servidor, o que simule la conexión.

En el caso del ejemplo, hemos inyectado las dependencias mediante un parámetro, pero esta solo es una de las maneras. Podemos inyectar dependencias de varias maneras:

  1. Inyección de dependencias en parámetros de constructor.
  2. Inyección de propiedades.
  3. Inyección de dependencias en parámetros de métodos.

Vamos a ver más detalladamente cada uno.

Inyección de dependencias en parámetros de constructor

 
var emailSender = new EmailSender();
GeneradorInformes generador = new GeneradorInformes(emailSender);
//--------------------------------------------------------------
public class GeneradorInformes
{
    //Dependencia para el envio de correos
    IEmailSender _emailSender;

    //Pasamos la dependencia en el construcor
    public GeneradorInformes(IEmailSender emailSender)
    {
        _emailSender = emailSender;
    }

    public void GenerarInforme(List profesores)
    {
        //Trabajo de maquetacion
        //.......

        _emailSender.Enviar(profesores);
    }
}

Inyección de propiedades

 
//Instanciamos la clase de envio de emails
var emailSender = new EmailSender();
GeneradorInformes generador = new GeneradorInformes();
generador.EmailSender = emailSender;
generador.GenerarInforme(profesores);
//-----------------------------------------------------
public class GeneradorInformes
{
    //Propiedad con la dependencia
    public IEmailSender EmailSender { get; set; }
    public GeneradorInformes()
    {
    }

    public void GenerarInforme(List profesores)
    {
        //Trabajo de maquetacion
        //.......

        EmailSender.Enviar(profesores);
    }
}

En este caso, hay que tener en cuenta que la dependencia no estará disponible durante el constructor, por si la necesitásemos.

Inyección de dependencias en parámetros de métodos

Es el caso que hemos visto en el primer ejemplo.

Contenedor IOC

La utilización de contenedores IOC (Inversion Of Control) nos abstraen de la necesidad de generar las clases cada vez. Simplemente es configurar un contenedor el cual nos sirva las dependencias que necesitamos en cada momento. Esto facilita mucho la labor de trabajo al no tener que gestionar nosotros mismos las clases. Existen bastantes sistemas IOC para .NET, pero en este caso, vamos a utilizar "Microsoft.Extensions.DependencyInjection".

Para eso, tenemos que añadir el paquete desde NuGet:

DI

O vía consola:

PM->Install-Package Microsoft.Extensions.DependencyInjection -Version 2.1.1

Ahora vamos a registrar los servicios (Asumo que tenemos un DbContext creado en base a los post de Entity Framework  Core), pero para ello, primero tenemos que crear el interfaz IGeneradorInformes y hacer que Generador de informes la implemente:

 
public interface IGeneradorInformes
{
    void GenerarInforme();
}

//.........

public class GeneradorInformes : IGeneradorInformes
//......

Una vez que lo tengamos, empezamos a registrar los servicios:

 
//Generamos la coleccion de servicios
IServiceCollection serviceCollection = new ServiceCollection();
//Registramos el DbContext
serviceCollection.AddDbContext(options=>
                options.UseMySql("Server=localhost;Database=postefcore;Uid=root;Pwd=root;"));
//Registramos el EmailSender
serviceCollection.AddSingleton();
//Registramos el generador de informes
serviceCollection.AddSingleton();

//Construimos el contendor IoC
var services = serviceCollection.BuildServiceProvider();

Después, simplemente necesitamos llamar a su metodo "GetService<T>()" para obtener una instancia de la clase concreta con todas sus dependencias inyectadas:

 
IGeneradorInformes generador = services.GetService();
generador.GenerarInforme();

Este sistema funciona muy bien en MVC 5 y MVC Core, ya que esta construido totalmente sobre el sistema de inyección de dependencias, pero es cierto que en aplicaciones que no sean MVC, hay que trabajarselo un poco. Digo esto porque necesitamos mantener activa una referencia a "services" para poder pedirle las clases. Esto lo podemos resolver bastante fácilmente con un tipo estático que almacene nuestra referencia:

 
using Microsoft.Extensions.DependencyInjection;
using System;

namespace PostInyeccion
{
    public static class Injector
    {
        static IServiceProvider _proveedor;

        public static void GenerarProveedor(IServiceCollection serviceCollection)
        {
            _proveedor = serviceCollection.BuildServiceProvider();
        }

        public static T GetService()
        {
            return _proveedor.GetService();
        }
    }
}

Veamos como usarlo en nuestro código:

 
static void Main(string[] args)
{
    //Generamos la coleccion de servicios
    IServiceCollection serviceCollection = new ServiceCollection();
    //Registramos el DbContext
    serviceCollection.AddDbContext(options =>
        options.UseMySql("Server=localhost;Database=postefcore;Uid=root;Pwd=root;"));
    //Registramos el EmailSender
    serviceCollection.AddSingleton();
    //Registramos el generador de informes
    serviceCollection.AddSingleton();

    Injector.GenerarProveedor(serviceCollection);
    //Obtengo clase desde el IOC
    IGeneradorInformes generador = Injector.GetService();
    generador.GenerarInforme();
}

Con esto, registraremos todas las dependencias al inicio de nuestro programa, y llamaremos a Injector.GetService<T>() cuando necesitemos utilizar una dependencia.

A simple vista, parece que es mucho trabajo y poca recompensa, pero en las siguientes entradas veremos las ventajas que aporta trabajar así cuando hablemos de las pruebas unitarias y del "Moking".

Puedes descargar el código de ejemplo desde este enlace.

Deja un comentario