Unity Application Block

Unity Application Block es un contenedor para inyección de dependencias (Dependency Injection). Soporta inyección de constructores, propiedades y métodos. Puede configurarse mediante código o archivo de configuración. También incluye un mecanismo de intercepción para incorporar lógica transversal, como logging y autorización, al estilo de programación orientada a aspectos. Unity Application Block es parte de Enterprise Library.

Dependency Injection

Dependency Injection es un patrón de diseño que facilita el desarrollo de aplicaciones modulares y extensibles. Es especialmente adecuado en el ámbito de las aplicaciones empresariales que suelen ser desarrolladas como módulos, componentes y servicios independientes. Cada uno de estos componentes puede evolucionar por separado, en diferentes equipos y en diferentes tiempos, y deben ser re-ensamblados continuamente para formar una aplicación cohesionada.
Dependency Injection también facilita incorporar testing al proceso de desarrollo, permitiendo reemplazar las implementaciones de servicios y componentes por objetos simulados con facilidad.

Para usar el patrón Dependency Injection debe mantener las referencias a los servicios y componentes usando Interfaces o clases base, y no debe crear las clases concretas que implementan estas interfaces, esa labor es delegada al contenedor. El contenedor Dependency Injection sabe como resolver las dependencias de una clase, mantiene mapeos que indican que implementación de una interfaz debe usar, es capaz de crear instancias de clases concretas e inyectarlas. Puede mantener también referencias a servicios en forma de singleton, manteniendo el control del tiempo de vida de estas instancias.
Los mapeos del contenedor se pueden configurar como código o mediante un archivo de configuración lo que permite cambiar partes de la aplicación sin tener que recompilar los fuentes.

El contenedor inyecta las dependencias a los objetos usualmente al momento de crearlos. Debe crear las instancias de los objetos que tengan dependencias a través del contenedor usando el método Resolve<t>. Esto da la posibilidad al contenedor de inyectar las dependencias mediante llamadas a un constructor, propiedades y métodos. Las dependencias pueden definirse en la misma clase usando atributos, o en el archivo de configuración.
Cuando se crea un objeto usando el contenedor este inyecta las dependencias no solo en el objeto creado sino a toda la ramificación de dependencias. Si una de las dependencias tiene a su vez otras dependencias, estas ultimas también se inyectaran.

Las principales características de Unity Application Block son:

  • Soporta inyección de constructores, propiedades y métodos.
  • Puede utilizarse como un contenedor jerárquico.
  • La inyección de constructores es automática, no requiere atributos o configuración.
  • La inyección de propiedades y métodos requiere atributos o configuración.
  • El contenedor puede crear clases concretas no incluidas en el mapeo del contenedor.
  • Puede usarse el estilo de interfaz fluida (fluent interface) para configurar el contenedor.
  • El contenedor puede ser extendido.
  • Soporta intercepción.
Operatoria

El primer paso para usar Unity es crear una instancia del contenedor. En una aplicación puede haber mas de un contenedor, incluso puede organizar los contenedores jerárquicamente, de esta forma si una referencia no es encontrada se buscara en el contenedor padre, y así sucesivamente hacia arriba en la jerarquía de contenedores, si el ultimo contenedor de la jerarquía tampoco puede resolver la dependencia entonces se lanzara una excepción.

Un contenedor es representado por la clase UnityContainer. En el siguiente ejemplo se crea una instancia de la clase UnityContainer y se asigna a la variable myContainer:

IUnityContainer myContainer = new UnityContainer();

Una vez creado el contenedor, el siguiente paso es configurar el mapeo, información que define como resolver las dependencias. Esto puede hacerse mediante código o usando el archivo de configuración. En el siguiente ejemplo usamos código para definir un mapeo entre la interfaz IMyService y la case concreta MyService. El contenedor creara e inyectara una instancia de la clase concreta MyService cada vez que encuentre una dependencia a la interfaz IMyService.

myContainer.RegisterType<IMyService, MyService>();

También puede mapear una clase base y una subclase, por ejemplo:

myContainer.RegisterType<MyServiceBase, DataService>();

Puede dar un nombre a un mapeo lo cual permite mantener diferentes clases concretas mapeadas a una misma interfaz. En el siguiente ejemplo se registran dos mapeos para la interfaz IMyService que se distinguen por nombre.

myContainer.RegisterType<IMyService, MyService>("servicioNormal");
myContainer.RegisterType<IMyService, MyFastService>("servicioRapido");

Cuando se defina una dependencia se deberá especificar cual de las dos implementaciones debe inyectar el contenedor indicando el nombre.
Puede también registrar un objeto preexistente en el contenedor usando el método RegisterInstance, este se comportaran como un singleton, es decir, el contenedor guardara una referencia a la instancia e inyectara esta misma instancia para todas las dependencias. En el siguiente ejemplo se crea una instancia del servicio EmailService y se registra en el contenedor.

EmailService myEmailService = new EmailService();
myContainer.RegisterInstance<IMyService>(myEmailService);
LifeTime

Puede pasar al método RegisterType adicionalmente un objeto LifeTimeManager para controlar el tiempo de vida (LifeTime) de las instancias.
Cuando no pasa un objeto LifeTimeManager esta indirectamente indicando al contenedor que los objetos son transitorios y por lo tanto el contenedor debe crear una instancia nueva cada vez que inyecta una dependencia.
Aveces es deseable usar otra aproximación, por ejemplo que el contenedor guarde una referencia a una instancia única y siempre inyecte la misma, es decir, un comportamiento similar a un singleton. Para lograr esto debe pasar un objeto ContainerControllerLifeTimeManager cuando registre el mapeo usando el método RegisterType. En el siguiente ejemplo se registra la clase CustomerService como un singleton, la primera vez que se requiera la dependencia sera creado el objeto y se guardara la referencia.

myContainer.RegisterType<IMyService, CustomerService>(new ContainerControlledLifetimeManager());

Existen otras posibilidades de configuración para el tiempo de vida. ExternallyControlledLifetimeManager, indica al contenedor siempre inyecte la misma instancia pero que mantenga una referencia débil (weak reference) al objeto, de tal forma que el tiempo de vida de la instancia puede ser controlado externamente por la aplicación. PerThreadLifetimeManager permite mantener una instancia para cada Thread (hilo de ejecución).

Estilo de Interfaz Fluida

Unity Application Block soporta el estilo de interfaz fluida. Los métodos que se usan para configurar el contenedor devuelven como parámetro de retorno una referencia al propio contenedor. Esto hace posible encadenar los métodos en una sola instrucción disminuyendo la verbosidad del proceso. En el siguiente ejemplo en una sola instrucción se crea el contenedor y se registran dos mapeos y una instancia.

EmailService myEmailService = new EmailService();
IUnityContainer myContainer = new UnityContainer()
   .RegisterType<IMyService, DataService>()
   .RegisterType<IMyUtilities, DataConversions>()
   .RegisterInstance<IMyService>(myEmailService);
Configuración Usando el Archivo de Configuración

También puede configurar el contenedor usando el archivo de configuración. En el siguiente ejemplo se definen alias para los tipos mas usados, de esta forma se evita indicar el nombre completamente calificado cada vez que se quiere indicar un tipo. Se definen diferentes tipos de mapeos e instancias explicados con comentarios.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="unity"
              type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,
                 Microsoft.Practices.Unity.Configuration, =1.2.0.0,
                 Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
  </configSections>

  <unity>

    <!-- En esta seccion puede definir alias para no escribir el nombre completo de los tipos cada vez -->
    <typeAliases>

      <!-- Alias para los Lifetime manager -->
      <typeAlias alias="singleton"
           type="Microsoft.Practices.Unity.ContainerControlledLifetimeManager,
               Microsoft.Practices.Unity" />

      <!-- Alias para mis tipos -->
      <typeAlias alias="IMyInterface"
           type="MyApplication.MyTypes.MyInterface, MyApplication.MyTypes" />
      <typeAlias alias="MyRealObject"
           type="MyApplication.MyTypes.MyRealObject, MyApplication.MyTypes" />
      <typeAlias alias="MyRealObjectFast"
           type="MyApplication.MyTypes.MyRealObjectFast, MyApplication.MyTypes" />
    </typeAliases>
    
    <!-- Aqui define la configuracion de uno o mas contenedores -->
    <containers>
    
      <container name="containerOne"> 
        <types>
          <!-- Mapeo sin usar alias -->
          <type type="MyApplication.MyTypes.MyInterface, MyApplication.MyTypes" mapTo="MyApplication.MyTypes.MyRealObject, MyApplication.MyTypes" />

          <!-- Mapeo usando alias definidos arriba -->
          <type type="IMyInterface" mapTo="MyRealObject" />

          <!-- Mapeos con nombre -->
          <type type="IMyInterface" mapTo="MyRealObject" name="servicioLento" />
          <type type="IMyInterface" mapTo="MyRealObjectFast" name="servicioRapido" />

          <!-- Inidicar un LifeTimeManager usando alias -->
          <type type="IMyInterface" mapTo="MyRealObject">
            <lifetime type="singleton" />
          </type>
        </types>

        <!-- Puede definir instancias, en este caso tipos sencillos -->
        <instances>
          <add name="MyInstance1" type="System.String" value="Some value" />
          <add name="MyInstance2" type="System.DateTime" value="2008-02-05T17:50:00"  />
        </instances>

      </container>

      <!-- ... mas contenedores aquí ... -->

    </containers>
  </unity>
</configuration>

En esta versión de Unity Application Block no hay soporte para configurar el contenedor usando la herramienta de configuración de Enterprise Library, por lo tanto debe trabajar con el archivo XML directamente.

Los contenedores no cargan automáticamente la configuración desde el archivo de configuración, debe hacerlo manualmente. En el siguiente ejemplo se crea una instancia del contenedor, luego se obtiene la configuración usando el método GetSection de la clase ConfigurationManager, y se aplica la configuración con nombre containerOne al contenedor.

IUnityContainer container = new UnityContainer();
UnityConfigurationSection section
  = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
section.Containers["containerOne"].Configure(container);
Definir las Dependencias

Puede definir las dependencias marcando los miembros de la clase mediante atributos o expresarlas en el archivo de configuración. Nos centraremos en el uso de atributos que es lo mas común dejando para otro articulo el uso del archivo de configuración para la definición de dependencias.

La inyección de constructores es automática, no es necesario usar atributos. En el ejemplo la clase ClienteService requiere el servicio IProductoService. El contenedor inyectara una clase concreta de la interface IProductService usando el constructor. Si hay mas de un constructor, el contenedor usara el constructor con el mayor numero de parámetros. Es posible usar el atributo InjectionConstructor para especificar el constructor que el contenedor desea utilizar.

    public class ClienteService
    {
        private IProductoService productos;

        public ClienteService(IProductoService dep)
        {
            productos = dep;
        }
    } 

La inyección de propiedades no es automática, las propiedades deben ser marcadas con el atributo Dependency. En el siguiente ejemplo la clase ClienteService tiene una referencia a IProductService. La propiedad se marca con el atributo Dependency, esto hará que el contenedor inyecte una clase concreta que implemente la interface IProductService.

    public class ClienteService
    {
        private IProductoService productos;

        [Dependency]
        public IProductoService Productos
        {
            get { return productos; }
            set { productos = value; }
        }
    } 

La inyección de métodos también requiere atributos. En el ejemplo la misma clase ClientService ahora es configurada para recibir la instancia con inyección de método. El método Initialize es marcado con el atributo InjectionMethod lo que hará que el contenedor inyecte una clase concreta que implementa la interfaz IproductoService.

    public class ClienteService
    {
        private IProductoService productos;

        [InjectionMethod]
        public void Initialize(IProductoService dep)
        {
            productos = dep;
        }
    } 

Si necesita inyectar una instancia con nombre debe pasar le nombre de la instancia como parámetro del atributo Dependency. En el caso de inyección de constructores y métodos se debe marcar el parámetro usando el atributo Dependency incluyendo el nombre.

El contenedor también puede inyectar instancias de clases concretas que no están mapeadas. Por ejemplo, si marca una propiedad con el atributo Dependency y el tipo de dato de la propiedad es una clase concreta, el constructor intentara crearla e inyectara todas las dependencias que tenga esta clase. Lo mismo en constructores y métodos, el contenedor es capaz de crear los parámetros que sean clases concretas no mapeadas.

Crear los Objetos Usando el Contenedor

Una vez que el contenedor esta configurado con los mapeos e instancias, y que las clases están marcadas con los atributos para indicar las dependencias que requieren, estamos listos para funcionar. Utilizamos el método Resolve para crear un objeto mediante el contenedor. Al crear el objeto se explorara para encontrar si tiene dependencias, en tal caso el contenedor inyectara las dependencias según fue configurado. En el siguiente ejemplo se usa le método Resolve para crear una instancia de la clase concreta mapeada a la interfaz IMyService y se inyectaran todas sus dependencias.

IMyService result = myContainer.Resolve<IMyService>();

Puede usar el contenedor para crear una clase concreta de que no este mapeada para que inyecte las dependencias. En el siguiente ejemplo se usa el método Resolve para crear una instancia de un formulario Windows Forms llamado FormOrdenes y el constructor inyecta las dependencias que encuentra en el formulario.

FormOrdenes result = myContainer.Resolve<FormOrdenes>();
Contenedores Jerárquicos

Puede formar una jerarquía de contenedores usando el método CreateChildContainer(). En el siguiente ejemplo se crea una instancia del contenedor myContainer y luego se usa el método CreateChildContainer para crear la jerarquía de contenedores.

IUnityContainer myContainer = new UnityContainer();
IUnityContainer subContainer = myContainer.CreateChildContainer();
IUnityContainer subSubContainer= subContainer.CreateChildContainer();
Intercepción

Unity Application Block incluye una mecanismo de intercepción incluso mas flexible que el de Policy Injection Application Block. Lo mas lógico que Unity reemplace por completo a Policy Injection en las futuras versiones de Enterprise Library.
Esto tiene mucho sentido pues si se usa Policy Injection para intercepción y Unity para dependencia, tendrá que utilizar dos fabricas para crear los objetos, es mas si un objeto requiere intercepción e inyección de dependencias entonces crearlo con una fabrica y pasarlo por la otra para que quede totalmente ensamblado. Definitivamente es deseable que intercepción e inyeccion operen a través de un canal común.
Lamentablemente en esta versión de Unity Application Block la documentación de como configurar el contenedor para intercepción es muy escasa. Tampoco hay soporte para la herramienta de configuración de Enterprise Library. Sin embargo, existe una extensión para Unity que permite configurar leer la configuración de Policy Injection Application Block para configurar el contenedor. Por lo tanto quizás la mejor forma de trabajar sea utilizar las herramientas de configuración de Policy Injection y cargar esta configuración en el contenedor. De esta forma el contenedor sera capaz de inyectar e interceptar en un solo paso.
La infraestructura de handlers y matching rules de Unity es la misma que en Policy Injection Application Block así que lea este articulo para interiorizarse del modo de uso.
Próximamente: Como configurar Unity con configuración de Policy Injection.

Extensiónes

Unity Application Block es flexible, presenta múltiples puntos de extensión.

  • Puede crear nuevos clases LifeTimeMangers que implementen otras maneras de manejar la vida de los objetos que usa el contenedor.
  • Puede crear extensiones de contenedor que extienden el comportamiento del contenedor por ejemplo definiendo nuevas y particulares formas de crear objetos e inicializar objetos. Puede definir nuevos atributos que el contenedor identificara para realizan tareas especificas. Es necesario un conocimiento del componente ObjectBuilder para obtener todo el poder de las extensiones de contenedor.
  • Crear nuevas reglas de coincidencia (matching rules) para definir que miembros reciben funcionalidad adicional usando mecanismo de intercepción.
  • Crear nuevos handlers que implementa funcionalidad adicional que se puede agregar a métodos y propiedades usando el mecanismo de intercepción.
Vea tambien
Links