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

2. Contexto de Pedidos (Pedido, ItemPedido e Pagamento)

Este é o coração da nossa refatoração. É um contexto focado em transação e integridade.
O que importa: Preço no momento da compra, quantidade, cálculo de impostos, regras de pagamento e garantia de que um pedido pago não seja alterado.

Independência: O "Produto" aqui é apenas uma referência (ID e Preço). Não nos importa a foto ou a descrição detalhada do marketing.

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:


José Carlos Macoratti