Contenedores de inyección de dependencias

En estos momentos, parece comúnmente aceptado que, en el diseño de una arquitectura mínimamente decente para una aplicación, es casi imprescindible el uso de un contenedor de inyección de dependencias. El uso de este tipo de componentes nos permitirá mantener lo más desacopladas posibles nuestras clases, con las innegables ventajas que ello supone (testeo, mantenibilidad, …).

Existen bastantes implementaciones a nuestra disposición de estos contenedores: Unity, Spring.Net, Autofac, NInject, StructureMap, CastleWindsor, cada uno con sus ventajas e inconvenientes. Es recomendable tener una idea mínima de cuales son los puntos fuertes de cada contenedor para, en función del tipo de aplicación que construyamos y de nuestros requerimientos, tener claro cual nos interesa elegir (porque hay que tener claro que ninguno es perfecto!). Una buena referencia es la información que nos proporciona Mark Seeman en el libro Dependency Injection in .NET.

Pero nosotros necesitamos de alguna manera, una forma común, independiente del contenedor que elijamos, de introducir este concepto dentro de la arquitectura de nuestra aplicación. La forma más sencilla es crear una abstracción del concepto de inyección de dependencias que sea común a todos los contenedores y así abstraernos de la implementación específica. La buena noticia es que esto ya lo han hecho por nosotros!. En Codeplex, existe un proyecto supervisado por el equipo de Patterns and Practices de Microsoft que precisamente ofrece esto (muy interesante revisar el porqué surgió este proyecto). Se trata de una pequeña librería que define una interfaz con una abstracción común del patrón ServiceLocator y una clase estática que hace las veces de variable de ambiente para acceder a esta abstracción. Además, ofrece una clase base para implementar con cada uno de los diferentes contenedores un proveedor especifico. Otra buena noticia es que muchos de los contenedores tienen ya implementaciones para usar esta librería. Es más, en la última versión de Unity, la 2.1, esta clase viene incluida en la propia dll de Unity (UnityServiceLocator).

Su uso es realmente sencillo. Lo único que debemos hacer es crear algún tipo de inicialización en nuestra aplicación de tal forma que, al iniciarse, configure el contenedor y asigne el proveedor específico a la variable de ambiente ofrecida por la librería.

Por ejemplo, para su uso con Unity, podría ser algo tan sencillo como esto:

    /// <summary>
    /// Implementación del IoC con Unity.
    /// </summary>
    public static class UnityIoC
    {
        /// <summary>
        /// Inicializa el contenedor.
        /// </summary>
        public static void Initialize()
        {
            // Creamos el contenedor
            var container = new UnityContainer();

            // Registramos los tipos
            container.AddNewExtension<CommonTypesExtension>();

            // Definimos el contenedor en el ServiceLocator
            ServiceLocator.SetLocatorProvider(() => new UnityServiceLocator(container));
        }
    }

Como vemos, creamos el contenedor, registramos los tipos y definimos el provider a través de la implementación por defecto del provider en Unity. Así de sencillo!!

Su uso en la inicialización de una aplicación simple de consola por ejemplo sería así:

    internal class Program
    {
        internal static void Main(string[] args)
        {
            // Inicializamos
            UnityIoC.Initialize();

            // Obtenemos la instancia de ambiente
            var locator = ServiceLocator.Current;

            // Resolvemos un tipo y ejecutamso una operación
            var testService = locator.GetInstance<ITestService>();
            testService.TestOperation();

            Console.ReadLine();
        }
    }

Como comentario, personalmente me gusta mucho el uso de las extensiones de Unity para realizar el registro de los tipos en el contenedor. Esto nos permite tener el código mucho más limpio, ya que cada clase tiene una responsabilidad bien marcada. IoCUnity establece la variable de ambiente y la extensión registra los tipos.

La implementación de una extensión también es muy sencilla:

 public class CommonTypesExtension : UnityContainerExtension
    {
        protected override void Initialize()
        {
            // Registrar tipos comunes
            Container.RegisterType<ILogger, MyLogger>();
            Container.RegisterType<ITestService, TestService>();
        }
    }

Utilizado de esta manera, tenemos el contenedor disponible desde cualquiera de nuestros ensamblados simplemente añadiendo una referencia a Microsoft.Practices.ServiceLocation.

¿La mejor manera? Con NuGet, por supuesto!

Proveedores personalizados

Pero, ¿que pasa si necesitamos algo más de lo que nos deja hacer el proveedor por defecto?

Pongamos un ejemplo. Una de las cosas que me gusta de la guía de arquitectura N Capas orientada al dominio publicada desde Microsoft España es el planteamiento que se hace con el contenedor de inyección de dependencias. En vez de de utilizar un único contenedor, organizan una jerarquía de contenedores desde un contenedor raíz añadiendo tantos contenedores hijos como “entornos” queramos tener en el sistema. Después, elegimos el entorno concreto definiendo una variable en el archivo de configuración de la aplicación. Una de las grandes ventajas de este sistema es que podríamos variar el comportamiento de nuestra aplicación ¡incluso en tiempo de ejecución!

Para incorporar esta funcionalidad, deberemos implementar nuestro proveedor personalizado. Pero no os preocupéis, tampoco es nada complicado. Sólo debemos heredar de la implementación por defecto que encontramos en Microsoft.Practices.ServiceLocation, que se encuentra en la clase ServiceLocatorImplBase.

Aquí va una posible implementación:

using System;
using System.Collections.Generic;
using System.Configuration;

using Microsoft.Practices.ServiceLocation;
using Microsoft.Practices.Unity;

namespace IoC.Unity
{
    public class CustomUnityServiceLocator : ServiceLocatorImplBase
    {
        #region Members

        private readonly IDictionary<string, IUnityContainer> containersDictionary;

        #endregion

        #region Constructores

        public CustomUnityServiceLocator(IDictionary<string, IUnityContainer> containersDictionary)
        {
            if (containersDictionary == null)
            {
                throw new ArgumentNullException("containersDictionary");
            }

            this.containersDictionary = containersDictionary;
        }

        #endregion

        #region Métodos

        protected override object DoGetInstance(Type serviceType, string key)
        {
            var containerName = this.GetContainerName();
            this.CheckContainerName(containerName);

            var container = this.containersDictionary[containerName];

            return container.Resolve(serviceType, key, new ResolverOverride[0]);
        }

        protected override IEnumerable<object> DoGetAllInstances(Type serviceType)
        {
            var containerName = this.GetContainerName();
            this.CheckContainerName(containerName);

            var container = this.containersDictionary[containerName];

            return container.ResolveAll(serviceType, new ResolverOverride[0]);
        }

        protected virtual string GetContainerName()
        {
            return ConfigurationManager.AppSettings["defaultIoCContainer"];
        }

        private void CheckContainerName(string containerName)
        {
            if (string.IsNullOrWhiteSpace(containerName))
            {
                throw new ArgumentNullException("containerName");
            }

            if (!this.containersDictionary.ContainsKey(containerName))
            {
                throw new InvalidOperationException("No se encuentra la clave en el diccionario");
            }
        }

        #endregion
    }
}

Como veis, sólo estamos obligados a definir dos métodos abstractos de la clase base (DoGetInstance y DoGetAllInstances) que se encargan de resolver realmente la instancia (o todas las instancias) utilizando el contenedor que tengamos definido en la configuración.

Esto es todo. Ahora a desacoplar nuestras aplicaciones.

Solución de ejemplo en GitHub