Domain Model e CQRS: Modernizando sua Arquitetura Delphi
Enterprise Patterns com Dext: Domain Model & CQRS - Como aplicar Domain Model e CQRS para modernizar sua arquitetura Delphi e preparar seu sistema para alta escalabilidade.
No desenvolvimento de software corporativo, frequentemente caímos na armadilha de usar uma única classe para tudo. O mesmo objeto valida regras, mapeia o banco de dados, calcula impostos e serve a tela.
O resultado todos conhecemos: sistemas rígidos, classes gigantes (“God Classes”) e o medo constante de alterar qualquer coisa em produção.
Inspirado nos padrões de arquitetura Enterprise (comumente vistos no ecossistema .NET Core e Java), o Dext traz para o Delphi a capacidade de aplicar o princípio da Segregação de Responsabilidades de forma elegante e performática.
Hoje vou demonstrar uma arquitetura híbrida que separa o Estado (Persistência) do Comportamento (Regras de Negócio).
1. O Conceito: Entidades Leves vs. Modelos Ricos
Seção intitulada “1. O Conceito: Entidades Leves vs. Modelos Ricos”O segredo da arquitetura robusta é aceitar que como guardamos o dado é diferente de como processamos o dado.
A Entidade de Persistência (O Estado)
Seção intitulada “A Entidade de Persistência (O Estado)”Esta classe deve ser leve, um espelho fiel da tabela. O banco de dados é a nossa “Fonte da Verdade”: se o dado está gravado lá, assumimos que ele está íntegro.
Para manter o código limpo, utilizo o Fluent Mapping do Dext, eliminando a poluição visual de atributos dentro da classe de domínio.
// POCO: Puro, leve e focado apenas em dados.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. O Modelo de Domínio Rico
Seção intitulada “2. O Modelo de Domínio Rico”Aqui reside a “gravidade” das regras de negócio. O Model não é um mero transportador de dados; ele é o Guardião da Integridade. Observe como não expomos setters públicos; a única forma de alterar o estado é através de métodos que garantem as regras.
type TOrderModel = class private FEntity: TOrderEntity; // O target dos dados public constructor Create(Entity: TOrderEntity);
// Comportamentos (Ações de Negócio) procedure AdicionarItem(Produto: TProduct; Quantidade: Integer); procedure SubmeterPedido;
// Acesso Read-Only (Encapsulamento Total) property Entity: TOrderEntity read FEntity; end;
implementation
procedure TOrderModel.SubmeterPedido;begin // 1. Validação de Estado if FEntity.Status <> 'Rascunho' then raise EDomainError.Create('Apenas pedidos em rascunho podem ser submetidos.');
// 2. Validação de Invariantes (Regras "Duras") if FEntity.Total < 500.00 then raise EDomainError.Create('O pedido mínimo para faturamento é R$ 500,00.');
// 3. Transição de Estado Segura FEntity.Status := 'AguardandoAprovacao';end;
procedure TOrderModel.AdicionarItem(Produto: TProduct; Quantidade: Integer);begin // Fail Fast: Impede dados sujos de entrarem no sistema if Quantidade <= 0 then raise EDomainError.Create('Quantidade inválida.');
var Item := TOrderItemEntity.Create; Item.ProductId := Produto.Id; Item.Quantity := Quantidade; Item.UnitPrice := Produto.PrecoVenda;
FEntity.Items.Add(Item);
// Atualiza o total imediatamente. A entidade nunca fica inconsistente. FEntity.Total := FEntity.Total + (Item.UnitPrice * Quantidade);end;3. Consumo Elegante com Fluent Specifications
Seção intitulada “3. Consumo Elegante com Fluent Specifications”A beleza desta arquitetura se revela nos Endpoints (Controllers). Como separamos as responsabilidades, podemos usar o poder do Specification Pattern (estilo Ardalis) integrado ao Dext.
Veja a diferença brutal entre os dois mundos convivendo em harmonia:
O Endpoint de Leitura (Query)
Seção intitulada “O Endpoint de Leitura (Query)”Aqui usamos a sintaxe fluente do Dext. O código é declarativo: dizemos o que queremos, não como buscar.
// GET /orders/pendingApp.MapGet('/orders/pending', function(Context: THttpContext; Repo: IOrderRepository): IResult begin // "Traga pedidos onde Status é 'Submitted' E Total > 1000, // incluindo os Itens, ordenados por Data" var Spec := Specification.Where<TOrderEntity>( (OrderEntity.Status = 'Submitted') and (OrderEntity.Total > 1000) ) .Include('Items') .OrderBy(OrderEntity.CreatedAt.Desc);
// O Repository apenas executa a especificação. Limpo. var Orders := Repo.ToList(Spec);
Result := Results.Ok(Orders); end);O Endpoint de Escrita (Command)
Seção intitulada “O Endpoint de Escrita (Command)”Aqui a disciplina entra em cena. Instanciamos o Model para garantir que nenhuma regra seja violada antes da persistência.
// POST /ordersApp.MapPost('/orders', function(Context: THttpContext; Repo: IOrderRepository; Dto: TCreateOrderDto): IResult begin var Entity := TOrderEntity.Create; var Model := TOrderModel.Create(Entity);
try // O Model orquestra as regras "pesadas" for var Item in Dto.Items do Model.AdicionarItem(Item.ProductId, Item.Qty);
Model.SubmeterPedido; // Valida estado e invariantes
// Persiste apenas se o Model aprovou tudo 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. O Cheque-Mate: Testabilidade Unitária
Seção intitulada “4. O Cheque-Mate: Testabilidade Unitária”Ao adotar este padrão, ganhamos um benefício que vale ouro em projetos grandes: a capacidade de testar regras de negócio isoladamente.
No modelo antigo (“God Class” acoplada ao componente de banco), para testar se o “Pedido Mínimo de R$ 500,00” funciona, você precisaria subir um banco de dados, inserir registros e rodar o sistema. Lento e frágil.
Nesta arquitetura híbrida, o TOrderModel é agnóstico ao banco. Você pode escrever um Teste Unitário (DUnit/DUnitX) que instancia a entidade em memória, passa para o Model e verifica o comportamento em milissegundos.
procedure TTestOrderModel.TestarValidacaoDeMinimo;var Entidade: TOrderEntity; Model: TOrderModel;begin // Arrange: Entidade em memória, sem banco de dados! Entidade := TOrderEntity.Create; Entidade.Total := 100.00; Model := TOrderModel.Create(Entidade);
// Act & Assert Assert.WillRaise(procedure begin Model.SubmeterPedido end, EDomainError, 'Deve bloquear pedidos abaixo de 500 reais' );end;Isso garante a qualidade do software muito antes dele chegar em produção.
Conclusão
Seção intitulada “Conclusão”Adotar Enterprise Patterns com Dext transforma a engenharia do seu projeto Delphi. Ao separar Estado de Comportamento, atingimos o equilíbrio perfeito:
- Leitura: Expressiva e performática com Fluent Specifications.
- Escrita: Segura e blindada com Domain Models.
- Qualidade: Testabilidade real fora do banco de dados.
O Dext fornece a fundação (ORM, Fluent Mapping, Specifications) para que você pare de brigar com o código e comece a desenhar arquiteturas robustas.
Este é apenas o primeiro passo. Uma vez que seu domínio está rico e isolado, novas possibilidades se abrem: Event Sourcing, filas de mensageria e APIs de altíssima performance. Mas essa é uma conversa para o nosso próximo artigo.
Veja o repositório do Dext no GitHub: https://github.com/cesarliws/dext