Castle.Windsor Fluent Registration API

Ya vimos en un post anterior como podemos configurar el container de Castle con Fluent Registration API al igual que con xml. Ahora vamos a ver algunas cosas que podemos hacer con Fluent donde, quizás, empiece a sacar ventaja. Espero que el siguiente código sea suficientemente claro.

Creamos nuestro container

IWindsorContainer container = new WindsorContainer();
Registramos todos los DAOs
container.Register(
AllTypes.Of<IDAO>()
.From(typeof(InvoiceDAO).Assembly.GetTypes())
.WithService.FromInterface()
.Configure(cr => cr.LifeStyle.Singleton));

“registramos en el container” (línea 1), “todos los types que implementan IDAO” (línea 2), “del assembly al que pertenece InvoiceDAO (en este caso DMS.DAOs.NH” (línea 3), “como servicio tomamos la interface que implementan” (línea 4) y “los configuramos como singleton” (línea 5).

Registramos todos los models
container.Register(
AllTypes.Of<IModel>()
.From(typeof (InvoiceViewModel).Assembly.GetTypes())
.WithService.FromInterface());

creo que ya no hace falta “leerles” lo que dice el código. La diferencia con la registración de los DAOs es que en este caso registramos todas las implementaciones de IModel en el assembly DMS.Models.Impl.

Registramos todos los services
container.Register(
AllTypes.Of<IService>()
.From(typeof(InvoiceService).Assembly.GetTypes())
.WithService.FromInterface()
.Configure(cr => cr.Interceptors<SessionInterceptor>()));
Igual que antes, salvo por que les configuramos el interceptor (SessionInterceptor).

Conclusión

Por si no quedó claro, la ventaja que intento mostrarles es que no necesitamos declarar cada componente (existente o que existirá), sino que con esta configuración y si seguimos implementando las interfaces correspondientes (IDAO, IModel e IService) automáticamente se agregarán al container y se inyectaran las dependencias.

Data Transfer Object (DTOs)

 

¿Qué son los DTOs?

Son entidades serializables que se suelen utilizar en la transferencia de información entre capas físicas de la aplicación. También son útiles en situaciones donde queremos desvincular capas aunque no exista una división física real. Los DTOs representan la información de las BEs a partir de cierto nivel (o componentes) de nuestra aplicación.

Usando BEs y DTOsScope de las BEs y los DTOs

En este caso, vamos a usar BEs para la parte mas interna de nuestra aplicación mientras que DTOs para la parte mas externa. Ejemplos de esto podrían ser:

  • A partir de los servicios del tipo WS y WCF pues debemos usar entidades serializables (y nuestro dominio muy posiblemente no lo sea).
  • Para la capa de presentación en aplicaciones web con un dominio de cierta complejidad y con uso exhaustivo de ajax.

En el gráfico de la derecha, el límite entre BEs y DTOs es Services, mientras que los Mappers son quienes van a hacer esta conversión por lo que operan en ambos “mundos”.

Usando simplemente las BEs

Scope de las BEs sin DTOs

También podemos optar por no usar DTOs sino que simplemente usamos las BEs en toda nuestra aplicación, ejemplos de estos casos pueden ser:

  • En aplicaciones de escritorio (tipo winforms o de consola) es mas común encontrar este tipo de gestión de objetos ya que, al estar ejecutándose todo en el mismo espacio de memoria, es mas simple el manejo de los aspectos relacionados al lazy loading y no es necesaria la serialización.
  • En aplicaciones web con dominios relativamente simples, con un buen manejo de los lazies loading o sin ajax que interaccione con BEs que se cargan en distintos request.

En estos casos puede ser mejor usar las BEs en nuestra capa de presentación para no agregar mas types y evitarnos la conversión entre BEs y DTOs, así como no cargar algunos lazy loading innecesariamente (hecho que puede ocurrir con la serialización).

Y si usamos las BEs en toda nuestra aplicación ¿que debemos tener en cuenta?
  • Que los ciclos de vida de los objetos asociados al de las BEs, por ejemplo el ciclo de vida de la session de nh por el lazy loading (ojo con las aplicaciones que usen ajax y el manejo de la session de nh por request).
  • De no terminar serializando mucha mas información de la necesaria en los casos de servicios que exponen BEs (imaginen en el ejemplo serializar los 4 objetos del diagrama cuando solo eran necesarios los datos del DTO).
  • Que las propiedades de tipo IEnumerable<T> no se van a serializar automáticamente, tampoco las ICollection<T> ni las IList<T>, etc.

Siguiendo el camino de los DTOs

Si elegimos ir por la separación en DTOs y BEs, seguramente queramos contar con algún experto (o algunos) para convertir unos en otros. Algunos de estos pueden ser AutoMapper o EmitMapper.

AutoMapper

AutoMapper es un componente que nos ayuda a mapear BEs a DTOs, ¿que quiere decir esto?, que si respetamos ciertas convenciones de nombres podemos transferir la información desde BEs en DTOs con unas pocas líneas de código.

Nuestras BEs (ver dominio completo)

Nuestro DTO

InvoiceDiagram InvoiceDTODiagram

Si miramos nuestro DTO y nuestras BEs, podemos inferir que en las propiedades:

  • BillingCity, BillingCountry, BillingState, InvoiceDate, y Id queremos los datos correspondientes a sus equivalentes BEs.Invoice
  • CustomerFirstName y CustomerLastName son los datos correspondientes a Customer.FirstName y Customer.LastName respectivamente.
  • Total es el resultado del método GetTotal() de Invoice.

Si respetamos estas convenciones de nombres (donde, básicamente, quitamos los “.” y respetamos los nombres), podemos pedirle a AutoMapper que nos pase la información desde nuestras BEs a nuestro DTO con solo 2 líneas de código:

Mapper.CreateMap<Invoice, InvoiceDTO>();
InvoiceDTO dto = Mapper.Map<Invoice, InvoiceDTO>(invoice);

En la primer línea estamos configurando AutoMapper para que relacione las propiedades de estas clases. Esta línea es necesaria solo por única vez primera vez.

En la segunda le estamos pidiendo que nos de un nuevo DTO a partir del objeto invoice.

¿Cómo decidimos si usamos AutoMapper, EmitMapper u otro?

No lo decidimos, como ya vimos en un post anterior, usamos un bajo acoplamiento entre quienes consuman este mapeador y la implementación real. Lo que nos permitirá cambiar la implementación cuando lo consideremos necesario, sin necesidad de cambiar mas que la configuración de IoC Container en el resto de nuestra aplicación.

En este caso definimos una interface que van a cumplir nuestras implementaciones:

IMapperBEtoDTO

Luego hacemos las implementaciones, la de automapper sería algo así:

public class MapperBEtoDTO : IMapperBEtoDTO
{
    public MapperBEtoDTO()
    {
        Mapper.CreateMap<Invoice, InvoiceDTO>();
    }

    public TD Map<TB, TD>(TB be) where TD : IDTO
    {
        return Mapper.Map<TB, TD>(be);
    }
}

Y, como dijimos en el post anterior, en toda nuestras aplicación hacemos referencia a IMapperBEtoDTO que está en un proyecto diferente a las implementaciones y mediante IoC inyectamos la clase concreta que queremos utilizar.

Para mas detalles, pueden ver en el código de ejemplo los proyectos

  • DMS.Mappers (donde está la interface)
  • DMS.Mappers.AutoMapper (donde está la implementación)
  • DMS.Services.Impl (donde se usa la interface)
  • DMS.Integration.Testing (donde se inyecta al servicio, con castle, dicha implementación)

Persistencia del dominio de nuestro ejemplo

Nuestro dominio de objetos

Domain

Nuestro modelo relacional (tomado de Chinook 1.1)

modelo relacional

¿Cómo lo persistimos?

¿Cómo guardamos y recuperamos nuestro dominio de objetos de la una base de datos?... como se imaginarán no estoy pensando en escribir los insert, update, delete o select necesarios sino que, siguiendo en tren de delegar responsabilidades a expertos, usamos un ORM para la persistencia de nuestras entidades, nuestra primera implementación va a ser con nhibernate (nh). Esto quiere decir que vamos a hacer una implementación de DAOs con nh que se va a llamar DAOs.NH.

¿Cómo hacemos para no quedar “atados” a NHibernate?

Sólo vamos a usar (referenciar) nhibernate desde las implementaciones correspondientes (al momento de este post DAOs.NH, DAOs.NH.Castle y BEs.Validations.NHV). Es decir que no vamos a tener referencias a Nhibernate desde otras implementaciones como Models.Impl o Services.Impl. Ni decir que el Core de nuestra aplicación no va a conocerlo (esto es, los contratos, las BEs y los DTOs).

Para ver como usar NHibernate de la forma tradicional (hasta el momento) pueden ver la documentación oficial o los muchos ejemplos que hay en internet.

Configurando NH con ConfOrm

En este post vamos a ver la configuración de NHibernate mediante ConfOrm para nuestro dominio, y para esto vamos a hacer algunos ajustes a la base de datos (db). Es verdad que también podríamos indicar en los mappings (o a ConfOrm) las diferencias entre el modelo de objetos y el modelo relacional, pero como considero que la base de datos también es parte de nuestra aplicación y por lo tanto es factible de refactorización es que decido modificarla. Los scripts para esto están en la carpeta scripts de nuestro código de ejemplo. Dichos cambios consisten en renombrar algunos campos y una tabla, no modificamos en concepto el diseño de la db.

Ahora bien, vamos al código:

ObjectRelationalMapper orm = new ObjectRelationalMapper();

orm.TablePerClass<Album>();
orm.TablePerClass<Artist>();
orm.TablePerConcreteClass<Person>();
orm.TablePerClass<Genre>();
orm.TablePerClass<Invoice>();
orm.TablePerClass<InvoiceLine>();
orm.TablePerClass<MediaType>();
orm.TablePerClass<Playlist>();
orm.TablePerClass<Track>();

orm.Bag<Playlist>(pl => pl.Tracks);
orm.ManyToMany<Playlist, Track>();

Mapper mapper = new Mapper(orm);

Type type = typeof(Track);
HbmMapping mapping = mapper.CompileMappingFor(type.Assembly.GetTypes());

Configuration configuration = new Configuration();
configuration.Configure();

configuration.AddDeserializedMapping(mapping, "DMS_BEs");

return configuration;

Como podemos ver, el objetivo de esto es llegar al objeto Configuration de nh, el cual antes configurábamos a partir de los .hbm.xml embebidos en el assembly o con attributes sobre nuestro dominio. Para ver las distinas opciones de configuración de ConfOrm pueden revisar el blog de Fabio que seguro encontrarán mucho.

¿y como quedaron estos mappings?

No es necesario escribir los mappings en un archivo, pero si queremos verlos podemos:

<?xml version="1.0" encoding="utf-8"?>
<hibernate-mapping xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" namespace="DMS.BEs" assembly="DMS.BEs" xmlns="urn:nhibernate-mapping-2.2">
    <class name="Invoice">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <many-to-one name="Customer" />
        <property name="InvoiceDate" />
        <property name="BillingAddress" />
        <property name="BillingCity" />
        <property name="BillingState" />
        <property name="BillingCountry" />
        <property name="BillingPostalCode" />
        <bag name="Lines" inverse="true" cascade="all,delete-orphan">
            <key column="Invoice" on-delete="cascade" />
            <one-to-many class="InvoiceLine" />
        </bag>
    </class>
    <class name="Track">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <property name="Name" />
        <many-to-one name="Album" />
        <many-to-one name="MediaType" />
        <many-to-one name="Genre" />
        <property name="Composer" />
        <property name="Milliseconds" />
        <property name="Bytes" />
        <property name="UnitPrice" />
    </class>
    <class name="Artist">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <property name="Name" />
    </class>
    <class name="Genre">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <property name="Name" />
    </class>
    <class name="MediaType">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <property name="Name" />
    </class>
    <class name="Playlist">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <property name="Name" />
        <bag name="Tracks">
            <key column="playlist_key" />
            <many-to-many class="Track" />
        </bag>
    </class>
    <class name="InvoiceLine">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <many-to-one name="Invoice" />
        <many-to-one name="Track" />
        <property name="UnitPrice" />
        <property name="Quantity" />
    </class>
    <class name="Album">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <many-to-one name="Artist" />
        <property name="Title" />
        <bag name="Tracks" inverse="true" cascade="all,delete-orphan">
            <key column="Album" on-delete="cascade" />
            <one-to-many class="Track" />
        </bag>
    </class>
    <class name="Person" abstract="true">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <property name="FirstName" />
        <property name="LastName" />
        <property name="Address" />
        <property name="City" />
        <property name="State" />
        <property name="Country" />
        <property name="PostalCode" />
        <property name="Phone" />
        <property name="Fax" />
        <property name="Email" />
    </class>
    <union-subclass name="Employee" extends="Person">
        <property name="Title" />
        <property name="BirthDate" />
        <property name="HireDate" />
        <many-to-one name="ReportsTo" />
    </union-subclass>
    <union-subclass name="Customer" extends="Person">
        <property name="Company" />
        <many-to-one name="SupportRepresentant" />
    </union-subclass>
</hibernate-mapping>

Conectando las piezas

Como dijimos en un post anterior, para lograr un bajo acoplamiento hacemos referencia a contratos (interfaces) y no a implementaciones (clases), pero cuando queremos ejecutar nuestra aplicación (o parte de ella) necesitamos hacer uso de las implementaciones. Y qué mejor que delegar el control de creación de objetos en algún framework especializado para tal fin. ver Inversion of Control (IoC) y Dependency Injection (DI)

En nuestro ejemplo usamos Castle Windsor, pero podría ser Sprint.NET, Unity, LinFu, etc. y hasta sería interesante abstraernos de quien sea nuestro IoC / DI, y para esto contamos con Common Service Locator.


La idea de todo esto es poder solicitar un determinado objeto, por ejemplo un Model, y que ya tenga sus referencias a otros objetos inyectadas, como por ejemplo los DAOs que necesita.

¿Cómo hacemos este "cableado" con Castle?

 ver documentación completa

Para declarar un componente:

<component id="IInvoiceDAO"
           service="DMS.DAOs.IInvoiceDAO, DMS.DAOs"
           type="DMS.DAOs.NH.InvoiceDAO, DMS.DAOs.NH"
           lifestyle="singleton">
</component>

donde id es la identificación del componente tanto para referencias internas en el archivo de configuración como para solicitarlo desde nuestra aplicación, service es el contrato que debe cumplir y type es la implementación que deseamos usar.

y para interconectar componentes: 

        <component id="IInvoiceViewModel"
                       service="DMS.Models.IInvoiceViewModel, DMS.Models"
                       type="DMS.Models.Impl.InvoiceViewModel, DMS.Models.Impl">
            <
parameters>
                <
InvoiceDAO>${IInvoiceDAO}</InvoiceDAO>
            </
parameters>
        </
component>

donde parameters son la lista de parámetros del constructor de InvoiceViewModel o propiedades púbicas de este. En este caso estamos inyectando el componente definido como IInvoiceDAO.

Nota: si tenemos un único componente del tipo del parámetro, Castle lo va a inyectar sin necesidad de que se lo especifiquemos. es decir, que en este ejemplo no sería necesario indicar el parámetro.

Otra forma de configurar el container

También podemos configurar Castle Windsor mediante Fluent Registration API de la siguiente manera:

IWindsorContainer container = new WindsorContainer()
.AddFacility("factory.support", new FactorySupportFacility())
.Register(Component.For<MockHelper>()
.ImplementedBy<MockHelper>())
.Register(Component.For<IInvoiceDAO>()
.UsingFactoryMethod(kernel => kernel.Resolve<MockHelper>().CreateInvoiceDAO())
.LifeStyle.Singleton)
.Register(Component.For<IInvoiceViewModel>()
.ImplementedBy<InvoiceViewModel>()
.LifeStyle.Transient);

Como podemos ver, en esta configuración no estamos especificando los parámetros, como dijimos antes en Nota, no es necesario especificarlo en este caso.

Usando CommonServiceLocator

Finalmente, para abstraernos de la implementación del IoC container, podemos crear un ServiceLocator pasándole como parámetro el container ya configurado:

IServiceLocator serviceLocator = new WindsorServiceLocator(container);

y luego nos referiremos a este de la siguiente forma:

IInvoiceViewModel model = serviceLocator.GetInstance<IInvoiceViewModel>();

Descargar el código fuente completo

Arquitectura de bajo acoplamiento

Pensar una arquitectura de bajo acoplamiento
Para esto podemos dividir la aplicación en varios componentes, hacer referencia a los contratos (interfaces) y no a las implementaciones y asignarle sólo una responsabilidad a cada componente. Esto nos permitirá cambiar la implementación de dicho componente sin afectar los otros.

Por ejemplo DAOs tiene la responsabilidad del acceso a datos, no representa las entidades del dominio (que es responsabilidad de BEs), no tiene lógica de negocio (que es responsabilidad de Models), sólo se encarga de la persistencia de nuestro dominio (BEs). Quienes usan los DAOs, en realidad hacen referencia al contrato de DAOs, no a su implementación.

Arquitectura de nuestro ejemplo
Nuestro ejemplo se divide en tres grupos: Testing, Core e Implementations.
Grupo Testing:
Aquí están los test de nuestra aplicación, podríamos decir que estos tests son los que definen qué hace la aplicación en un modo verificable. Sólo podemos asegurar que funcione correctamente aquella característica que tenga su correspondiente test.
Los tests se dividen en tests unitarios (como Models.Testing) y tests de comportamiento o integración (como Integration.Testing).

Grupo Core:
- BEs: son las entidades que representan nuestro dominio.
- DTOs: son objetos serializables para la transferencia de información entre capas (físicas).
- DAOs: es el contrato que debe cumplir cualquier implementación de acceso a datos.
- Mappers: es el contrato que debe cumplir un conversor de BE a DTO y de DTO a BE.
- Models: es el contrato que debe cumplir cualquier implementación de lógica de negocios.
- Services: es el contrato que debe cumplir cualquier implementación de servicios de nuestra aplicación.

Como podemos ver, en el core de nuestra aplicación tenemos las definiciones de los componentes y no la lógica de los mismos (implementaciones). Aquí definimos los límites entre las piezas de nuestra aplicación y no cómo se va a resolver cada problemática en particular. Ésto nos permitirá cambiar las implementaciones cuando encontremos mejores alternativas.

Grupo Implementations:
Aquí encontramos las implementaciones de cada contrato, al momento de redactar este articulo hay una de cada uno. La implementación de Mappers está dividido en dos proyectos, uno implementa la transformación de DTO a BE (Mappers.AutoMapper) y otra de BE a DTO (Mappers.Reload).

Como podemos ver en el diagrama, las implementaciones sólo conocen los contratos de los otros componentes, esto es lo que nos va a garantizar que hay un bajo acoplamiento.

Arquitectura de sistemas

¿A qué nos referimos cuando hablamos de la arquitectura de una aplicación?. Nos referimos a los componentes en que la vamos a dividir y como se van a relacionar estos componentes. Hablamos también de las divisiones físicas y lógicas en que vamos a dividir la aplicación con el objetivo de cumplir con los requerimientos técnicos (no funcionales).

¿y de qué NO hablamos?. No hablamos de cómo, la aplicación, va a resolver el negocio en cuestión.

Entonces... ¿que esperamos de la arquitectura de nuestra aplicación?. Esperamos que resuelva las necesidades técnicas sin hacer más complejo el trabajo de los programadores que desarrollan el negocio. Que su implementación no sea costosa a nivel performance y que no perdamos las ventajas de cada entorno en aires de generalizar para todos los escenarios posibles (e imposibles). El equilibrio entre estos puntos, y otros que me estaré olvidando e iremos completando, es el desafío.

Podríamos pretender que la arquitectura nos permita evolucionar, que podamos refactorizar y tengamos la certeza que todo sigue funcionando. Que si encontramos otra forma mejor de resolver un requerimiento, no sea difícil el cambio. Y con esto ya se empiezan a filtrar conceptos de metodología que también iremos discutiendo.

El ejemplo que vamos a ir armando es sobre la base de datos Chinook 1.1, veremos la implementación de varios frameworks y por qué lo hacemos.

El código fuente ya empieza a estar disponible, aunque en constante refactorización.