viernes, 8 de noviembre de 2013

Dependency Injection e IoC Container

Dependency Injection (DI)

"Suministrar a una clase los objetos de los que depende en lugar de que sea la propia clase quien lo cree".

En el anterior post Dependency Inversion Principle hablamos de cómo siempre crear dependencias a clases abstractas e interfaces de un nivel de abstracción igual o superior al de nuestra clase.


Esto no llevó a refactorizar un pequeño diseño de esto:

a esto: 

Pero intencionadamente dejé pendiente cómo nuestra clase ClientsBL iba a obtener una referencia a ClientsDAL ya que no podemos crear una instancia de una interfaz (IClientsDAL). 

En el anterior post Dependency Inversion Principle ya adelanté que la solución a ese problema es la DI, pero he preferido explicar esta solución en un post aparte porque creo que es importante aclarar conceptos antes de seguir.

Dependency Inversion Principle, Dependency Injection e Inversion of Control
A menudo estos tres conceptos se confunden o incluso se piensan que son el mismo. Bien, realmente son tres conceptos distintos aunque es cierto que suelen aparecer muy relacionados, quizá ese sea el motivo de la confusión.

Voy a intentar explicar cada uno de ellos:
Dependency Inversión Principle: Como hemos visto en el post Dependency Inversion Principle, la inversión de dependencias hace referencia a la forma de los objetos de los que depende nuestra clase. Dependemos de interfaces y clases abstractas en lugar de depender de clases concretas.

Dependecy Injection: Como veremos en este post, hace referencia a cómo una clase obtiene sus dependencias, es decir, los objetos de los que depende.

Inversion of Control: En el próximo post veremos este concepto en detalle. Este es un concepto muy amplio y aplicable a diferentes contextos. Hace referencia a sobre quién inicia las llamadas.

Sé que las definiciones anteriores no aclaran mucho, pero por el momento lo importante es tener claro que son tres conceptos diferentes aunque trabajan bien juntos.

Bueno vamos a lo que toca ahora, explicar el segundo de estos conceptos. 

Dependency Injection (DI) e Ioc Container.

Antes de empezar y por si alguien trae ideas preconcebidas, lo diré de forma que todos nos entendamos:
Dependency Injection != Usar un IoC Container
  • Se puede realizar DI sin usar un IoC Container ni ninguna otra herramienta externa.
  • Se puede usar un IoC Container sin realizar DI, de hecho esta es una mala práctica muy extendida.

DI es un patrón usado para crear instancias de objetos con funcionalidad de la que otros objetos dependen sin conocer en tiempo de compilación que clases concretas se usará para proporcionar la funcionalidad.
DI hace referencia a la forma en que una clase obtiene sus dependencias. Si aplicando el principio de inversión de dependencias nuestras clases dependen de clases abstractas e interfaces que no podemos instanciar, las instancias concretas de esas interfaces deben ser proporcionadas a nuestra clase "desde fuera".  Las formas más habituales de "inyectar" esas dependencias a nuestras clases son:

Inyección por el constructor. En nuestra aplicación  para ACME Suministros S.A. la clase ClientsBL quedaría así si inyectamos la dependencia de IClientsDAL por el constructor:

    public class ClientsBL
    {
        private IClientsDAL dalClient;
        
        public ClientsBL(IClientsDAL cliDal)
        {
           dalClient=cliDal;
        }
        public IEnumerable<ClientData> GetClientsData()
        {
            return dalClient.GetClientData();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
           // Uso de ClientsBL inyectando las dependencias
           // mediante el constructor
           IClientDAL cliDal = new ClientDAL();
           ClientsBL cliBL = new ClientsBL(cliDal);
           var cliData = cliBL.GetClientsData();
        }
    }

Inyección mediante una propiedad. Veamos cómo sería la inyección mediante una propiedad:
    public class ClientsBL
    {
        private IClientsDAL dalClient;
        
        public IClientsDAL CliDal
        {
           get{return dalClient;}
           set{dalClient=value;}
        }
        public IEnumerable<ClientData> GetClientsData()
        {
            return dalClient.GetClientData();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
           // Uso de ClientsBL inyectando las dependencias
           // mediante una propiedad
           IClientDAL cliDal = new ClientsDAL();
           ClientsBL cliBL = new ClientsBL();
           cliBL.CliDal=cliDal;
           var cliData = cliBL.GetClientsData();
        }
    }

Inyección mediante un método. Así sería mediante un método.
    public class ClientsBL
    {
        private IClientsDAL dalClient;
        
        public void SetCliDal(IClientsDAL cliDal)
        {           
           dalClient=cliDal;
        }
        public IEnumerable<ClientData> GetClientsData()
        {
            return dalClient.GetClientData();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
           // Uso de ClientsBL inyectando las dependencias
           // mediante un método
           IClientsDAL cliDal = new ClientsDAL();
           ClientsBL cliBL = new ClientsBL();
           cliBL.SetCliDal(cliDal);
           var cliData = cliBL.GetClientsData();
        }
    }

Implementar DI con Factorías
Una forma de cambiar el código que crea los objetos ClientsBL y ClientsDAL en el método Main sería el uso de una factoría. Por ejemplo:
public class Factory
{
   public IClientsDAL CreateClientsDAL()
   {
      return new ClientsDAL();
   }

   public ClientsBL CreateClientsBL()
   {
      return new ClientsBL(CreateClientsDAL());
   }
}


class Program
{
   static void Main(string[] args)
   {
      // Uso de ClientsBL inyectando las dependencias 
      // mediante una factoría
      ClientsBL cliBL = new Factory().CreateClientsBL();
      var cliData = cliBL.GetClientsData();
   }
}


Usar factorías para implementar DI tiene una serie de problemas que hay que tener en cuenta:
  • Todo el código es "hardcode". Todas las opciones están implementadas en la propia factoría, lo que hace que la factoría esté muy relacionada con una implementación en particular.
  • Las factorías son personalizadas. Cada factoría debe implementarse de forma muy personalizada para que se adapte al contexto en el que se usan.
  • Tiempo de compilación. Todos los objetos de los que depende el objeto que queremos crear deben conocerse en tiempo de compilación.
  • Tiempo de vida de los objetos. Es difícil manejar el tiempo de vida de los objetos.
  • Grafos de dependencias muy pequeños. El mantenimiento de una factoría de este tipo es factible si el grafo de dependencias de los objetos es muy pequeño. Cuando el grafo empieza a crecer el coste de mantenimiento de la factoría se dispara.
Teniendo todo esto en cuenta veamos otra alternativa.

Implementar DI con IoC Container
Muchas de las deficiencias y problemas que hemos visto hasta ahora al inyectar dependencias se puede resolver con un contenedor de inversión de control.

Un contenedor de inversión de control (IoC Container) también llamado contenedor de inyección de dependencias (DI Container) es un componente responsable de la gestión de los objetos de la aplicación. 
Hay innumerables IoC Containers entre los que elegir (Unity, Autofac, Spring.NET, Ninject, etc.). 

La idea básica en todos ellos es la siguiente:
  1. Registramos todos los componentes en el contenedor.
  2. Resolvemos el componente raíz que necesitamos. Las dependencias son inyectadas automáticamente por el contenedor.
Las ventajas de usar el container son muchas. 
  • Registramos todos los tipos al arrancar la aplicación en un sólo sitio.
  • Se encarga de crear automáticamente el grafo de dependencias.
  • La mayoría de IoC Container permite la configuración por código o de forma declarativa (archivo XML).
  • Gestiona el ciclo de vida de los objetos.
  • etc.
Veamos cómo sería el ejemplo anterior usando UnityContainer:
using Microsoft.Practices.Unity;

namespace Acme.BL
{
    public class ClientsBL : IClientsBL
    {
        private IClientsDAL dalClient;

        public ClientsBL(IClientsDAL dalCli)
        {
            dalClient = dalCli;
        }

        public IEnumerable<ClientData> GetClientsData()
        {
            return dalClient.GetClientData();
        }
    }

    public interface IClientsBL
    {
        IEnumerable<ClientData> GetClientsData();
    }

    public interface IClientsDAL
    {
        IEnumerable<ClientData> GetClientData();
    }
}

namespace Acme.DAL
{
    public class ClientsDAL : IClientsDAL
    {
        public IEnumerable<ClientData> GetClientData()
        {
            var dataResult = Enumerable.Empty<ClientData>();
            // Get data from Database.
            // and fill dataResult
            return dataResult;
        }
    }
}


namespace DIMain
{
    public static class Program
    {
        static void Main(string[] args)
        {
            var container = BuildAndConfigureContainer();

            // Resolvemos la interfaz IClientsBL. 
            // Por cómo hemos configurado el container esto
            // devolverá un objeto de tipo ClientsBL al que el
            // container le ha inyectado la dependencia CliensDAL.
            //
            // Es como haber hecho lo siguiente:
            //
            //  var cliDal = new ClientsDAL();
            // var clientBL = new ClientsBL(cliDAL);
            //  var clientData = clientBL.GetClientData();
            //
            var clientBL = container.Resolve<IClientsBL>();
            var clientData = clientBL.GetClientsData();
        }

        /// 
        /// Crea un configura un container.
        /// 
        /// 
        /// Un UnityContainer configurado.
        /// 
        private static IUnityContainer BuildAndConfigureContainer()
        {
            // Creamos el container
            var container = new UnityContainer();

            // Registramos el tipo IClientDAL
            // Esto es, "Cuando necesite un objeto que cumpla 
            // la interfaz IClientsDAL, 
            // devuelveme una instancia de un objeto ClientsDAL"
            container.RegisterType<IClientsDAL, ClientsDAL>();

            // Registro el tipo IClientsBL.
            container.RegisterType<IClientsBL, ClientsBL>();

            return container;
        }
    }
}

Mala práctica común con IoC Container
Antes he mencionado que es posible usar IoC Container sin que ello implique DI. Antes de acabar este post me gustaría que viéramos una mala práctica muy común sobre esto. Supongamos esta implementación de la clase ClientsBL:

    public class ClientsBL : IClientsBL
    {
        private IClientsDAL dalClient;

        public ClientsBL()
        {
            // Por simplicidad no explico cómo crear el Singleton del container
            dalClient = Container.Instance.Resolve<IClientsDAL>();
        }

        public IEnumerable<ClientData> GetClientsData()
        {
            return dalClient.GetClientData();
        }
    }

En el constructor de ClientsBL obtenemos una instancia de un objeto que cumple la interfaz IClientsDAL a través del container.


¡Esto no es inyección de dependencias!

Estamos usando el IoC Container sí, pero no estamos realizando inyección de dependencias. La inyección de dependencias implica que no creamos los objetos de los que nuestra clase depende en nuestra propia clase, sino que son inyectados. En este caso lo único que hemos hecho es cambiar la dependencia que teníamos originalmente con ClientesDAL por una dependencia con el container.
Aunque vais a ver que esto en muchos sitios se "vende" como inyección de dependencias porque se usa un IoC Container, nosotros ya sabemos que son cosas distintas y que para que haya inyección de dependencias las dependencias tienen que inyectarse bien por el constructor, mediante una propiedad o mediante un método.

En el próximo post explicaré en detalle el concepto "Inversión de Control".


5 comentarios:

  1. Hola Miguel,
    me ha gustado mucho el post, dejas claro que DI e IoC no es lo mismo. Me gustaría un ejemplo de DI con IoC. Gracias

    ResponderEliminar
  2. Hola Jaume.
    Antes de subir un ejemplo de DI con IoC Container quería acabar el post sobre IoC. Ya está publicado el post Inversión de Control . Así que me pongo a preparar un pequeño ejemplo de cómo usar un IoC Container para realizar DI.

    Te aviso cuando lo tenga listo.
    Gracias por seguir el blog.

    ResponderEliminar
  3. Hola Miguel, podrías poner un ejemplo de un caso real donde se pueda usar un framework de IoC y si puedes realizar un ejercicio para poder descargarlo, sería de mucha ayuda para ponerlo en practica y usarlo en donde se le requiera.

    ResponderEliminar
  4. No se si comprendi bien.

    Pero la ventaja de la Inyeccion de dependecias es evitar atar tu codigo al contructor de la depedencia, es decir, que si agregan un nuevo argumento en el constrcutor de la dependencia, tu codigo no tendria que cambiar, dado que tu recibes un OBJETO.

    Alguna otra ventaja que no halla visto??

    ResponderEliminar
    Respuestas
    1. Realmente la ventaja es no depender de objetos concretos sino de interfaces o clases abstractas.
      Las ventajas son varias:

      Como hemos visto en el post al hacer que ClientsBL dependa de la interfaz IClientsDAL podríamos pasar por el constructor por ejemplo la clase SqlClients que obtiene datos de clientes desde una base de datos SQL Server. TextFileClients que obtine los datos de clientes de ficheros de texto plano, etc. Es decir, cambiamos el acceso a datos sin tener que cambiar la capa de negocio

      Facilita los test unitarios de nuestra clase ClientsBL pudiendo pasar objetos mock por el constructor.

      En definitiva permite crear sistemas más flexibles y que se pueden probar con más facilidad.

      Eliminar