lunes, 25 de febrero de 2013

Arquitecturas para Domain-Driven Design - Parte IV

Ya queda poco para explicar los conceptos mas básicos de DDD y ponernos con la arquitectura. Quiero recordar a los lectores que el tema de DDD es un campo enorme y estos posts son simplemente orientativos para facilitar un punto de entrada sencilla a cualquiera que quiera documentarse y aprender a fondo DDD. Al finalizar esta lista de conceptos básicos agregare reseñas a la bibliografía y documentación de la cual he plagiado todo esto.



En el post anterior expliqué las Entidades. Hoy toca:

Aggregate y Aggregate Roots

No lo pienso traducir porque "raíces agregadas" queda muy feo.

En el dominio de un sistema hay cosas que inevitablemente deben ir juntas de la mano. Un aggregate es precisamente ese conjunto de cosas. Un aggregate root mantiene unido y coherente dicho conjunto.

Un sencillo ejemplo

En un sistema de ventas, un Cliente puede tener una referencia a los Pedidos de ese cliente y un pedido debe tener referencia a las Líneas del Pedido (ítem, cantidad, precio del ítem, etc).

Se puede observar que un Pedido no tiene sentido sin un Cliente que haya realizado ese Pedido, y una Línea de Pedido no tiene sentido sin el Pedido. Son conceptos que van de la mano y por tanto se puede deducir que Cliente y Pedido es un aggregate y que Pedido y LineaDePedido es otro aggregate.

Ahora debemos deducir cual seria el aggregate root. Para esto, solo tenemos que mirar cual es la Entidad principal que actuaría de "punto de entrada", o la entidad desde la cual "tiramos del hilo" para realizar las operaciones.

En este ejemplo está claro que la raíz de uno de los agregados es Cliente y la raíz del otro es Pedido. También se puede observar que, aunque Pedido actúa como root de LineaDePedidos, al ser éste un hijo de Cliente, no se considerará aggregate root principal para los repositorios del dominio.

¿Y todo esto para que?

La característica principal de un aggregate root es que actúa como un ente único que controla el acceso a sus hijos. Gracias a esto se mantiene la coherencia del conjunto. Básicamente provee un patrón para mantener la lógica de dónde pertenece realmente un ítem.

Es la entidad raíz la que expone las acciones a realizar para con sus hijos. Es el Cliente el que expone una acción para realizar un nuevo Pedido (Cliente.RealizarNuevoPedido) o para cancelarlo (Cliente.CancelaPedido). Esto nos permite encapsular las reglas y restricciones del dominio.

Si una restricción nos dice que un Cliente sólo puede tener 3 Pedidos abierto a la vez, es responsabilidad del Cliente (en la acción Cliente.RealizarNuevoPedido) el contar cuántos Pedidos tengo abiertos; si no llego al máximo debo realizar el nuevo pedido, y si no puedo realizar más pedidos debo notificarlo de alguna manera y no realizar el nuevo pedido.

Se aplica un caso parecido con respecto a Pedidos y LineasDePedido.

Otra característica que deben cumplir los aggregate root es que son las únicas entidades que retornan los repositorios. Nunca se debería poder obtener de la capa de persistencia un Pedido directamente. Debo obtener el Cliente y "tirar del hilo" para llegar al Pedido, o seguir "tirando del hilo" para llegar a una Línea de Pedido. Hay que "navegar" a través de las entidades raíces.

No dejéis que os engañe un experto del dominio que diga, por poner un ejemplo chorra, que hay que listar todos los Pedidos abiertos en el sistema independientemente del Cliente para que un operador les de el visto bueno o los marque como erróneos. Dado que es probable que por otro lado se necesite bloquear a un Cliente la capacidad de realizar un nuevo Pedido (debido a problemas con un cobro, una tarjeta de crédito bloqueada o cualquier otra cosa), una forma correcta de enfrentar esto sería recuperar del repositorio todos los identificadores de los clientes y la información (con sus identificadores) de sus pedidos abiertos a través de una proyección de los datos con la 1ª forma normal y mostrar esos Pedidos por pantalla. Cuando el operador los marcase como correctos, deberíamos obtener la entidad cliente por su identificador utilizar una acción expuesta por el Cliente (Cliente.AceptarPedido(identificadorPedido)) que aplicara las reglas y restricciones para con los Pedidos según el estado actual de ese Cliente.

En resumen

Los aggregates proveen un agrupamiento lógico de Entidades y Objetos-Valor. El aggregate root actúa de punto de entrada para ese conjunto, encargándose de las normas y restricciones que deban cumplir las colecciones de hijos.

5 comentarios:

  1. Muy buen artículo! Y es que cada vez me confundía sobre los Agregados.

    Saludos desde Santa Cruz, Bolivia.

    Twitter: @uialberto

    ResponderEliminar
  2. Muchas gracias. Pero en este caso el merito no es mio. Mi intención no es enseñar teoria DDD; si no llevar esa teoría a la practica en una arquitectura de software. Estos posts son muy introductorios y básicamente son una amalgama resumida de los artículos y libros con los que yo he aprendido DDD. Al final del repaso de teoría pondré un post con las referencias dando el merecido crédito a la gente que realmente ha escrito esto.

    ResponderEliminar
  3. Buen dia despues de haber leido tu articulo me surge dos pequeñas dudas.
    1.Solo los agreggate roots tienen repositorios no? Entonces en tu ejemplo solo exisitiria un repostiorio para Cliente. Pero y si quiero listar todos los pedidos?. Tengo que obtener todos los clientes y luego de ahi listar los pedidos para cada uno?

    2. Supongamos que tenemos tambien la entidad Producto, en este caso un producto seria un Agreggate Root o un Agreggate de Pedido?


    Saludos!
    Muchas Gracias!!!

    ResponderEliminar
  4. Buenos días Martin.

    El modelado de los agregados es el tema mas peliagudo de DDD.

    El diseño de agregados depende mucho de las necesidades, reglas e invariantes del dominio (ademas del rendimiento y escalabilidad, pero eso es otro tema). Un agregado tiene sentido cuando no puedes realizar alguna acción en una entidad sin que esto implique otra acción en otra entidad. Es un contexto de Transaccionalidad y ACID (http://es.wikipedia.org/wiki/ACID).

    En el ejemplo que pongo puedes observar que modificar (darle el visto bueno o marcarlo como erróneo) un pedido conlleva chequear si el usuario tiene la cuenta bloqueada, por lo que debes tener en memoria el cliente y realizar la acción de dar el visto bueno al pedido a traves de la entidad Cliente.

    Realizar la tarea a través de la entidad raíz ( Cliente.AceptarPedido(idPedido) ) permite que el cliente pueda chequearse a si mismo para corroborar que puede aceptar el pedido o, por ejemplo, que tenga la cuenta corriente bloqueada y no deba aceptar mas pedidos. Es cuestión de diseñar bien las clases y encapsular las responsabilidades (en este caso es el cliente el responsable de negarse a aceptar un pedido).

    En un buen diseño orientado a objetos verás que se cumple la máxima "No preguntes, pide". No preguntamos por el estado del Cliente y realizamos una accion. Le debemos pedir al Cliente que realice la accion y el cliente decida si puede o no. No debemos tener en nuestro sistema código que se repite en un montón de sitios preguntando cosas como estas:

    If (cliente.ctaBloqueada and cliente.numPedidos < 3) or cliente.saldo > 0 {

    pedido.aceptado = true
    cliente.numPedidos++

    }

    En el caso de Cliente.AceptarPedido(idPedido) la entidad Cliente internamente se encargara de aplicar sus invariantes llamando a sus propios métodos privados(Cliente.PuedoAceptarPedidos() que englobaría this.CtaCorrienteIsBloqued() and this.TengoPedidosAlMaximo(), etc; y así ir desgranando las responsabilidades poco a poco).

    Como puedes ver, las entidades de dominio y los agregados se utilizan para encapsular las reglas y las invariantes a las acciones. Esto solo tiene sentido cuando realizamos una tarea en el sistema que implica alguna modificación (insert, update, delete).
    Para no podernos saltar esta encapsulación creamos "repositorios de dominio" que solo permiten obtener agregate roots o entidades que no son agregados raiz pero que no son "hijos" de ningun agregado (obviamente). Estos repositorios tiene funciones de consulta para traerse entidades de persistencia pero solo se usan para obtener las entidades a las que le vamos a realizar la tarea pertinente que nos haya mandado el usuario.

    ResponderEliminar
  5. Para las vistas hay que utilizar Command Query Responsibility Segregation (CQRS) que consiste en utilizar "repositorios de vista" que permite traer la información de forma desnomalizada (1ª forma normal) para plantarla directamente en la vista.

    Podrías tener un repositorio que se trajese el modelo de vista siguiente:

    Class CheckPedidosViewModel{

    public int ClienteID;
    public int PedidoID;
    //mas propiedades

    }


    RepositorioVistas.getCheckPedidosView() {
    //codigo consulta

    return CheckPedidosViewModel

    }

    mostrar esto en un grid de una pagina web (esto es listar todos los pedidos) y cuando el usuario pulse el botón "Dar Visto Bueno" (una modificacion en el dominio) de una linea del grid; obtendremos el ClienteID y el PedidoID de esa linea, recuperamos el modelo de dominio a través de los "repositorios de dominio" y realizamos la acción:

    clienteSeleccionado = ClienteRepository.getByID(clienteID);
    cliente.AceptarPedido(pedidoID)

    Para que esto surja de forma natural se requiere que la interfaz de usuario este basada en tareas (Task-Based UI) y no en CRUD; puesto que, aunque pueda parecer lo contrario, el diseño de la interfaz de usuario afecta directamente a la arquitectura de la aplicación.

    Con respecto a tu segunda pregunta.

    Un Producto no seria un aggregado de Pedidos puesto que realizar alguna modificación en el catalogo de productos o en algún dato del producto no implica tocar ninguna otra entidad para mantener consistencia y coherencia. Por lo que no necesita un contexto de transaccion para operaciones ACID. Por lo tanto, asumimos que Producto es una entidad con su correspondiente repositorio.

    Obviamente todo esto depende muchísimo del dominio de tu aplicación y no siempre en todos los sistemas un Producto tiene por que modelarse de esta forma.

    ResponderEliminar