domingo, 15 de marzo de 2009

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

Desarrollar un buen Modelo de Dominio es un arte, pero la tarea de diseñar e implementar elementos individuales del modelo puede resultar relativamente sistemática. En la Parte II de su libro Domain-Driven Design: Tackling Complexity in the Heart of Software (ISBN: 0-321-12521-5), Eric Evans propone seguir un conjunto de patrones de dominio, ya probados y estudiados, para conseguir modelar un dominio que resulte práctico para la implementación. En este post veremos estos patrones, que son parte del ecosistema de Domain-Driven Design y son los que Evans llama "los componentes fundamentales de Model-Driven Design":


Aislando el Dominio

Usualmente, las líneas de código que efectivamente resuelven los problemas del dominio constituyen apenas una pequeña porción del sistema. Sin embargo, la importancia de estas líneas es absolutamente desproporcionada con respecto a su tamaño. Como vimos en los posts relacionados con Aplicaciones Enterprise, en un sistema grande la importancia de separar los objetos de dominio del resto de la aplicación es extremadamente importante. La tarea principal de un equipo de trabajo de DDD es aislar el dominio de los demás conceptos del sistema relacionados con la tecnología. La estrategia de dividir las funcionalidades del sistema en capas es uno de los caminos más apropiados para desacoplar el dominio de la tecnología.

I. Patrón 1: Layered Architecture

En el primer post sobre Aplicaciones Enterprise vimos una separación de capas típica, que es la que propone Martin Fowler en su famoso libro Patterns of Enterprise Application Architecture, la biblia fundamental para la construcción de Aplicaciones Enterprise y el nuevo testamento de los Patrones de Diseño:


Pero Fowler explica muy bien que ésta no es la única forma de dividir en capas una aplicación, y en el capítulo 8, en la última sección, da ejemplos de varios otros esquemas de capas para arquitecturas enterprise:
  • Presentación / Controlador / Dominio / Data Mapping / Data Source [Capas de Brown]
  • Cliente / Presentación / Negocio / Integración / Recursos [Capas del viejo core de J2EE]
  • Presentación / Negocio / Acceso a Datos [Capas de Microsoft DNA]
  • Presentación / Aplicación / Servicios / Dominio / Persistencia [Capas de Marinescu]
  • Consumer / Consumer Helper / Aplicación / Dominio / Acceso a Persistencia / Public Stored Procedures / Private Stored Procedures [Capas de Nilsson]
Evans propone una arquitectura muy parecida a la de Fowler que vimos en los post anteriores, pero sin capa de Acceso al Negocio o Service Layer:


Hay un montón de formas de dividir un sistema de software en capas, pero a través de la experiencia y la convención, la industria del desarrollo de software ha convergido en este patrón llamado Layered Architecture, el cual nos muestra que, por lo menos, vamos a contar con una capa de dominio, una capa de infraestructura, una capa de aplicación y algún cliente o interfaz de usuario que consuma esa aplicación. Los nombres de las capas en las diferentes arquitecturas pueden variar, alguna capa puede estar o no estar, o se puede dividir en dos o más, pero lo importante es que nunca puede faltar la capa de Dominio o de Negocio. Esta capa se debe aislar del resto de las capas del sistema. Esta capa será la expresión final del negocio, la implementación limpia de un modelo guiado por el dominio. Domain-Driven Design requiere sólo la capa de negocio para existir. Los objetos de dominio deben quedar libres de la responsabilidad de mostrarse, guardarse y/o responder a tareas de la aplicación, para poder enfocarse en expresar el modelo de dominio.

II. Anti-Patrón: Smart UI

Smart UI es un anti-patrón en el contexto de la filosofía de DDD, es una elección mutuamente exclusiva con el enfoque de Eric Evans. Sin embargo, puede darse el caso de tener que desarrollar una aplicación tan sencilla para la cual no haga falta adentrarse en la complejidad de DDD. Como vimos en el primer post de Patrones Enterprise, optar por un Domain Model para organizar la arquitectura de un sistema implica una curva de aprendizaje realmente alta. Martin Fowler nos habla de Transaction Script, de Table Module, y de Domain Model, tres patrones para encarar la arquitectura del sistema. DDD es un conjunto de buenas prácticas para implementar un Domain Model.

Smart UI es un patrón que entra en esta categoría de patrones de organización del dominio de Fowler, y es mucho más sencillo que un Transaction Script, ya que Smart UI no separa la vista de la aplicación y tan siquiera Transaction Script sí.

Evans dice que este patrón tiene varias ventajas: la productividad para el desarrollo de una aplicación simple es muy alta, los desarrolladores no necesitan mucha experiencia para construir una aplicación, etc. Pero también hay serias desventajas: la migración a otra tecnología es imposible, el escalamiento se hace tortuoso, no hay reutilización de código ni de conceptos (prácticamente no hay conceptos), la integración con otras aplicaciones es casi imposible (salvo a través de una base de datos) y, por supuesto, la desventaja más importante y, supongo, la que resume todo, Smart UI no es Orientación a Objetos.

De todas formas, es un patrón más de dominio a considerar para cuando hay muy poco tiempo, la aplicación es muy chica o se trata simplemente de un prototipo no evolutivo.

Un Modelo Expresado en Software

III. Associations
Un modelo que tiene una asociación entre un Cliente y un Vendedor muestra dos cosas. Por un lado, abstrae una relación comercial entre dos personas físicas, reales. Por el otro, representa una referencia entre dos objetos, la encapsulación de una búsqueda a una base de datos a través de una clave foránea o cualquier otra implementación a nivel diseño o código fuente.

Para que el modelo sea más sencillo y fácil de implementar, es importante limitar las asociaciones lo más posible. Para esto, existen al menos tres criterios para simplificar una asociación:

1) Imponer una dirección a la asociación
2) Añadir un cualificativo en algún extremo de la asociación (para reducir la multiplicidad)
3) Eliminar las asociaciones no esenciales

El libro de Evans da como ejemplo una relación llamada presidente entre Persona y País. Es una relación 1 a *, donde un País tuvo muchas Personas que fueron presidentes.


A priori esta asociación es bidireccional. Sin embargo, dependiendo del dominio, quizá no sea necesario que esta asociación realmente sea bidireccional. En este ejemplo, raramente alguien va a necesitar conocer de qué País fue presidente tal Persona; la consulta más interesante va a ser qué Personas fueron presidentes de tal País. Claramente esta asociación puede convertirse en unidireccional aplicando el primer criterio e imponiéndole la dirección desde País hasta Persona:


Este refinamiento refleja una mejor visión del dominio y hace más práctico al diseño. Claramente una dirección es mucho más importante que la otra y, de esta forma, se refleja en el modelo y desacopla el concepto importantísimo de Persona del concepto mucho menos importante de presidente.

El segundo criterio hace referencia a un entendimiento mucho más profundo del dominio. Agregar un cualificativo en algún extremo de la asociación puede reducir esta relación de 1 a * a una simple relación de 1 a 1, y como corolario explicitar una regla muy importante en el modelo. En el ejemplo, puede darse el caso de que al negocio lo que más le importe preguntar es quién fue el presidente de tal País en determinado período:


Notemos que ahora la relación presidente cualificada en un período entre País y Persona, reduce la multiplicidad de la asociación a una Persona fue presidente de un País en determinado período.

El último criterio es bastante obvio y hace referencia a que si en realidad no es relevante para el modelo relacionar Personas con Países directamente conviene remover la asociación. Recordemos: limitar las asociaciones lo más posible.


IV. Patrón 2: Entities

El patrón Entity se basa en que hay objetos que no son fundamentalmente definidos por sus atributos, sino más bien por un hilo de continuidad e identidad que se mantiene a través de todo un ciclo de vida. Una Entidad puede crearse, persistirse, recuperarse, viajar a través de una red y armarse del otro lado, e independientemente de que para reconstruirlo tengamos que crear una instancia nueva de objeto, la entidad deberá conservarse. Un caso de pérdida de identidad en un sistema de software conduce a serios problemas de corrupción de datos y errores fatales de ejecución.

Toda Entidad debe contar con un método para comparar su identidad con otra instancia de su misma clase. A veces un objeto puede poseer la misma identidad que otro, a pesar de no coincidir el valor de todos sus atributos. A veces puede ocurrir el caso contrario. Una Entidad debe ser distinguible más allá de que exista otro objeto con todos sus atributos equivalentes. La identidad de una Entidad va más allá del valor de sus atributos.



Una Entidad es algo que tiene continuidad a través de un ciclo de vida y es distinguible independientemente de los atributos que son importantes para la aplicación.

Se deben definir operaciones que garanticen la producción de un único resultado para cada objeto. Se puede adjuntar un símbolo o un uid (unique id). El modelo debe definir qué significa tener una única identidad.



Ejemplos obvios de Entities pueden ser: Persona, Cliente, Factura, Vuelo, País, y un largo etcétera.


La responsabilidad más básica de un Entity es establecer continuidad de modo que su comportamiento sea claro y predecible. DDD recomienda modelar las Entidades lo más pequeñas y sencillas posible, añadiendo sólo el comportamiento y los atributos necesarios. Cualquier comportamiento y/o atributo que pueda ser trasladado a otra Entidad o a otro Value Object, bienvenido sea. Y la operación obligatoria, imprescindible, es la igualdad ante otro objeto.

La definición de identidad emerge del modelo. Definir las Entidades demanda entender profundamente el dominio. Antes hablamos de generar un único ID para cada Entidad. El ID se debe generar cuando la Entidad se crea por primera vez y luego, ese identificador, deberá permanecer inmutable; se puede persistir en una base de datos, pasar a través de una red, lo que sea, pero jamás se debe alterar el valor de un ID. Y como el ID es generado automáticamente por el dominio, el usuario nunca debe verlo o tener acceso a él, exceptuando que el mismo sea de particular interés, como puede ser un trackID, un número de ticket, de reclamo, etc.

En muchos casos puede suceder también que el ID venga de afuera del sistema, desde otro sistema, por ejemplo. Éste puede ser el caso del Documento Nacional de Identidad (DNI) para Argentina o del Número de Seguro Social para Estados Unidos; estos ID suelen ser generados por entidades gubernamentales. Usar como identidad un DNI o cualquiera de este tipo de identificaciones no es un mecanismo seguro (puede suceder que un extranjero no posea DNI o que una persona no desee revelar su Número de Seguro Social por cuestiones de privacidad).

La generación de un único ID para definir la identidad de un Entity está muy relacionado con el patrón Identity Field de Martin Fowler.

V. Patrón 3: Value Objects

Mantener el seguimiento de las Entidades es esencial, pero darle identidad a otros objetos que no son Entidades puede herir gravemente la performance y ensuciar el modelo. No todos los objetos son Entidades. Hay algunos objetos que representan aspectos descriptivos del dominio sin incluir un concepto de identidad. Estos objetos son los llamados Value Objects.

Ejemplos de Value Objects pueden ser: Color, Dirección, Rueda, Lápicera, Herramienta, etc. Sin embargo, decidir qué objetos son Value Objects y qué objetos Entities dependerá mucho del dominio. En un sistema de servicios postales, Dirección probablemente sea un Entity y no un Value Object como en la mayoría de los casos. Los Value Objects son instanciados para representar elementos del diseño de los cuales importa qué son y no quién son.

Los Value Objects no necesariamente tienen que ser simples. Nada impide que un Value Object contenga un montón de atributos y operaciones. En general, son objetos usados para pasar por parámetro en los mensajes entre otros objetos. Frecuentemente son transitorios, creados únicamente para la operación y luego descartados. Suelen ser usados como atributos de Entities u otros Value Objects.

Como recomendación principal, Evans nos dice que debemos tratar a los Value Objects como objetos inmutables. Es muy importante conceptualmente que los Value Objects sean inmutables, ya que no nos interesa distinguir un Value Object de otro, cuando necesitamos alterar su estado, simplemente tiramos el original y creamos uno nuevo. Por otro lado, Evans también nos dice que es recomendable dedicar tiempo a la optimización del manejo de los Value Objects, ya que suelen ser numerosos en cualquier dominio. Esto nos lleva a una contradicción, ya que crear objetos inmutables y tirarlos a cada rato, claramente no es óptimo. Por eso, hay casos especiales en los que, en pos de la performance, se puede permitir que los Value Objects sean mutables; por ejemplo cuando los valores del Value Objects cambian frecuentemente o cuando las operaciones de creación y destrucción del objeto son muy costosas.

En general, se prefiere que los Value Objects sean inmutables.


VI. Patrón 4: Services

En algunos casos, el diseño más claro y pragmático incluye operaciones que conceptualmente no pertenecen a ningún objeto. Sería un error encajarle esas operaciones a cualquier Entidad o Value Object que se encuentre por ahí dando vueltas. Por eso, para estos casos, estas operaciones deben ser incluidas como métodos en un Servicio que se modele de forma explícita en el dominio.


Los Servicios son actividades, funciones, operaciones, no cosas. No conservan estado; son stateless. Un Servicio es una operación ofrecida como una interfaz suelta en el dominio, que no encapsula estado, como sí lo hacen los Entities y los Value Objects. Services es un patrón bastante común en los frameworks técnicos, pero el concepto también puede ser aplicado a la capa de dominio.

Un Servicio debe tener una responsabilidad bien definida, y esa responsabilidad y su interface deben estar definidas como parte del Modelo de Dominio. Los nombres de las operaciones deben surgir del Lenguaje Ubicuo o deben ser agregados. Los parámetros y los resultados del Servicio deben ser objetos de dominio.

Cuando un proceso significativo, o una transformación en el dominio, no es una responsabilidad natural de un Entity o un Value Object, se debe agregar una operación al modelo como una interfaz independiente declarada como un Servicio.


Los Servicios no sólo son usados en la capa de dominio. Es importante distinguir los Servicios que pertenecen al dominio, de los Servicios que pueden encontrarse en otra capa de infraestructura o de aplicación. A veces puede resultar una tarea difícil. Una regla valiosa puede ser considerar que los Servicios técnicos carecen absolutamente de significado para el negocio, en cambio los de dominio forman parte del modelo, y está bien que así sea. Un Servicio para exportar un reporte a PDF claramente es un Servicio técnico. Un Servicio para transferir dinero de una cuenta a otra es un Servicio de dominio.



Es muy común construir una capa de Servicios para manipular la capa de dominio. Es una forma de desacoplar el dominio de la presentación. Muchos de los Servicios de dominio pueden vivir en esta capa. Es una capa que ya nombré en varios posts (incluso en éste); es la capa de Acceso al Negocio o Service Layer.

VII. Patrón 5: Modules

La modularización en la capa de dominio debe emerger como una parte significativa del modelo; debe servir para contar la historia del dominio a gran escala, a muy alto nivel. Hay un límite para la cantidad de objetos en los que una persona puede pensar a la vez. Por eso, es importante modulizar de forma de agrupar conceptos o incumbencias de negocio. En general, los lenguajes de programación proveen una sintaxis para crear Módulos, y los IDEs hacen su parte mostrándonos esos módulos agrupados de forma visual. Si estamos en el mundo de Java, por ejemplo, estos módulos serán los paquetes.

Bajo acoplamiento y alta cohesión son dos principios generales de diseño que aplican tanto a clases individuales como a Módulos. Un bajo acoplamiento entre los diferentes módulos, hace posible el análisis del módulo de forma aislada, con el mínimo de referencias e interacciones con el mundo exterior, compuesto por un ecosistema de otros módulos.


Refactorear Módulos es más trabajoso y más difícil que refactorear clases; probablemente no sea una tarea que tengamos que hacer frecuentemente, a menos que partamos de un diseño demasiado pobre.

Los Módulos son mecanismos de comunicación. Cuando se decide colocar una clase dentro de algún módulo, es una forma de explicarle al próximo programador que mire el diseño, que el concepto y la incumbencia que representa esa clase sólo se puede entender dentro del contexto del Módulo en que fue ubicada. Si pensamos en términos de un libro, los Módulos son los capítulos.


La modularización de un dominio es una tarea que requiere alto nivel de abstracción y sumo cuidado. Un error temprano en la elección de los Módulos puede desembocar en Módulos con alto acoplamiento, lo que después complicará muchísimo las tareas de refactor.

Pero, ¿qué significa un error en la modularización del dominio? Para DDD, por supuesto, una forma errónea de modularizar la capa de dominio es de acuerdo a incumbencias técnicas. Recordemos que la capa de dominio debe ser una proyección del Modelo de Dominio, del cual el experto del negocio debe tener conocimiento y comprender. Por eso la separación de Módulos debe ser de acuerdo a conceptos de negocio. Eric Evans pone el ejemplo de muchas malas implementación de Java EE usando EJBs. A menudo, se suelen encontrar Entity Beans en el modelo, que agrupan un conjunto de atributos para después poder persistir o hacer cualquier otra cosa, y por otro lado un conjunto de Session Beans que operan sobre esos datos haciendo uso de la lógica de negocio. Claramente estos Entity Beans no se corresponden con los Entities de DDD. Los Entities deben contener los datos y la lógica de negocio para operar sobre esos datos. Hacer lo contrario es lo que Martin Fowler llama un modelo anémico. Encima, dice Evans, el error más grave se da a la hora de modularizar, cuando se suele ubicar a los Entity Beans en un paquete y a los Session Beans en otro. Eso es un ejemplo de modularización por incumbencias técnicas. En todo caso, el Entity Bean y el SessionBean que pertenecen al mismo concepto (en resumen: al mismo Entity) deberían vivir en un mismo paquete. (A quien le interese el tema de los EJBs, puede leer un poco en el blog de Tecnologías Java, que tiene una sección dedicada a EJB 3.)


La separación de paquetes por incumbencias técnicas, tiene dos grandes costos:
  • El código no revelará de forma explícita el modelo
  • Los desarrolladores del dominio perderán la capacidad de dividir el modelo en piezas significativas
Los Módulos, en una capa de dominio, sirven para separar el negocio en áreas o incumbencias significativas y también para aislar la capa entera del resto del código. En cambio, es muy posible que para el resto de las capas, una modularización por incumbencias técnicas sea mucho más apropiada.


VIII. Paradigmas de Modelado

No es necesario que un Modelo de Dominio tenga que estar construido con un Modelo de Objetos. Evans dice que existen Model-Driven Designs implementados en lenguajes de paradigma lógico como Prolog, con modelos construidos en base a reglas lógicas y hechos.


Si gran parte del problema del dominio se podría expresar de forma más natural en otro paradigma, quizá tendría sentido cambiar de paradigma y elegir otra plataforma de implementación. Pero también se puede dar el caso de que sólo una parte del dominio responda a X paradigma, en cuyo caso, si la interdependencia con esa pieza del problema es razonablemente pequeña, se puede encapsular. Podemos tener un modelo que mezcle paradigmas, pero sólo si es necesario; mezclar paradigmas aumenta la complejidad del desarrollo.

Como desarrolladores, a menudo nos enfrentamos a sistemas cuyos fragmentos de código están implementados con diferentes paradigmas y éstos se mezclan de formas más o menos elegantes. El ejemplo clásico es el del paradigma orientado a objetos y el de tablas relacionales para los motores de base de datos. Los ORM, en estos casos, se encargan de desacoplar de forma elegante estos dos paradigmas, quitando la responsabilidad al programador de tener que lidiar con la impedancia entre ambos diseños. Otro ejemplo clásico puede ser el de un conjunto de reglas de negocio representado por un motor de reglas de negocio, o un proceso modelado con un paradigma gráfico como el que implementa un motor de workflows.


El paradigma predominante hoy en día es el orientado a objetos. Evans nos da cuatro consejos para lidiar con distintos paradigmas inmersos en un sistema orientado a objetos:
  • No luchar contra la implementación del paradigma. Siempre existe otra forma de pensar en el dominio. Es importante encontrar los conceptos del modelo que encajan con el paradigma.
  • Apoyarse en el lenguaje ubicuo, como nexo conductor entre ambos paradigmas.
  • No obsesionarse con UML. UML es ideal para el paradigma de objetos, pero la fijación de UML puede conducir a diseños demasiados retorcidos para otros paradigmas.
  • Ser escéptico. ¿Es realmente apropiada la herramienta? Sólo porque tengamos algunas reglas de negocio, por ejemplo, no significa que tengamos que lidiar con la complejidad de un motor de reglas de negocios. Las reglas pueden ser expresados en objetos, por ejemplo usando el patrón Specification. La mezcla de paradigmas complicará el sistema a veces de forma innecesaria.