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(); } } |
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:
NET - Unit of Work - Padrão Unidade de ...