DDD: 2- Framework de IoC (parte 2)

En el artículo anterior implementamos todas las interfaces necesarias para usar algunos elementos de configuración a nivel de aplicación. Entre estos elementos estaba el Framework de IoC.

En ese mismo artículo explicábamos por qué decidimos inyectar al Framework de dependecia, así que hoy nos dedicaremos a implementar todas las interfaces y realizar algunos test.

Antes de empezar, deciros que he realizado una pequeña modificación a la interfaz IContainerConfiguration que vimos en el artículo anterior. Soy de los que cree que cuando algún código (mio o no) queda digno de “admirar” (lo cual no quiere decir “correcto” Smile with tongue out), me quedo rato mirándolo aún días o semanas después de haberlo implementado. Esto, además de parecer que pierdo el tiempo, me  ayuda a ver posibles refactoring que en su momento no vi.

En uno de esos momentos de “bobería” me di cuenta que la interfaz IContainerConfiguration no necesita saber el QulifiedName para nada y que con el tipo era suficiente. Es verdad que de alguna forma debo recuperar el tipo, pero de eso que se encargue quien implemente la interfaz.

IContainerConfiguration ahora quedaría así:

public interface IContainerConfiguration
{
    Type IocObjectType { get; }
}

Después de hecho el cambio, entramos en materia.

Pensando con el corazón y no en el performance, voy a utilizar como Framework de inyección de dependencia el Unity de Microsoft, pero recuerden, de la forma que lo hemos implementado podríamos usar cualquiera siempre que implementemos la interfaz IContainer.

Para temas de configuración, usaré el archivo de configuración de la aplicación. Al igual que el framework de IoC, podríamos obtener la configuración de cualquier otro lado tan solo implementando las distintas interfaces.

Empezamos con la implementación del UnityContainer, nuestro único requerimiento es implementar la interfaz IContainer.

public sealed class UnityContainer : IContainer, IDisposable
{
    private readonly Microsoft.Practices.Unity.UnityContainer _container;

    public UnityContainer()
    {
        _container = new Microsoft.Practices.Unity.UnityContainer();
    }

    #region Miembros de IContainer

    public void InitializeContainer()
    {
        var section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
        section.Configure(_container);
    }

    /// <summary>
    /// Retorna una nueva instancia del tipo T usando IoC
    /// </summary>
    /// <typeparam name="T">Tipo del nuevo objeto a instanciar</typeparam>
    /// <returns>Instancia del tipo generico T</returns>
    public T GetInstanceOf<T>()
    {
        return _container.Resolve<T>();
    }

    /// <summary>
    /// Retorna una nueva instancia del tipo T usando IoC
    /// </summary>
    /// <typeparam name="T">Tipo del nuevo objeto a instanciar</typeparam>
    /// <typeparam name="TParamType">
    /// Un parámetro de tipo TParamType que se le pasa al construnctor de T
    /// </typeparam>
    /// <returns>Instancia del tipo generico T</returns>
    public T GetInstanceOf<T, TParamType>(TParamType paramInjection)
    {
        _container.RegisterInstance(typeof(TParamType), paramInjection);
        return _container.Resolve<T>();
    }

    #endregion

    #region Miembros de IDisposable

    /// <summary>
    /// El Objeto UnityContainer del EnterpriseLibrary implementa IDisposable.
    /// Al construir una instancia de este objeto dentro de nuestra clase, necesitamos
    /// implementar dicha interfaz.
    /// </summary>
    public void Dispose()
    {
        _container.Dispose();
    }

    #endregion
}

La idea no es entrar a analizar el UnityContainer de Microsoft, sino me gusta, uso otro y nada cambia en toda mi aplicación. De cualquier forma, por detallar un poco la implementación de la interfaz, vemos la inicialización de Unity en el método que provee la interfaz (InitializeContainer), luego usamos el Container para satisfacer los dos métodos de la interfaz que retornan una instancia de un objeto basado en su interfaz (con o sin parámetro) y todo listo.

¿Hacemos entonces unos Test a ver qué tal? Ah no, que no puedo, primero tengo que implementar todo el tema de configuración. Pues lo que adoro de esta arquitectura es precisamente esto, sí, tengo que implementar el tema de configuración, pero como la misma sale de una interfaz, puedo hacerlo sin tener que llegar a implementar todo lo que se refiere al archivo de configuración. Smile

class UnityConfiguration : IContainerConfiguration
{
    #region Implementation of IContainerConfiguration

    public Type IocObjectType
    {
        get { return typeof(Project.IoC.EnterpriseLibrary.UnityContainer); }
    }

    #endregion
}

Aquí implemento la interfaz de configuración para el IoC y retorno directamente el tipo que implementa el IContainer. (Nota: El tipo UnityContainer que retorno aquí no es el de Microsoft, es el mío que implementa la interfaz IContainer).

Voy a crearme unos objetos bien simples para realizar los test

public interface IA
{
    int ObjectID { get; }
}

public interface IB
{
    IA ObjectInjection { get; }
}

public class A : IA
{
    public A()
    {
        ObjectID = 1;
    }

    #region Implementation of IA

    public int ObjectID { get; private set; }

    #endregion
}

public class B : IB
{
    public B(IA paramInjection)
    {
        ObjectInjection = paramInjection;
    }

    #region Implementation of IB

    public IA ObjectInjection { get; private set; }

    #endregion
}

 

Configuramos el Unity de Microsft para crear los Alias y los mapeos y vamos por los test.

<configSections>
  <section name=»unity»
            type=»Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,
            Microsoft.Practices.Unity.Configuration» />
</configSections>

<unity>
  <typeAliases>
    <typeAlias alias=»IA» type=»Solution.Test.Infrastructure.IoC.IA, Solution.Test» />
    <typeAlias alias=»IB» type=»Solution.Test.Infrastructure.IoC.IB, Solution.Test» />
    <typeAlias alias=»A» type=»Solution.Test.Infrastructure.IoC.A, Solution.Test» />
    <typeAlias alias=»B» type=»Solution.Test.Infrastructure.IoC.B, Solution.Test» />
  </typeAliases>
  <containers>
    <container>
      <register type=»IA» mapTo=»A» />
      <register type=»IB» mapTo=»B» />
    </container>
  </containers>
</unity>

Los test:

[TestClass]
public class IocTest
{
    private readonly IContainer _ioc;

    public IocTest()
    {
        _ioc = new IocFactory().Create(new UnityConfiguration());
    }

    [TestMethod]
    public void TestCreateObjetWithoutConstructorParameters()
    {
        var a = _ioc.GetInstanceOf<IA>();

        Assert.IsNotNull(a);
        Assert.AreEqual(1, a.ObjectID);
    }

    [TestMethod]
    public void TestCreateObjetWithConstructorParameters()
    {
        var a = new A();
        var b = _ioc.GetInstanceOf<IB, IA>(a);

        Assert.IsNotNull(b);
        Assert.AreEqual(1, b.ObjectInjection.ObjectID);
    }
}

Flirt male Me encanta este momento…

test-ioc

Bien, pasemos del sentimentalismo y la adoración del color verde del Success Open-mouthed smile y vayamos a la configuración. La implementación de la configuración del framework de IoC desde el archivo de configuración quedaría de la siguiente forma:

public class ContainerConfigurationElement : ConfigurationElement, IContainerConfiguration
{
    [ConfigurationProperty("unityType", IsRequired = true)]
    public string IocQualifiedName
    {
        get { return (string)this["unityType"]; }
        set { this["unityType"] = value; }
    }

    public Type IocObjectType
    {
        get { return Type.GetType(IocQualifiedName); }
    }
}

Recuerden que ya la propiedad que nos da el QualifiedName no pertenece a la interfaz, sin embargo sí que la implementamos cuando queremos obtener quién será nuestro  Framework de IoC desde la configuración.

Hagamos lo mismo para los Settings de mi aplicación:

public class AppConfigurationElement : ConfigurationElement, IAppConfigurationElement
{
    #region Implementation of IAppConfigurationElement

    [ConfigurationProperty("name", IsRequired = true)]
    public string Name
    {
        get
        {
            return (string)this["name"];
        }
        set
        {
            this["name"] = value;
        }
    }

    [ConfigurationProperty("datetimeFormat", IsRequired = false, DefaultValue = null)]
    public string DateTimeFormat
    {
        get
        {
            var dtFormat = (string)this["datetimeFormat"] ??
                  new CultureInfo(LanguageDefault, false).DateTimeFormat.ToString();

            return dtFormat;
        }
        set
        {
            this["datetimeFormat"] = value;
        }
    }

    [ConfigurationProperty("languageDefault", IsRequired = true)]
    public string LanguageDefault
    {
        get
        {
            return (string)this["languageDefault"];
        }
        set
        {
            this["languageDefault"] = value;
        }
    }

    [ConfigurationProperty("timeZoneOffset", IsRequired = false, DefaultValue = 0D)]
    public double TimeZoneOffset
    {
        get
        {
            return (double)this["timeZoneOffset"];
        }
        set
        {
            this["timeZoneOffset"] = value;
        }
    }

    #endregion
}

Ahora ya podemos  implementar la interfaz que agrupa los elementos de configuración.

public class AppConfigurationSection : ConfigurationSection, IAppConfigurationSection
{
    [ConfigurationProperty("Setting")]
    private AppConfigurationElement ConfigSettings
    {
        get
        {
            return (AppConfigurationElement)this["Setting"];
        }
        set
        {
            this["Setting"] = value;
        }
    }

    [ConfigurationProperty("IocConfiguration")] 
    private ContainerConfigurationElement ConfigIocConfiguration
    {
        get { return (ContainerConfigurationElement)this["IocConfiguration"]; }
        set { this["IocConfiguration"] = value; }
    }

    #region Implementation of IAppConfigurationSection

    public IContainerConfiguration IocConfiguration
    {
        get { return ConfigIocConfiguration; }
        set { ConfigIocConfiguration = (ContainerConfigurationElement) value; }
    }

    public IAppConfigurationElement Settings
    {
        get { return ConfigSettings; }
        set { ConfigSettings = (AppConfigurationElement) value; }
    }

    #endregion
}

Vean que hemos tenido que definir propiedades que retornen un ConfigurationElement. Esto es requerido para poder leer las configuraciones del archivo de configuración, pero yo en ningún momento deseo que esos tipos sean conocidos por mi aplicación (recuerden que trabajamos ajenos a la tecnología que usemos). Es por eso que esta clase crea las propiedades concretas para la configuración como privadas y, las que usará mi aplicación (públicas) retornan la interfaz del tipo creado.

Por último, implementamos IGlobalSettings. Aquí me implemento un Singleton a nivel de aplicación para acceder a mi configuración. Dos razones, tengo un único punto de acceso (por eso el síngleton) y los datos que guarda mi configuración no cambian durante todo el ciclo de vida de la aplicación (por eso digo que es a nivel de aplicación)

public sealed class Global : IGlobalSettings
{
    /// <summary>
    /// Singleton. Thread safe.
    /// </summary>
    private static readonly Lazy<Global> _instance = new Lazy<Global>(() => new Global());

    /// <summary>
    /// IocFactory.
    /// </summary>
    private readonly IContainer _ioc;

    private Global()
    {
        var cfg = (IAppConfigurationSection)ConfigurationManager.GetSection("Project");

        _ioc = new IocFactory().Create(cfg.IocConfiguration);
        Settings = cfg.Settings;
    }

    public static Global Application
    {
        get { return _instance.Value; }
    }

    #region IAppConfigurationSection Members

    public IContainer Ioc { get { return _ioc; } }
    public IAppConfigurationElement Settings { get; private set; }

    #endregion
}

Ya podemos ir a por los Test de configuración. El IoC ya lo probamos, por lo que con saber que el tipo retornado por la configuración es correcto (implementa IContainer), tengo suficiente

Antes de escribir los test, creamos la configuración en el archivo de configuración de la aplicación.

<configSections>
  <section name=»Project»
        type=»Project.Settings.Config.AppConfigurationSection, Project.Settings.Config»
        requirePermission=»false» />
</configSections>

<Project>

  <Setting name=»My Project DDD»
                languageDefault=»es-ES»
                datetimeFormat=»dd-MM-yyyy HH:mm»
                timeZoneOffset=»1″  />

  <IocConfiguration unityType=»Project.IoC.EnterpriseLibrary.UnityContainer,
                                                                        Project.IoC.EnterpriseLibrary» />

</Project>

y ahora sí…

[TestClass]
public class ConfigTest

    [TestMethod]
    public void TestConfiguration()
    {
        var settings = Global.Application.Settings;

        Assert.AreEqual("My Project DDD", settings.Name);
        Assert.AreEqual("es-ES", settings.LanguageDefault);
        Assert.AreEqual("dd-MM-yyyy HH:mm", settings.DateTimeFormat);
        Assert.AreEqual(1, settings.TimeZoneOffset);
    }

    [TestMethod]
    public void TestIocConfiguration()
    {
        var ioc = Global.Application.Ioc;
        Assert.IsInstanceOfType(ioc, typeof(Project.IoC.EnterpriseLibrary.UnityContainer));
    }
}

Result:

test-config

y Chirrin-chirran…. Smile

5 comentarios sobre “DDD: 2- Framework de IoC (parte 2)”

  1. Hola,

    Estoy siguiendo la serie con curiosidad y me entra una duda.

    ¿Por qué haces la configuración singleton en lugar de dejar que Unity gestione su ciclo de vida?

  2. Hola Juanma…

    Ten en cuenta que el singleton no es para los objetos que creamos con Unity.

    El singleton lo utlizamos para mantener en todo momento una referencia a las distintas configuraciones de nuestra aplicación, entre las cuales está el framework de persitencia.

    Desde nuestro Singleton (Global) tenemos acceso al idioma por defecto que tendremos, al formato de fecha y hora, y a todo lo que creas que no cambiará durante el ciclo de vida de tu aplicación. Uno de esos elementos que no cambiaría sería el Unity.

    No se si con esto entiendas a lo que me refiero, de no ser así, pregunta sin problemas…

    🙂

    Un salu2, y gracias por segir la serie, después de esto empezaremos con la unidad de trabajo (UnitOfWork) y los repositorios…

  3. Mmmm…
    A la espera de ver más…
    No termino de ver claro el porque encapsular tu contenedor de IoC en otra clase, ni rl porque de mantenerlo «accesible» a través de la clase Global.
    No veo ventaja en abstraerte del contenedor de IoC que uses, si aplicas DI:
    1) Tan sólo referencias el controlador de IoC en un solo sitio (el bootstrapper). Por lo que cualquier cambio (de Unity a Windsor p.ej.) supondrá un solo cambio (el bootstrapper). Así pues dado que hay un solo cambio no hay ventaja alguna en abstraerte.
    2) Si necesitas acceder explicitamente al contenedor, entiendo que es porque realizas un .Resolve(). Entonces la cuestión es: es service locator un patrón o un antipatrón (http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx)??

    Por supuesto ya digo, eso son pensamientos a la espera de sucesivos posts!
    Saludos!

  4. Estoy totalmente de acuerdo con Eduard, por propia experiencia la abstracción de este concepto no aporta absolutamente nada.
    Si tuvieras, algún dia, que cambiar tu framework, sería sencillo teniendo claro cual es tu «Composition Root» o bottstrapper o «last responsible moment» o X, como quieras llamarle.
    En cuanto al resolve, tampoco se debería ver como también te hace ver Eduard, con una muy buena referencia hacia el blog de Mark Seemann

    Unai

  5. @Eduard, @Unai…

    Gracias por la referencia al blog de Mark  y por los comentarios..

    Pues el objetivo de todo esto era precisamente lo que Eduard plantea en (2)… :'(

    ummm.. preparando Service Pack 1… 🙂

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *