Ir al contenido

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.

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;

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:

Aquí usamos la sintaxis fluida de Dext. El código es declarativo: decimos qué queremos, no cómo buscar.

// GET /orders/pending
App.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);

Aquí la disciplina entra en escena. Instanciamos el Model para garantizar que ninguna regla sea violada antes de la persistencia.

// POST /orders
App.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);

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.

Adoptar Enterprise Patterns con Dext transforma la ingeniería de su proyecto Delphi. Al separar Estado de Comportamiento, alcanzamos el equilibrio perfecto:

  1. Lectura: Expresiva y performante con Fluent Specifications.
  2. Escritura: Segura y blindada con Domain Models.
  3. 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