CRUD Tradicional vs Abordagem DDD - V


     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, no DDD, um repositório não é apenas uma classe de acesso a dados (como um DAO). A metáfora correta é: O Repositório simula uma coleção de objetos em memória.

Pense no repositório como uma lista (List<T>) gigante. Você não se preocupa como a lista guarda os dados (se é em disco, em RAM ou via API); você apenas pede o objeto por um identificador ou adiciona um novo.



A Regra de Ouro: Um Repositório por Aggregate Root

Só criamos repositórios para Aggregate Roots e em nosso sistema temos os aggregates roots: Pedido, Cliente e Produto, logo vamos implementar abstrações para estes agregados criando as interfaces IPedidoRepository, IClienteRepository e IProdutoRepository.

Não teremos IItemPedidoRepository. Por quê?

Porque o ItemPedido é controlado pelo aggregate root Pedido. Se você quiser um item, deve buscá-lo através do Pedido. Isso garante que as Invariantes (regras de negócio) sejam respeitadas.

Por que usar Repositórios?

Vou elencar 3 pilares para justificar o uso dos repositórios na abordagem DDD:

Ubiquitous Language (Linguagem Ubíqua): O código de domínio deve falar "negócio". _repository.Add(pedido) soa muito mais como domínio do que _context.Pedidos.Add(pedido) e _context.SaveChanges().

Persistência Ignorante: O seu Aggregate Pedido tem regras complexas (como não mudar após pago). O repositório permite que o Domínio não saiba se os dados estão indo para o SQL Server, MongoDB ou um arquivo JSON.

Testabilidade: É muito mais fácil "mockar" uma interface IPedidoRepository do que tentar simular o comportamento complexo de um DbSet do EF Core em testes unitários.

Como implementar ?

A implementação no DDD geralmente é dividida em duas partes: a Interface (no Domínio) e a Implementação (na Infraestrutura).

Passo A: Definir a Interface

A interface define as intenções de negócio.

public interface IPedidoRepository
{
   Task<Pedido?> ObterPorIdAsync(Guid id);
   Task AdicionarAsync(Pedido pedido); 
   Task AtualizarAsync(Pedido pedido);

}

Passo B: Implementação com EF Core (Camada de Infraestrutura)

Aqui é onde o "trabalho sujo" de banco de dados acontece.

Exemplo de código para ilustrar:

public class PedidoRepository : IPedidoRepository
{
    private readonly VendasContext _context;
    public PedidoRepository(VendasContext context)
    {
        _context = context;
    }
    public async Task<Pedido?> ObterPorIdAsync(Guid id)
    {
        // Importante: No DDD, o repositório deve carregar o Agregado COMPLETO
        return await _context.Pedidos
            .Include(p => p.Itens) // Garante que a coleção de itens venha junto
            .FirstOrDefaultAsync(p => p.Id == id);
    }
    public async Task AdicionarAsync(Pedido pedido)
    {
        await _context.Pedidos.AddAsync(pedido);
    }
}

Onde o Repositório se encaixa no fluxo?

Muitos desenvolvedores confundem Repositório com Service. O fluxo a seguir mostrar o seu papel:

A Application Service recebe um comando (ex: "Adicionar Item ao Pedido").
O Service pede ao Repository o Aggregate Root (Pedido) pelo ID.
O Repository entrega o objeto Pedido com todos os seus Itens carregados.
O Service chama o método de negócio no objeto: pedido.AdicionarItem(...).
O Service chama o Unit of Work (ou o repositório...) para persistir as mudanças.

O repositório NÃO existe para “esconder o EF Core

Ele existe para:

✔ Expressar intenções do domínio
✔ Centralizar regras de acesso a aggregates
✔ Evitar que o Application Service vire “mini-ORM”
✔ Melhorar testabilidade e clareza

“Repositório não é abstração de banco. É abstração de coleção de aggregates.”

Implementação das interfaces dos repositórios

IPedidoRepository

public interface IPedidoRepository
{
   Task<Pedido?> ObterPorIdAsync(Guid id);
   Task AdicionarAsync(Pedido pedido); 
   Task AtualizarAsync(Pedido pedido);

}

Assim, Repositório trabalha com Aggregate Root
Ele Não expõe ItemPedido, Pagamento, etc.
Ele Não retorna IQueryable 

Nota:  Repositórios devem retornar coleções materializadas (como IEnumerable, List, ou o próprio objeto). Retornar IQueryable vaza detalhes de infraestrutura, permitindo que a camada de aplicação faça consultas SQL arbitrárias, o que quebra o encapsulamento e pode causar problemas de performance (N+1)

IProdutoRepository

public interface IProdutoRepository
{
   Task<Produto?> ObterPorIdAsync(Guid id);
   Task AdicionarAsync(Produto produto); 
   Task AtualizarAsync(Produto produto);

}

IClienteRepository

public interface IClienteRepository
{
   Task<Cliente?> ObterPorIdAsync(Guid id);
   Task AdicionarAsync(Cliente cliente); 
   Task AtualizarAsync(Cliente cliente);

}

A seguir uma sugestão de implementação destes repositórios feita na camada Infrastructure:

PedidoRepository.cs

public sealed class PedidoRepository : IPedidoRepository
{
    private readonly VendasContext _contexto;
    public PedidoRepository(ContextoApp contexto)
    {
        _contexto = contexto;
    }
    public async Task<Pedido?> ObterPorIdAsync(Guid id)
    {
        return await _contexto.Pedidos.FindAsync(id);
    }
    public async Task AdicionarAsync(Pedido pedido)
    {
        await _contexto.Pedidos.AddAsync(pedido);
    }
    public async Task SalvarAsync()
    {
        await _contexto.SaveChangesAsync();
    }
}

Observe que Repositório não tem regra de negócio. Ele só carrega e salva aggregates.

ProdutoRepository

public sealed class ProdutoRepository : IProdutoRepository
{
    private readonly VendasContext _contexto;
    public ProdutoRepository(ContextoApp contexto)
    {
        _contexto = contexto;
    }
    public async Task<Produto?> ObterPorIdAsync(Guid id)
    {
        return await _contexto.Produtos.FindAsync(id);
    }
    public async Task AdicionarAsync(Produto produto)
    {
        await _contexto.Produtos.AddAsync(produto);
    }
    public async Task SalvarAsync()
    {
        await _contexto.SaveChangesAsync();
    }
}

ClienteRepository

public sealed class ClienteRepository : IClienteRepository
{
    private readonly ContextoApp _contexto;
    public ClienteRepository(ContextoApp contexto)
    {
        _contexto = contexto;
    }
    public async Task<Cliente?> ObterPorIdAsync(Guid id)
    {
        return await _contexto.Clientes.FindAsync(id);
    }
    public async Task AdicionarAsync(Cliente cliente)
    {
        await _contexto.Clientes.AddAsync(cliente);
    }
    public async Task SalvarAsync()
    {
        await _contexto.SaveChangesAsync();
    }
}

E a título de exemplo vou mostrar a seguir a implementação de um serviço para Pedido :

PedidoAppService.cs

public sealed class PedidoAppService
{
    private readonly IPedidoRepository _pedidoRepo;
    private readonly IProdutoRepository _produtoRepo;
    private readonly IClienteRepository _clienteRepo;
    public PedidoAppService(
        IPedidoRepository pedidoRepo,
        IProdutoRepository produtoRepo,
        IClienteRepository clienteRepo)
    {
        _pedidoRepo = pedidoRepo;
        _produtoRepo = produtoRepo;
        _clienteRepo = clienteRepo;
    }
    public async Task<Guid> CriarPedidoAsync(Guid clienteId)
    {
        var cliente = await _clienteRepo.ObterPorIdAsync(clienteId)
            ?? throw new InvalidOperationException("Cliente não encontrado.");
        var pedido = new Pedido(cliente.Id, cliente.Endereco);
        await _pedidoRepo.AdicionarAsync(pedido);
        await _pedidoRepo.SalvarAsync();
        return pedido.Id;
    }
    public async Task AdicionarItemAsync(
        Guid pedidoId,
        Guid produtoId,
        int quantidade)
    {
        var pedido = await _pedidoRepo.ObterPorIdAsync(pedidoId)
            ?? throw new InvalidOperationException("Pedido não encontrado.");
        var produto = await _produtoRepo.ObterPorIdAsync(produtoId)
            ?? throw new InvalidOperationException("Produto não encontrado.");
        pedido.AdicionarItem(produto.Id, produto.Preco, quantidade);
        await _pedidoRepo.SalvarAsync();
    }
    public async Task RegistrarPagamentoAsync(
        Guid pedidoId,
        decimal valor)
    {
        var pedido = await _pedidoRepo.ObterPorIdAsync(pedidoId)
            ?? throw new InvalidOperationException("Pedido não encontrado.");
        pedido.RegistrarPagamento(new Money(valor));
        await _pedidoRepo.SalvarAsync();
    }
}

Note que o Application Service não conhece EF Core,  ele só conhece interfaces; com isso os testes  ficam simples e o ORM até pode ser trocado se isso realmente for necessário.

 “O DDD não obriga repositório. Ele obriga domínio limpo. O repositório existe para proteger esse domínio.”

E estamos conversados...  

"Miserável homem que eu sou! Quem me livrará do corpo desta morte?
Dou graças a Deus por Jesus Cristo nosso Senhor. Assim que eu mesmo com o entendimento sirvo à lei de Deus, mas com a carne à lei do pecado."
Romanos 7:24,25

Referências:


José Carlos Macoratti