Mostrando las entradas con la etiqueta arquitectura de sistemas. Mostrar todas las entradas
Mostrando las entradas con la etiqueta arquitectura de sistemas. Mostrar todas las entradas

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)

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.