domingo, 19 de abril de 2009

DDD - Parte 2: Los Componentes Fundamentales de un Modelo Guiado por el Dominio #2

En el post anterior estuvimos viendo los primeros Patrones de Dominio Fundamentales de DDD:
  • Layered Architecture
  • Associations
  • Entities
  • Value Objects
  • Services
  • Modules
En este post nos centraremos en los tres Patrones que nos faltaron:
  • Aggregates
  • Factories
  • Repositories
Los tres patrones que trataré en este post, se encargan de encapsular distintas partes del ciclo de vida de un objeto.

El Ciclo de Vida de un Objeto de Dominio

Cada objeto en el dominio tiene un ciclo de vida que puede ser más o menos complejo dependiendo de su responsabilidad y/o propósito. Hay muchos objetos que son simples, transitorios, que se crean con un constructor simple, se usan para algún cálculo y se abandonan a las piadosas manos del recolector de basura. Pero los objetos que más nos interesan en este post son aquellos cuya vida se prolonga más allá de la existencia del programa que las usa.


Estos objetos son Entidades o Value Objects, que son creados a través de un Factory, modificados en memoria y guardados, reconstruidos o archivados en un Repository.

IX. Patrón 6: Aggregates


Ejemplo típico: una Persona (un Entity) que tiene una Dirección (un Value Object). Ambos objetos están asociados mediante una composición. La Dirección pertenece a la Persona y no tiene razón de ser fuera de la misma. No se actualiza una Dirección, se actualiza una Persona, se actualiza la dirección de una Persona.

Una Agregación es un grupo de objetos asociados que deben tratarse como una unidad a la hora de manipular sus datos. En el caso del ejemplo, Persona y Dirección forman parte de una misma Agregación. Otro ejemplo típico es el de Auto, con Rueda y Motor; salvo que se trate de un dominio muy específico, los componentes del auto sólo se modificarán a través del Auto. Auto, Rueda y Motor formarán una única Agregación.


Cada Agregación cuenta con una Entidad Raíz (root) y una Frontera (boundary). La Frontera define qué está dentro de la Agregación y qué no. La Entidad Raíz es una Entidad contenida en la Agregación y será el único punto de entrada a la Agregación. Esto significa que:
  • La Entidad Raíz es responsable de chequear las reglas de consistencia (invariantes) entre los distintos objetos de la Agregación en la carga, modificación y eliminación de datos.
  • La Entidad Raíz tiene una identidad global. Las Entidades Internas tienen una identidad local. Las Entidades Internas sólo pueden ser distinguibles dentro de la Agregación.
  • Fuera del contexto de la Agregación, no puede existir ninguna referencia a un miembro que no sea la Entidad Raíz. Los demás miembros de la Asociación no existen más allá de la Frontera.
  • La Entidad Raíz puede manejar referencias a Entidades Internas de otras Agregaciones, pero estas referencias deben ser transitorias. La Entidad Raíz no puede conservar la referencia.
  • La Entidad Raíz puede manejar referencias a Value Objects de otras Agregaciones y no importa lo que suceda con ella, ya que, como vimos en el post anterior, los Value Objects son sólo valores y al no tener identidad tampoco tienen una relación fuerte con la Agregación.
  • Sólo la Entidad Raíz puede ser obtenida directamente a través de un query a la base de datos. Los demás objetos de la Agregación son obtenidos navegando a través de las Asociaciones.
  • Los objetos miembro de la Agregación pueden contener referencias a otras Entidades RaízAgregaciones.
  • Una operación de eliminación debe remover absolutamente toda la Agregación en una sola transacción. Con un lenguaje que provea un garbage collector, esto es fácil, ya que no existe ninguna referencia fuera de la Agregación salvo la de la Entidad Raíz. Por eso, al eliminar la Entidad Raíz, se eliminará la Agregación completa.
  • Cuando se hace commit sobre un cambio efectuado en cualquier objeto dentro de la Agregación, todas las reglas internas de consistencia (invariantes) de la Agregación deben ser satisfechas.

Las Agregaciones marcan el alcance de las reglas de consistencia que deben mantenerse en todas las etapas del ciclo de vida de los objetos. Los siguientes dos patrones, Factories y Repositories, operan sobre las Agregaciones, encapsulando la complejidad de las transiciones durante el ciclo de vida.

X. Patrón 7: Factories

El concepto de Factory es bien simple y quien conozca los patrones fundamentales de Erich Gama entenderá perfectamente. Los Factories sirven para encapsular la creación de objetos complejos. La creación de una Entidad, por ejemplo, podría ser tan compleja que involucre a muchas otras Entidades y Value Objects. Imaginemos que se trata de la Entidad Raíz de una Agregación. Es evidente que su creación implica la creación de muchos otros objetos y el cumplimiento de reglas de negocio y consistencia que deben hacerse cumplir. Para abstraer al cliente y al objeto en sí de este complejo mecanismo es que se provee un Factory que se encargue del trabajo sucio.


Eric Evans presenta este patrón clasificándolo como componente fundamental de un Model-Driven Design. El patrón nos dice que traslademos la responsabilidad de crear instancias de objetos complejos y Agregaciones a un objeto externo, que puede no tener responsabilidad en el modelo, pero que sigue siendo parte de la capa de dominio. Luego, proporcionemos una interfaz que encapsule todo el complejo montaje de relaciones, así el cliente puede referenciar a dicha interfaz y no a una instancia concreta.

Mediante los Factories podemos crear Agregaciones enteras al momento de instanciar la Entidad Raíz, haciendo cumplir todas sus invariantes.


Las implementaciones de Factory más populares son las de Erich Gama: Factory Method, Abstract Factory, Builder y Prototype. Los cuatro son Patrones Creacionales, según la clasificación de Design Patterns de GoF.

No es la idea explayarme demasiado en este post sobre los Factories (para eso está el libro de Evans). Evans da consejos de implementación, sugiere lugares donde deberían descansar los Factories, habla de la diferencia entre tener un mero constructor y un Factory (y cuando puede servirnos uno u otro), explica dónde podría ubicarse la lógica de consistencia, detalla las diferencias entre un Entity Factory y un Value Object Factory, y por último, concluye con el uso de Factories para la reconstitución de objetos de dominio.


Hasta la última sección siempre se sugiere que los Factories sirven para la creación de objetos (la primera transición en el ciclo de vida). Aquí se hace un fuerte énfasis en la utilización de Factories para la reconstitución de objetos desde una base de datos, una red o lo que sea. Un Factory usado para este propósito cuenta con dos principales diferencias respecto a los demás Factories de creación:
  • No asigna nuevas identidades a los Entities. Sólo reconstituye el objeto desde la persistencia, respetando su identidad ya definida.
  • Maneja distintas invariantes. Si el objeto ya existiera en algún lado del sistema, las reglas de integración exclusivas para la alta no tienen sentido en esta fase del ciclo de vida. Sin embargo, la reconstrucción puede ocasionar inconsistencias y ciertas invariantes pueden no cumplirse. El Factory debería contar con una estrategia de reparación, lo que puede complicar muchísimo más el código que en un simple Factory de creación.
Por ende, los Factories encapsulan las transiciones de creación y reconstitución del ciclo de vida del objeto. Otras transiciones cuya complejidad técnica pueden contaminar de forma no deseada la capa de dominio son las que van y vuelven desde el almacenamiento. Los encargados de encapsular estas transiciones son los Repositories.


XI. Patrón 8: Repositories


Para operar sobre un objeto, necesitamos obtener una referencia al mismo. Existen dos formas de obtener esta referencia:
  • A través de una operación de creación: un Factory, un constructor, un operador de creación nativo del lenguaje.
  • A través de una asociación entre objetos: navegando a través de la relación con otro objeto.
La segunda forma es muy poderosa y es uno de los conceptos fundamentales de la orientación a objetos. Pero para poder llegar al objeto que queremos navegando a través de la red de Asociaciones, necesitamos obtener el primer objeto de todos, el que para una Agregación sería la Entidad Raíz.

Si bien desde un punto de vista técnico, la reconstitución de un objeto es un caso concreto de la creación, desde el punto de vista del dominio, un cliente necesita un medio práctico para adquirir referencias a objetos que ya existen persistidos en una base de datos o en algún otro servidor de la red. El patrón Repository es un simple framework conceptual que encapsula el proceso de reconstitución y lo diferencia del proceso de creación encapsulado por el patrón Factory. Un Factory construye nuevos objetos; un Repository busca objetos viejos, ya creados.

Un Repository representa todos los objetos de una cierta clase como un conjunto conceptual (comúnmente emulado). Su interfaz es similar a la de una colección ordinaria, en la que uno puede agregar o remover objetos. Los Repositories encapsulan el mecanismo de reconstitución de los objetos. Reciben un simple request para obtener un objeto o una lista de objetos que satisfagan un cierto criterio y ellos se encargan de reconstruir los objetos de donde sea que haya que hacerlo, de una base de datos, de un archivo de texto, de memoria, de una red, de un web service, etc, etc.

Es importante entender que los Repositories son también conceptos del dominio. Es la forma que el modelo de dominio tiene de reconstituir Entidades y Value Objects y almacenarlos para que puedan sobrevivir más allá de la ejecución del programa. La interfaz del Repository vivirá en la capa de dominio; la implementación es posible que viva en otra capa (por ejemplo en la de Persistencia). Un Repository relegará las queries recibidas en un Data Mapper, en otro Repository o en algún otro objeto específico de infraestructura que se encargue de resolver la petición. Martin Fowler trata muy bien este tema desde el punto de vista técnico en su Patterns of Enterprise Application Architecture (del cual ya estuve hablando en post anteriores), encapsulándolo en tres patrones del tipo Object-Relational Metadata Mapping: Metadata Mapping, Query Object y Repository. Se puede encontrar una buena implementación de Repository en ese libro, una implementación anterior a DDD.

Un Repository sirve para crear la ilusión de que tenemos una colección en memoria de absolutamente todos los objetos de dominio de un tipo específico. Necesitaremos un Repository para cada tipo de objeto al que accederemos de forma global. Por supuesto que no haremos un Repository para cada objeto. A las demás Entidades y Value Objects accederemos a través de las relaciones de este objeto reconstituido con el Repository. Como regla general, es casi seguro que tengamos un Repository por cada Entidad Raíz de cada Agregación.

Conclusión

Resumiendo, la Parte 2 de Domain-Driven Design: Tackling Complexity in the Heart of Software (ISBN: 0-321-12521-5) habla de los componentes fundamentales de un Model-Driven Design. Comienza con un patrón, Layered Architecture, que nos dice que una forma apropiada de aislar el dominio del resto de la aplicación es a través de una correcta separación de capas. Hay muchas formas de dividir en capas una aplicación, una de las más comunes es la que ya venimos viendo en los últimos posts: Presentación / Acceso al Dominio / Dominio / Persistencia / Base de Datos. En la capa de Dominio es donde vivirá toda la lógica de negocio y los datos sobre los cuales esta lógica opera.

(Notar que muchos frameworks técnicos que actualmente hay en el mercado organizan la aplicación de diferentes maneras. Algunas separaciones en capas pueden ir totalmente en contra de los principios de DDD. Por ejemplo, en Java EE se ha definido una especificación para aplicaciones enterprise (EJB 3), cuya arquitectura no es del todo compatible con la arquitectura de DDD. La división de capas en Java EE 5 es Presentación / Lógica de Negocio / Persistencia / Base de Datos. A primera vista uno podría pensar que la única diferencia entre una arquitectura y otra es que la de EJB 3 carece de Acceso al Negocio (o Service Layer). Esta diferencia es menor y realmente no es importante, ya que no se trata de una diferencia conceptual. La Service Layer sirve para desacoplar la Presentación del Dominio, que tranquilamente se puede implementar en Java EE. El problema fundamental es que EJB 3 separa en distintas capas la lógica de negocio y los datos con los que esta lógica opera. En la capa de Lógica de Negocio viven los Session Beans que implementan toda la lógica del dominio, todas las operaciones específicas del negocio, y en la capa de Persistencia viven los Entity Beans, que representan el modelo de objetos de negocio y contienen el mapeo objeto-relacional para que el Entity Manager pueda ejecutar queries a una base de datos relacional. Eric Evans nos recuerda constantemente que los Entity Beans de EJB no tienen nada que ver con el patrón Entity de DDD, por más que los nombres puedan confundirnos. Evans dice que al separar la lógica de negocio de las Entidades en dos capas distintas estamos perdiendo la portabilidad, la reutilización, los conceptos y el conocimiento del dominio. Estamos desperdigando el modelo, mezclándolo con incumbencias técnicas y perdiendo la aislación, requerimiento fundamental para una correcta utilización de DDD.

A quien le interese saber más sobre EJB3, en el blog de Tecnologías Java hay una introducción al tema y, en un futuro, es muy probable que me explaye un poco más sobre esta incompatibilidad de arquitecturas.)

Una vez aislado el dominio, Evans nos cuenta que existen Asociaciones entre los objetos de dominio y que es muy importante limitar la cantidad de ellas. Para ello nos da un conjunto de tips bastante útil a la hora de modelarlas. En el dominio pueden existir tres tipos de objetos: Entities, Value Objects y Services. Los Entities son objetos de dominio que tienen identidad y que por lo tanto son distinguibles. Los Value Objects no tienen identidad, por ende no son distinguibles; simplemente son objetos que representan algún valor o estado. Los Services son objetos puramente funcionales, que publican servicios para que sean consumidos por otros objetos de dominio o por la capa superior, y que manipulan Entities y Value Objects. Cuando un modelo crece bastante como para ser manejado de forma atómica, Evans nos enseña a separarlo en Módulos utilizando algún mecanismo provisto por el lenguaje de programación utilizado (en el caso de Java, los Módulos serían Paquetes).

A continuación, Evans se enfoca en el ciclo de vida de los objetos de dominio. Dice que un objeto nace, a través de un Factory o de un código simple de creación, se manipula en memoria y luego se almacena en un Repositorio de objetos de su mismo tipo. Este objeto puede ser vuelto a pedir al Repositorio en cualquier momento para volver a ser modificado y volverlo a guardar, archivarlo o borrarlo. Gracias a los Repositorios, el dominio puede abstraerse de la infraestructura que reside en la capa inferior y que se encarga de la Persistencia de los objetos.

Pero no todos los objetos serán accedidos directamente a través de Repositorios. Habrá objetos que se manipularán indirectamente, a través de Asociaciones establecidas con el objeto que sí se trajo del Repositorio. Así se establecen las Agregaciones, que encapsulan un grupo de Entidades y Value Objects que serán creados, modificados y borrados como una sola unidad. Vistas desde afuera, las Agregaciones cuentan con una Entidad Raíz, que es la que generalmente será recuperada desde un Repositorio, y una Frontera, que encerrará un conjunto de objetos internos que son invisibles al exterior. (Una aclaración que no he mencionado antes es que no siempre la raíz de una Agregación tiene que ser una Entidad; puede haber excepciones; pero el caso más común es que sea una Entidad, cuya identidad única le brinda una identidad única a la Agregación entera.)


En este capítulo se habla también de otras cosas. Se habla de los distintos paradigmas de modelado que pueden convivir en una sola aplicación. Se hace incapié en la impedancia de paradigmas que se da con más frecuencia en una aplicación enterprise: la del modelo de objetos con el modelo relacional. Evans da consejos muy útiles para el diseño de objetos que serán persistidos en una base de datos relacional (tema que no he tratado en los posts y que es altamente recomendable leer desde el libro).


Por último, dedica una sección completa a un ejemplo bien completo en el que pone en práctica cada uno de los conceptos fundamentales explicados. El ejemplo también lo dejo para que lo lean del libro.

------------------------------------------


DDD no termina aquí. A esta segunda parte le continúan dos partes más, espectaculares. Una habla sobre Refactoring del Código y del Diseño hacia una mejor comprensión del Dominio, que trata temas fascinantes como, entre otros, el modelado de diseños flexibles, formas de lograr saltos importantes en la comprensión del dominio, formas de llevar a la luz conceptos que hasta el momento estaban implícitos (y ocultos) y una interesante aplicación sobre los Patrones de Análisis de Martin Fowler. La otra parte habla sobre Diseños Estratégicos. Sobre todo se enfoca en aplicaciones enterprise, aplicaciones con dominios inmensos, donde el modelo de dominio debe ser construido por distintos equipos de trabajos. Trata los temas de mantenimiento de la integridad del modelo, destilación de un modelo, estructuras de gran escala (modelos bien bien grandes) y cierra con un capítulo donde combina todos los patrones y estrategias vistas en esta sección.

Como se habrán dado cuenta los que llegaron hasta acá, Domain-Driven Design: Tackling Complexity in the Heart of Software (ISBN: 0-321-12521-5) es un libro que no tiene desperdicio.