CRUD
Tradicional vs Abordagem DDD - III
![]() |
Neste artigo vou continuar uma comparação com a implementação de um CRUD tradicional e a abordagem do Domain Driven Design. |
Continuando o artigo anterior, vamos fazer a refatoração usando a a bordagem do DDD. Começamos pelo domínio: Vamos definir os agregados, removendo navegações entre eles (usaremos IDs), vamos colocamos regras nas entidades que estão anêmicasa. O Endereco vai se tornar um Value Object, e Total será calculado no domínio. Além disso o EF Core vai se adaptar – usaremos classes de configuração separadas.
Introdução ao Design Estratégico: Delimitando o Sistema
Antes de refatorarmos o código, precisamos entender onde cada peça do sistema
vive. No DDD, não modelamos um "sistema de vendas" como um bloco único. Nós o
dividimos em Bounded Contexts, que são fronteiras lógicas onde os termos e as
regras têm um significado específico.
Para o nosso curso, identificamos
três contextos principais que interagem entre si:
1. Contexto de
Catálogo (Produto e Categoria)
Aqui, o foco é a exposição. O
objetivo é gerenciar o que o cliente vê.
O que importa: Nome, descrição de
marketing, fotos e categorias.
Comportamento: Ativação/desativação de
produtos e organização da vitrine.
| Catalogo ├── Produto └── Categoria |
| Pedidos ├── Pedido ├── ItemPedido └── Pagamento |
3. Contexto Clientes (Cliente e Endereço)
Focado na
identidade e localização.
O que importa: Quem é o comprador e onde ele reside
legalmente.
Observação: O endereço no cadastro do cliente é uma
informação de perfil, enquanto o endereço no pedido é o local de entrega (um
dado histórico que não deve mudar se o cliente se mudar depois).
| Clientes ├── Cliente └── Endereco |
Ao definir essas fronteiras, percebemos que o Pedido não
precisa "conter" o objeto Produto completo,
ele precisa apenas do rastro do produto. Isso elimina o acoplamento excessivo
que vimos no CRUD tradicional.
Nesta refatoração, vamos focar no Contexto
de Pedidos. Veremos como o Pedido se torna o "Rei" (Aggregate Root)
deste contexto, protegendo todas as regras de pagamento e itens, enquanto se
comunica com os outros contextos (Catálogo e Cliente) apenas através de IDs.
Definição de Agregados
Pedido
(Aggregate Root): Inclui ItemPedido, Pagamento, EnderecoEntrega
(Value Object).
Produto: Agregado separado.
Categoria: Agregado separado.
Cliente: Agregado com
Endereco (Value Object).
Value Objects:
EnderecoEntrega: Imutável, parte de Pedido.
Money:
Opcional, para valores monetários (usaremos decimal simples por simplicidade,
mas recomendado para precisão).
Regras claras:
Pedido calcula total internamente, valida invariantes. Sem navegações entre
agregados – use ProdutoId em ItemPedido.
Por que Isso é Melhor?
Não navegar entre agregados evita acoplamento e carrega só o necessário.
O ID
é suficiente, assim ItemPedido guarda ProdutoId e PrecoAtual – preço fixo,
imune a mudanças em Produto.
O domínio não sabe sobre o EF Core, as
Entidades são puras, sem atributos ORM.
Não fica mais complexo, fica mais
correto, sustentável. O DDD não briga com EF Core – adapta os mapeamentos
criados para cada arquivo.
Redesenhando o domínio com DDD
Vamos agora refatorar o mesmo sistema do CRUD tradicional aplicando explicitamente os conceitos do DDD e tornando visível no código onde o acoplamento foi reduzido e onde as regras passaram a reinar.
Entidade: Pedido (Aggregate Root)
public sealed class Pedido
{
private readonly List<ItemPedido> _itens = new();
public Guid Id { get; }
// O Pedido NÃO navega para Cliente apenas guarda a identidade public Guid ClienteId { get; } public Endereco EnderecoEntrega { get; private set; } // Exposição somente leitura da coleção public IReadOnlyCollection<ItemPedido> Itens => _itens.AsReadOnly(); public Money Total { get; private set; }
public StatusPedido Status { get; private set; }
public Pagamento? Pagamento { get; private set; }
public Pedido(Guid clienteId) { Id = Guid.NewGuid(); ClienteId = clienteId; EnderecoEntrega = enderecoEntrega; Status = StatusPedido.Aberto; Total = Money.Zero; } /// Adiciona um item ao pedido respeitando as regras do negócio. public void AdicionarItem(Guid produtoId, Money preco, int quantidade) { // Invariante: pedido pago não pode ser alterado if (Status != StatusPedido.Aberto) throw new InvalidOperationException( "Não é possível alterar um pedido que não está aberto."); // Invariante simples de domínio if (quantidade <= 0) throw new InvalidOperationException( "A quantidade deve ser maior que zero."); var item = new ItemPedido(produtoId, preco, quantidade); _itens.Add(item); RecalcularTotal(); } /// Registra o pagamento do pedido. public void RegistrarPagamento(Money valorPago) { if (Status != StatusPedido.Aberto) throw new InvalidOperationException( "Pedido já foi pago ou cancelado."); if (valorPago != Total) throw new InvalidOperationException( "O valor pago deve ser igual ao total do pedido."); Pagamento = new Pagamento(valorPago); Status = StatusPedido.Pago; } private void RecalcularTotal() { Total = _itens.Aggregate( Money.Zero, (total, item) => total + item.Subtotal); } } |
Enum: StatusPedido
| public enum
StatusPedido { Aberto, Pago, Cancelado } |
A classe Pedido é um excelente exemplo de como o código se transforma ao sair de um modelo anêmico para um modelo rico. Abaixo, destaco os principais recursos táticos do DDD implementados e os motivos técnicos e de negócio por trás de cada um:
1. Encapsulamento da Coleção (private readonly List e
IReadOnlyCollection)
O Recurso: O uso de um campo privado _itens
para manipulação interna e uma propriedade pública Itens que expõe apenas
leitura (AsReadOnly).
O Motivo: No CRUD tradicional,
qualquer um pode fazer um pedido.Itens.Add(novoItem) por fora, ignorando as
regras de cálculo. No DDD, o Aggregate Root (Pedido) deve ter o controle total.
Ao expor apenas leitura, você força o uso do método AdicionarItem, garantindo
que ninguém altere a lista sem passar pelas validações.
2.
Identidade em vez de Navegação (Guid ClienteId)
O Recurso: O
pedido guarda apenas o Guid do cliente, sem a propriedade de navegação public
Cliente Cliente { get; set; }.
O Motivo: Isso define uma Fronteira de
Agregado. Se você navega para o cliente, o seu modelo de persistência tenta
carregar o grafo inteiro do banco de dados. Ao usar apenas o ID, você desacopla
os agregados (Vendas de CRM) e evita o carregamento desnecessário de dados que
não pertencem ao contexto de fechar um pedido.
3. Proteção de
Invariantes (Validações nos Métodos)
O Recurso: As verificações
if (Status != StatusPedido.Aberto) e if (quantidade <= 0).
O Motivo:
Invariantes são regras que não podem ser violadas nunca. No CRUD, essas regras
ficam em Services ou Controllers. Aqui, elas estão dentro da entidade. Isso
garante que o objeto Pedido nunca fique em um estado inválido na memória,
independentemente de quem o esteja chamando. O objeto se "autoprotege".
4. Métodos que Expressam Intenção (Linguagem Ubíqua)
O Recurso: Métodos como AdicionarItem e
RegistrarPagamento em vez de simples setters.
O Motivo: No mundo
real, um pedido não sofre um "Set Status = Pago". Ocorre um evento de negócio: o
pagamento é registrado. Nomear os métodos de acordo com o negócio (Linguagem
Ubíqua) torna o código autoexplicativo para o desenvolvedor e alinhado com o que
o especialista de domínio espera.
5. Estado Mutável Controlado
(private set e métodos privados)
O Recurso: Total
tem um private set e o método RecalcularTotal é privado.
O Motivo: O valor total de um pedido é uma consequência dos itens
adicionados. Ele não deve ser "definido" externamente (pedido.Total = 0).
Ao tornar o set privado e recalcular o total internamente sempre que um item é
adicionado, você elimina o risco de o banco de dados ter um total que não bate
com a soma dos itens.
6. Value Objects (EnderecoEntrega e Money)
O Recurso: O uso do tipo Money em vez de um simples decimal
e de EnderecoEntrega como um Value Object.
O Motivo:
Dinheiro não é apenas um número; ele possui regras (não pode
ser negativo, pode ter moedas diferentes). Ao usar um Value Object, você
encapsula a lógica aritmética (como visto no total + item.Subtotal) e
garante que valores monetários sejam tratados com a precisão e as regras
necessárias em todo o sistema, sem repetir código.
O que NÃO existe no Pedido (e isso é
proposital)
❌ DbContext
❌ Include
❌ Produto Produto
❌ Cliente
Cliente
❌ [ForeignKey]
❌ virtual
“Quando o domínio está certo,
o ORM vira detalhe.”
Value Object Money
public sealed record Money(decimal Valor) { public static Money Zero => new(0); public static Money operator +(Money a, Money b) => new(a.Valor + b.Valor); public static Money operator *(Money money, int quantidade) => new(money.Valor * quantidade); public override string ToString() => Valor.ToString("C");
}
|
Value Object EnderecoEntrega
public sealed record Endereco( string Rua, string Numero, string Cidade, string Estado, string CEP ); |
Entidade: Cliente (Aggregate Root)
Cliente tem endereço atual e podemo mudar ao longo do tempo como Value Object não afeta pedidos antigos:
public sealed class Cliente { public Guid Id { get; } public string Nome { get; private set; } public Endereco Endereco { get; private set; } public Cliente(string nome, Endereco endereco) { Id = Guid.NewGuid(); Nome = nome; Endereco = endereco; } public void AtualizarEndereco(Endereco novoEndereco) { Endereco = novoEndereco; } } |
Entidade: Categoria (Entity dentro do Aggregate Produto)
public sealed class Categoria { public string Nome { get; } public Categoria(string nome) { Nome = nome; } } |
Entidade: Produto (Aggregate Root)
Categoria não tem ID e não tem Repositório pois só existe dentro de Produto.
public sealed class Produto { public Guid Id { get; } public string Nome { get; private set; } public Money Preco { get; private set; } public Categoria Categoria { get; private set; } //*Atenção a este código que pode // gerar acoplamento public Produto(string nome, Money preco, Categoria categoria) { Id = Guid.NewGuid(); Nome = nome; Preco = preco; Categoria = categoria; } public void AlterarPreco(Money novoPreco) { Preco = novoPreco; } } |
Entidade: ItemPedido (Entity interna do Aggregate Pedido)
public sealed class ItemPedido { public Guid ProdutoId { get; } public Money Preco { get; } public int Quantidade { get; } public Money Subtotal => Preco * Quantidade; public ItemPedido(Guid produtoId, Money preco, int quantidade) { if (quantidade <= 0) throw new InvalidOperationException("Quantidade inválida."); ProdutoId = produtoId; Preco = preco; Quantidade = quantidade; } } |
Entidade: Pagamento (parte do Aggregate Pedido)
public sealed class Pagamento { public Money Valor { get; } public DateTime Data { get; } public Pagamento(Money valor) { Valor = valor; Data = DateTime.UtcNow; } } |
Com isso agora temos:
✅ Modelo completo
✅ Código contínuo
✅ Todas
as entidades e Value Objects
✅ Endereço no lugar correto
✅ Regras
protegidas pelo domínio
“No CRUD, o pedido aponta para o cliente. No
DDD, o pedido registra uma decisão do negócio no tempo.”
A seguir veremos como fazer a persistência com EF Core respeitando o DDD.
E estamos
conversados...
"Porque somos feitura sua, criados em Cristo Jesus
para as boas obras, as quais Deus preparou para que andássemos nelas"
Efésios 2:10
Referências:
NET - Unit of Work - Padrão Unidade de ...