Complejidad del negocio con patrones DDD y CQRS – II

Bienvenidos otra vez a esta segunda parte de cómo abordar la elaboración de una API con cierta complejidad en las especificaciones del dominio, y que por lo tanto nos serviremos de técnicas como DDD y CQRS para abordarlo. En el primer artículo vimos en líneas generales que significaba CQRS y DDD, y empezamos a hablar de ciertos conceptos como Bounded Context, Lenguaje Ubicuo y estructuración del proyecto en capas, todos aspectos propios de DDD. En ésta ocasión centraremos el contenido en el diseño e implementación en código de la capa más importante en DDD, la capa de dominio.

Antes de entrar en materia, recordaros que todos los ejemplos de código se encuentran en el repositorio de github dedicado al sitio web. Nada más que añadir, nos preparamos un café y al lio!! ☕

Capa de Dominio

Esta capa es el núcleo o corazón del software desde el punto de vista del negocio, es la responsable de representar los conceptos del negocio así como la información de la situación y de las propias reglas subyacentes. En éste punto es donde nacen las Entidades del Dominio, donde se capturan los datos relevantes junto con el comportamiento inherente al tratamiento de dichos datos.

Es fundamental para un correcto funcionamiento que en cada entidad se sigan los principios de:

  • Ignorancia de la persistencia
  • Ignorancia de la infraestructura

Al final ambos conceptos definen que las entidades del negocio se creen como clases puras (Clases POCO), es decir, sin ningún tipo de referencia a cómo el sistema de persistencia subyacente almacene esas entidades o cualquier framework que nos sirva para dicho propósito, con lo que conseguiremos un total desacoplamiento con el sistema de infraestructura por lo que si el día de mañana queremos cambiar de motor de base de datos no tendremos que tocar las entidades en sí. Además es recomendable que dichas clases tampoco tengan referencias con librerías externas o de terceros, que sean clases puras de C#, Java … etc.

Para ilustrar un poco el ejemplo aquí muestro una imagen de una estructura base para un proyecto de una Capa de Dominio, que será ademas el mismo proyecto que seguiré de ejemplo en todo el proceso de desarrollo:

El proyecto relacionado con el dominio de gestión de tickets, en este caso, se compone principalmente de dos carpetas principales, Aggregates y SeedWork.

  • Aggregates, lo conforman las entidades del dominio antes mencionadas como clases POCO. Las cuales se componen de la clase Agregado raíz (Ticket), junto con sus subclases relacionadas (Product y TicketType) y el contrato con el repositorio (ITicketRepository) ubicado en la capa de Infraestructura.
  • SeedWork, compuesto de clases base utilizadas por todos los agregados para facilitarnos su implementación y no tener que repetirnos con la creación de cada uno (DRY). Cuenta con clases como Entity.cs, para identificar una entidad del sistema y otorgarle algunos aspectos como Eventos de dominio, identificador único…etc; IAggregateRoot.cs, interfaz para indicar cuando una entidad es un Agregado Raíz; ValueObject.cs, para entidades que no forman una identidad per se, sino que complementan la información de otra entidad superior; y otras clases que iremos describiendo más adelante.

Agregado Raíz

El patrón agregado o raíz de agregación nace con el objetivo de simplificar un conjunto de reglas del dominio y de relaciones de entidades en un punto central o entidad base, si recordamos anteriormente comentábamos la existencia de Islas del Conocimiento o Bounded Contexts, como agrupaciones de clases relacionadas en un mismo contexto, pues bien, un Agregado Raíz se identificaría como el punto central o de entrada en cada Bounded Contexts. Todas las acciones o modificaciones que se apliquen en el entramado de entidades relacionadas pasarán siempre por su Agregado, es por esto que sólo estas entidades son las que contienen los repositorios de datos para acceso al sistema de persistencia.

Para ser más exactos esta sería la norma en el caso más simple de modelado del dominio, un Bounded Context asociado con un Agregado Raíz y sus sub-entidades relacionadas, pero no es así exactamente, no siempre existe una relación uno a uno entre ambos entes. La verdad es que para cada Bounded Context pueden existir varios Agregados Raíz, y estos pueden relacionarse entre sí, siempre y cuando su interacción no provoque un acoplamiento entre ellos, es más, la relación entre sus sub-entidades que se relacionen cada una de ellas con diferentes Agregados Raíz, debe pasar siempre por su agregado, nunca se han de relacionar estas sub-entidades de manera directa, puesto que el Agregado es el encargado de mantener su ecosistema de clases siempre acorde a cómo las reglas del negocio se hayan definido.

No existe una norma general y única en estos casos para modelar dominio, siempre depende de las necesidades del negocio que se quiera implementar en ese caso. Al final, a modelar y a cómo y cuando dividir las islas del conocimiento se aprende con la experiencia. Ahora vamos a seguir con un ejemplo del caso de la aplicación para gestión de tickets, y en concreto con la implementación del Agregado Raíz denominado Ticket.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using TicketManagement.Domain.SeedWork;

namespace TicketManagement.Domain.Aggregates.TicketAggregate
{
    public class Ticket : Entity<Guid>, IAggregateRoot
    {
        public int TicketNumber { get; private set; }

        public double CalculatedTotalPrice => GlobalDiscount > 0 && GlobalDiscount < 1 ? TotalPrice * GlobalDiscount : TotalPrice;
        public double GlobalDiscount { get; private set; }
        public double TotalPrice => Products.Sum(p => p.FinalPrice);
        public double CashReceived { get; private set; }
        public double Change { get; private set; }

        public TicketState Status { get; private set; }

        public ICollection<Product> Products { get; private set; }

        internal Ticket()
        {
            Id = Guid.NewGuid();
            Status = TicketState.PENDING;
            Products = new List<Product>();
        }

        public void ApplyDiscountInAllProducts(double discount)
        {

            if (Status != TicketState.BUILDING)
                throw new TicketException("Ticket must be in BUILDING state to admint discounts");

            if (discount <= 0)
                throw new TicketException("A discount of less than zero cannot be applied"); 

            if (discount >= 1)
                throw new TicketException("The discount must be a factor between 0 and 1"); 

            foreach (var product in Products)
            {
                product.ApplyDiscount(discount);
            }
        }

        public void ApplyDiscount(double discount)
        {
            if (Status != TicketState.BUILDING)
                throw new TicketException("Ticket must be in BUILDING state to admint discounts");

            if (discount <= 0)
                throw new TicketException("A discount of less than zero cannot be applied");

            if (discount >= 1)
                throw new TicketException("The discount must be a factor between 0 and 1");

            GlobalDiscount = discount;
        }

        public void PayTicket(double cash)
        {
            if (Status != TicketState.PENDING)
                throw new TicketException("Ticket must be in PENDING state to admin paiments");

            if (cash >= CalculatedTotalPrice)
            {
                CashReceived += cash;
                if (CashReceived >= CalculatedTotalPrice)
                {
                    Change = CashReceived - CalculatedTotalPrice;
                    Status = TicketState.PAID;
                }
            }
        }

        public void AddProduct(string name, double price, double discount)
        {
            if (Status != TicketState.BUILDING)
                throw new TicketException("Ticket must be in BUILDING state to add new products");

            var product = new Product(name, price);
            product.ApplyDiscount(discount);

            Products.Add(product);
        }

        public void RemoveProduct(Guid productId)
        {
            if (Status != TicketState.BUILDING)
                throw new TicketException("Ticket must be in BUILDING state to remove products");

            var product = Products.FirstOrDefault(p => p.Id == productId);
            Products.Remove(product);
        }
    }
}

Esta sería la forma de la raíz de agregación para el dominio de Tickets, como podemos apreciar esta entidad hereda de la clase Entity.cs, que como antes comentaba se utiliza como base para enriquecer nuestras entidades del dominio con cierta funcionalidad genérica, junto con la interfaz IAggregateRoot.cs indicando que esta entidad es un agregado raíz. Si seguimos observando esta entidad veremos que se compone de una serie de propiedades relativas a una gestión ficticia de tickets, propiedades como:

  • TicketNumber: como número identificador del propio ticket para el usuario.
  • GlobalDiscount: Descuento global aplicado al ticket.
  • TotalPrice: Propiedad calculada siendo este el precio total de todos los productos.
  • CashRecived: Dinero obtenido para pagar el ticket.
  • Change: Cambio que se le debe devolver al usuario una vez pague el ticket.
  • CalculatedTotalPrice: Propiedad calculada que representa el sumatorio del precio de todos los productos aplicando el descuento si este existe.
  • Status: Enumerado con los distintos estados del ticket (entraremos en detalle más adelante).
  • Products: Colección de productos a comprar que puede tener un ticket formado.

Todas estas propiedades sirven únicamente para emular la compra de un ticket en un portal web, por ejemplo. Y además también nos debemos de fijar tanto en el constructor de la clase como en los métodos de los que se compone. Aunque la lógica subyacente es bastante sencilla aquí quiero recalcar el concepto de las validaciones del dominio, nuestro dominio debe de ser consistente consigo mismo, esto significa que todos los métodos y constructores antes de nada deben de comprobar que todas las acciones que se realizan sobre el elemento cumplen con las especificaciones definidas en nuestro dominio. Especificaciones como por ejemplo, que el ticket al crearse debe de comenzar en estado pendiente o “Pending“, otro ejemplo seria que para aplicar un descuento o añadir productos al ticket éste debe de estar en estado “Building“. Debido a esto es por lo que todas estas propiedades tienen sus setters privados, para ser gestionados mediante los métodos implementados y evitar que desde el exterior puedan modificar erróneamente las propiedades.

Todas estas validaciones deben de definirse y hacer bastante hincapié en las reuniones con negocio, puesto que además este flujo nace del Lenguaje Ubicuo, comentado en el capitulo anterior de esta serie. Dicho esto no voy a profundizar más en todos los métodos de los que dispone el agregado raíz Ticket.cs, puesto que considero que es bastante sencillo y claro lo que pretendo hacer en cada uno, al final de lo único que se trata es de evitar que la entidad ticket pueda quedarse en un estado inconsistente o erróneo conforme a la información que vayamos extrayendo en las reuniones con negocio.

Pasemos ahora a echarle un vistazo a la clase Product.cs, la cual representa una colección de productos sobre el ticket:

using System;
using TicketManagement.Domain.SeedWork;

namespace TicketManagement.Domain.Aggregates.TicketAggregate
{
    public class Product : Entity<Guid>
    {
        internal Product(string name, double price)
        {
            Id = Guid.NewGuid();
            Name = name;
            SetPrice(price);
        }

        public string Name { get; private set; }

        public double Price { get; private set; }
        public double Discount { get; private set; }
        public double FinalPrice => Discount > 0 && Discount < 1 ? Discount * Price : Price;


        public void SetPrice(double price)
        {
            if (price <= 0) throw new TicketException("I can't create a product with a price less than zero");

            Price = price;
        }
            
        public void ApplyDiscount(double discount)
        {
            Discount = discount;
        }

    }
}

Como vemos esta entidad es mucho más simple que la anterior, y también nos percatamos de que hereda de la clase Entity.cs, que ya veremos lo que nos proporciona, y en cambio esta entidad no esta marcada con la interfaz IAgregateRoot.cs por lo que en nuestro sistema no representa un agregado raíz, sino que es más bien una entidad relacionada con Ticket.cs. Esta entidad se compone de las siguientes propiedades:

  • Name: Nombre del producto.
  • Price: Precio individual del producto.
  • Discount: Descuento aplicado de forma individual al producto.
  • FinalPrice: Propiedad calculada siendo el precio total del producto aplicando el descuento.

Junto con dos métodos para añadir el descuento y el precio al producto. Todas las normas de las estrictas validaciones de dominio también se aplican en esta entidad aunque la misma no sea marcada como agregado raíz, la única diferencia es que para poder modificar un producto, debes de pasar antes por el ticket que lo gestiona. Se ve claramente que el dominio implementado es bastante sencillo, y para que no quepa lugar a dudas vamos a ver como quedaría el modelado del dominio:

Quedando claro que simplemente son dos entidades relacionadas entre si Ticket.cs y Product.cs, siendo esta una relación de uno a muchos. Se puede observar también como aparece destacado el enumerado de TicketState.cs, cobrando relevancia ya que representa el estado interno de nuestro ticket. Y si buceamos un poco en su código veremos lo siguiente:

using TicketManagement.Domain.SeedWork;

namespace TicketManagement.Domain.Aggregates.TicketAggregate
{
    public class TicketState : Enumeration
    {
        public static TicketState BUILDING = new TicketState(0, "Construyendo");
        public static TicketState PENDING = new TicketState(1, "Pendiente");
        public static TicketState PAID = new TicketState(2, "Pagado");
        public static TicketState CANCELLED = new TicketState(3, "Cancelado");

        private TicketState(int id, string name) : base(id, name)
        {
        }
    }
}

No hace falta mirar demasiado para darte cuenta de que no es una implementación al uso de un enumerado, lo primero es que no se basa en el propio struct enum del lenguaje C#. Si no que es una clase con propiedades estáticas de ella misma y heredando de la clase Enumeration.cs que se ubica en la carpeta de Seedwork, y que por lo tanto explicaremos más adelante el porqué de esta implementación y las ventajas que nos ofrece. A fin de cuentas es tan solo un enumerado que nos facilita la identificación de estados en el código.

Otra apreciación de la te habrás dado cuenta seguramente es del uso de una excepción customizada para notificar de los errores, TicketException.cs:

using System;

namespace TicketManagement.Domain.Aggregates.TicketAggregate
{
    public class TicketException : Exception
    {
        public TicketException(string message) : base(message)
        {
        }
    }
}

Esta es una forma de identificar los errores provenientes de la capa de dominio que yo utilizo bastante, tan solo heredaremos de la clase base usada para las excepciones en C# Exception.cs. Y más adelante en la implementación de la capa de aplicación veremos realmente todo el potencial que nos aporta la simple identificación de estas excepciones.

Seedwork

Si recordamos en ésta carpeta ubicaremos todos las clases base o genéricas que utilizaremos a lo largo de todo nuestro dominio para facilitarnos el trabajar con DDD, digamos que es nuestra caja de herramientas en lo que a programación de la capa de dominio se refiere. Comenzaremos por lo tanto echando un vistazo a la clase Entity.cs, clase que recordemos es utilizada por todas las entidades que tienen constancia en nuestro dominio:

using MediatR;
using System;
using System.Collections.Generic;

namespace TicketManagement.Domain.SeedWork
{
    public abstract class Entity<T> where T : struct
    {
        int? _requestedHashCode;
        T _Id;

        private List<INotification> _domainEvents;

        public virtual T Id
        {
            get
            {
                return _Id;
            }
            protected set
            {
                _Id = value;
            }
        }

        public List<INotification> DomainEvents => _domainEvents;

        public void AddDomainEvent(INotification eventItem)
        {
            _domainEvents = _domainEvents ?? new List<INotification>();
            _domainEvents.Add(eventItem);
        }

        public void RemoveDomainEvent(INotification eventItem)
        {
            if (_domainEvents is null) return;
            _domainEvents.Remove(eventItem);
        }

        public override bool Equals(object obj)
        {
            if (obj == null || !(obj is Entity<T>))
                return false;
            if (Object.ReferenceEquals(this, obj))
                return true;
            if (this.GetType() != obj.GetType())
                return false;
            Entity<T> item = (Entity<T>)obj;

            return item.Id.Equals(this.Id);
        }

        public override int GetHashCode()
        {

            if (!_requestedHashCode.HasValue)
                _requestedHashCode = this.Id.GetHashCode() ^ 31;

            return _requestedHashCode.Value;

        }

        public static bool operator ==(Entity<T> left, Entity<T> right)
        {
            if (Object.Equals(left, null))
                return (Object.Equals(right, null));
            else
                return left.Equals(right);
        }

        public static bool operator !=(Entity<T> left, Entity<T> right)
        {
            return !(left == right);
        }
    }
}

Lo más de destacado de ésta implementación es por un lado el uso de la propiedad genérica Id, usada para establecer un identificador único en cada entidad, valor obligatorio en nuestras entidades de dominio que representan un propio ente en si y por lo tanto deben ser únicas en nuestro sistema. Y por otro lado y siendo además mas importante aún seria el uso de los eventos de dominio o DomainEvents. De momento tan solo nos contentaremos con saber que un evento de dominio es una acción ocurrida dentro de una entidad que necesita ser notificada al exterior para ser capturada y tratada por otro ente, ya que éste debe reaccionar a dicho cambio. No os preocupes, este concepto es lo suficientemente importante como para dedicarle una sección entera y eso es lo que haremos un poquito más adelante 😉

A fin de cuentas esta clase Entity.cs representa una base para una entidad de dominio y nos otorga la capacidad de disparar eventos de dominio. Pasemos con la interfaz IAgregateRoot.cs:

namespace TicketManagement.Domain.SeedWork
{
    public interface IAggregateRoot
    {
    }
}

Ya vemos la complejidad… es simple y llanamente una interfaz que será utilizada para marcar nuestras entidades que son agregados raíz en nuestro dominio, esto es necesario para que en las siguientes capas como la de infraestructura nos aseguremos que los repositorios de persistencia se aplican únicamente sobre éste tipo de entidades, por ejemplo. Y esto nos lo muestra la implementación de la clase IRepository.cs:

namespace TicketManagement.Domain.SeedWork
{
    public interface IRepository<T> where T : IAggregateRoot
    {
        IUnitOfWork UnitOfWork { get; }
    }
}

IUnitOFWork.cs:

using System;
using System.Threading.Tasks;

namespace TicketManagement.Domain.SeedWork
{
    public interface IUnitOfWork : IDisposable
    {
        void Commit();

        Task<int> CommitAsync();

        void RollbackChanges();

    }
}

Esta interfaz se utiliza como base para todos los repositorios de persistencia que utilicemos sobre nuestros agregados raíz, y como vemos también contiene una propiedad de la interfaz IUnitOfWork.cs. Usada ésta para permitirnos aplicar el patrón de unidad de trabajo que veremos cuando estemos tratando la capa de infraestructura. Una cosa que quizá te extrañe ahora mismo es que el uso de las interfaces para los repositorios de persistencia se estén ubicando en la capa de dominio, cuando al principio del artículo mencionábamos que la capa de dominio debería tratar de aplicar el principio de ignorancia de la persistencia e ignorancia de la infraestructura… 🙈

El motivo de ésto es que gracias al uso del contenedor de IoC podemos conectar ambas partes, permitiendo así el uso de nuestras entidades de dominio en la capa de persistencia. Con lo que sí que estaríamos evitando que el dominio conozca aspectos de la persistencia invirtiendo la dependencia y haciendo que la capa de persistencia conozca aspectos del dominio. Restringiendo eso sí y como ya he destacado que únicamente trabajen sobre los agregados raíz.

Veamos ahora la implementación de la clase Enumeration.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace TicketManagement.Domain.SeedWork
{
    public abstract class Enumeration : IComparable
    {
        public string Name { get; private set; }

        public int Id { get; private set; }

        protected Enumeration(int id, string name)
        {
            Id = id;
            Name = name;
        }

        public override string ToString() => Name;

        public static IEnumerable<T> GetAll<T>() where T : Enumeration
        {
            var fields = typeof(T).GetFields(BindingFlags.Public |
                                             BindingFlags.Static |
                                             BindingFlags.DeclaredOnly);

            return fields.Select(f => f.GetValue(null)).Cast<T>();
        }

        public override bool Equals(object obj)
        {
            var otherValue = obj as Enumeration;

            if (otherValue == null)
                return false;

            var typeMatches = GetType().Equals(obj.GetType());
            var valueMatches = Id.Equals(otherValue.Id);

            return typeMatches && valueMatches;
        }

        public int CompareTo(object other) => Id.CompareTo(((Enumeration)other).Id);

        public override int GetHashCode()
        {
            return HashCode.Combine(Name, Id);
        }

    }
}

Si os acordáis antes mencionaba el uso de esta clase en el enumerado de TicketState.cs, y hablamos de que no es para nada una implementación normal del típico enumerado que estamos acostumbrados a ver. El usar esta clase en su lugar es que gracias a esto nos servimos de una implementación basada en una clase y no en un struct de enumeración, enriqueciendo así la flexibilidad en código a la hora de trabajar con enumerados, es decir, es posible crear enumerados de un carácter aún mas complejo y sin romper el significado o la utilidad del struct de enumeración en si. La implementación se centra únicamente en incluir funcionalidad para la comparación y distinción entre los enumerados, sobrescribiendo los métodos Equals() y GetHashCode().

Aun así este uso de la enumeración no es totalmente estricto ni necesario, se puede trabajar con DDD usando los structs de enumeración sin problema alguno, simplemente os muestro otra herramienta más que nos puede venir bien a la hora de programar, y además no quedaría coherente si lo estoy usando en código el no mostraros el detalle de cómo funciona 🙂

Conclusión

Me parece que ahora mismo es un buen punto para terminar, ya que, por un lado la parte que viene ahora y que repasaremos en profundidad es la de incluir el uso de eventos de dominio y cómo incorporarlos en el proceso de persistencia de datos, y además creo que el artículo ya es lo suficientemente grande con conceptos y código que ir asentando. No os preocupéis ya que aún nos quedan cosas por ver en la implementación de esta capa de dominio antes de pasar a las demás, y con muchas líneas de código por delante que iremos evolucionando y perfeccionando, así que no quedaros con que ésta sera la imagen final del proyecto, ni mucho menos!!

Os recuerdo que cualquier sugerencia en el código, pregunta o posible bug(🐛) estaré totalmente encantado de responderos así que no dudéis en comentar!. Dicho esto me despido de todos vosotros y espero que con las mismas ganas y energías de ver como avanza esta serie. Hasta la próxima developers!!