Domain Model y CQRS: Modernizando su Arquitectura Delphi
Enterprise Patterns con Dext: Domain Model & CQRS - Cómo aplicar Domain Model y CQRS para modernizar su arquitectura Delphi y preparar su sistema para alta escalabilidad.
En el desarrollo de software corporativo, a menudo caemos en la trampa de usar una sola clase para todo. El mismo objeto valida reglas, mapea a la base de datos, calcula impuestos y sirve a la interfaz de usuario.
El resultado es familiar para todos nosotros: sistemas rígidos, “God Classes” (Clases Dios) y el miedo constante de cambiar cualquier cosa en producción.
Inspirado en los patrones de arquitectura Enterprise (comúnmente vistos en los ecosistemas .NET Core y Java), Dext trae a Delphi la capacidad de aplicar el principio de Separación de Responsabilidades de una manera elegante y performante.
Hoy demostraré una arquitectura híbrida que separa el Estado (Persistencia) del Comportamiento (Reglas de Negocio).
1. El Concepto: Entidades Ligeras vs. Modelos Ricos
Sección titulada «1. El Concepto: Entidades Ligeras vs. Modelos Ricos»El secreto de una arquitectura robusta es aceptar que cómo guardamos los datos es diferente de cómo procesamos los datos.
La Entidad de Persistencia (El Estado)
Sección titulada «La Entidad de Persistencia (El Estado)»Esta clase debe ser ligera, un espejo fiel de la tabla. La base de datos es nuestra “Fuente de la Verdad”: si el dato está grabado allí, asumimos que es íntegro.
Para mantener el código limpio, utilizo el Fluent Mapping de Dext, eliminando la contaminación visual de atributos dentro de la clase de dominio.
// POCO: Puro, ligero y enfocado solo en datos.type TOrderEntity = class private FId: Integer; FStatus: string; FTotal: Currency; FItems: IList<TOrderItemEntity>; public property Id: Integer read FId write FId; property Status: string read FStatus write FStatus; property Total: Currency read FTotal write FTotal; property Items: IList<TOrderItemEntity> read FItems; end;2. El Modelo de Dominio Rico
Sección titulada «2. El Modelo de Dominio Rico»Aquí reside la “gravedad” de las reglas de negocio. El Model no es un mero transportador de datos; es el Guardián de la Integridad. Observe cómo no exponemos setters públicos; la única forma de alterar el estado es a través de métodos que garantizan las reglas.
type TOrderModel = class private FEntity: TOrderEntity; // El target de los datos public constructor Create(Entity: TOrderEntity);
// Comportamientos (Acciones de Negocio) procedure AgregarItem(Producto: TProduct; Cantidad: Integer); procedure EnviarPedido;
// Acceso Read-Only (Encapsulamiento Total) property Entity: TOrderEntity read FEntity; end;
implementation
procedure TOrderModel.EnviarPedido;begin // 1. Validación de Estado if FEntity.Status <> 'Borrador' then raise EDomainError.Create('Solo pedidos en borrador pueden ser enviados.');
// 2. Validación de Invariantes (Reglas "Duras") if FEntity.Total < 500.00 then raise EDomainError.Create('El pedido mínimo para facturación es $ 500.00.');
// 3. Transición de Estado Segura FEntity.Status := 'EsperandoAprobacion';end;
procedure TOrderModel.AgregarItem(Producto: TProduct; Cantidad: Integer);begin // Fail Fast: Impide datos sucios de entrar en el sistema if Cantidad <= 0 then raise EDomainError.Create('Cantidad inválida.');
var Item := TOrderItemEntity.Create; Item.ProductId := Producto.Id; Item.Quantity := Cantidad; Item.UnitPrice := Producto.PrecioVenta;
FEntity.Items.Add(Item);
// Actualiza el total inmediatamente. La entidad nunca queda inconsistente. FEntity.Total := FEntity.Total + (Item.UnitPrice * Cantidad);end;3. Consumo Elegante con Fluent Specifications
Sección titulada «3. Consumo Elegante con Fluent Specifications»La belleza de esta arquitectura se revela en los Endpoints (Controladores). Como separamos las responsabilidades, podemos usar el poder del Specification Pattern (estilo Ardalis) integrado en Dext.
Vea la diferencia brutal entre los dos mundos conviviendo en armonía:
El Endpoint de Lectura (Query)
Sección titulada «El Endpoint de Lectura (Query)»Aquí usamos la sintaxis fluida de Dext. El código es declarativo: decimos qué queremos, no cómo buscar.
// GET /orders/pendingApp.MapGet('/orders/pending', function(Context: THttpContext; Repo: IOrderRepository): IResult begin // "Traiga pedidos donde Status es 'Submitted' Y Total > 1000, // incluyendo los Items, ordenados por Fecha" var Spec := Specification.Where<TOrderEntity>( (OrderEntity.Status = 'Submitted') and (OrderEntity.Total > 1000) ) .Include('Items') .OrderBy(OrderEntity.CreatedAt.Desc);
// El Repository solo ejecuta la especificación. Limpio. var Orders := Repo.ToList(Spec);
Result := Results.Ok(Orders); end);El Endpoint de Escritura (Command)
Sección titulada «El Endpoint de Escritura (Command)»Aquí la disciplina entra en escena. Instanciamos el Model para garantizar que ninguna regla sea violada antes de la persistencia.
// POST /ordersApp.MapPost('/orders', function(Context: THttpContext; Repo: IOrderRepository; Dto: TCreateOrderDto): IResult begin var Entity := TOrderEntity.Create; var Model := TOrderModel.Create(Entity);
try // El Model orquesta las reglas "pesadas" for var Item in Dto.Items do Model.AgregarItem(Item.ProductId, Item.Qty);
Model.EnviarPedido; // Valida estado e invariantes
// Persiste solo si el Model aprobó todo Repo.Add(Model.Entity);
Result := Results.Created(Model.Entity.Id, Model.Entity); except on E: EDomainError do Result := Results.BadRequest(E.Message); end; end);4. Jaque Mate: Testabilidad Unitaria
Sección titulada «4. Jaque Mate: Testabilidad Unitaria»Al adoptar este patrón, ganamos un beneficio que vale oro en proyectos grandes: la capacidad de probar reglas de negocio aisladamente.
En el modelo antiguo (“God Class” acoplada al componente de base de datos), para probar si el “Pedido Mínimo de $ 500.00” funciona, usted necesitaría subir una base de datos, insertar registros y correr el sistema. Lento y frágil.
En esta arquitectura híbrida, TOrderModel es agnóstico a la base de datos. Usted puede escribir una Prueba Unitaria (DUnit/DUnitX) que instancia la entidad en memoria, la pasa al Model y verifica el comportamiento en milisegundos.
procedure TTestOrderModel.TestarValidacionDeMinimo;var Entidad: TOrderEntity; Model: TOrderModel;begin // Arrange: Entidad en memoria, sin base de datos! Entidad := TOrderEntity.Create; Entidad.Total := 100.00; Model := TOrderModel.Create(Entidad);
// Act & Assert Assert.WillRaise(procedure begin Model.EnviarPedido end, EDomainError, 'Debe bloquear pedidos debajo de 500 dólares' );end;Esto garantiza la calidad del software mucho antes de que llegue a producción.
Conclusión
Sección titulada «Conclusión»Adoptar Enterprise Patterns con Dext transforma la ingeniería de su proyecto Delphi. Al separar Estado de Comportamiento, alcanzamos el equilibrio perfecto:
- Lectura: Expresiva y performante con Fluent Specifications.
- Escritura: Segura y blindada con Domain Models.
- Calidad: Testabilidad real fuera de la base de datos.
Dext provee la fundación (ORM, Fluent Mapping, Specifications) para que usted deje de pelear con el código y comience a diseñar arquitecturas robustas.
Este es solo el primer paso. Una vez que su dominio está rico y aislado, nuevas posibilidades se abren: Event Sourcing, colas de mensajería y APIs de altísimo rendimiento. Pero esa es una conversación para nuestro próximo artículo.
Vea el repositorio de Dext en GitHub: https://github.com/cesarliws/dext