Introdução
O Domain-Driven Design (DDD), conforme definido no livro original Domain-Driven Design: Tackling Complexity in the Heart of Software, de Eric Evans, é uma abordagem para o desenvolvimento de software focada na modelagem do domínio central do negócio. Este post mostra uma implementação fiel aos conceitos do livro em um projeto de pagamentos usando C#.
O que o DDD é
- ✅ Modelagem focada no domínio do negócio.
- ✅ Comunicação clara entre desenvolvedores e especialistas do domínio.
- ✅ Uso da Linguagem Onipresente (Ubiquitous Language).
- ✅ Separação clara de responsabilidades.
- ✅ Regras de negócio encapsuladas no domínio.
- ✅ Limites explícitos entre diferentes contextos (Bounded Contexts).
O que o DDD não é
- ❌ Não é necessário para sistemas simples.
- ❌ Não é sobre focar em detalhes técnicos.
- ❌ Não é apenas uma divisão em camadas.
- ❌ Não é obrigatório usar repositórios ou serviços para tudo.
- ❌ Não é uma metodologia rígida, mas uma abordagem de design.
1. Conceitos Fundamentais do DDD
- Bounded Contexts: Define o limite explícito de cada parte do sistema, garantindo que cada contexto tenha seu próprio modelo e linguagem.
- Entidades: Representam objetos com identidade única.
- Value Objects: Representam conceitos do domínio sem identidade própria.
- Agregados: Conjunto de entidades e objetos de valor que devem ser tratados como uma unidade.
- Repositórios: Fornecem um acesso controlado aos agregados.
- Serviços de Domínio: Contêm operações que não pertencem a uma única entidade.
- Fábricas: Simplificam a criação de objetos complexos.
1.1 Linguagem Onipresente (Ubiquitous Language)
Antes de ir para a implementação, é essencial definir a Linguagem Onipresente para garantir uma comunicação clara entre desenvolvedores e especialistas do domínio. No sistema de pagamentos, que vamos implementar apresenta conceitos como Pedido de Pagamento (conjunto de pagamentos de um cliente), Pagamento (transferência de valor) e Método de Pagamento (forma de pagamento, como "Cartão de Crédito" ou "PIX") são usados diretamente nas classes PedidoDePagamento, Pagamento e MetodoPagamento.
Para isso, os desenvolvedores devem colaborar com equipes como o Comercial, para entender os tipos de transações e métodos de pagamento aceitos; o Financeiro, para garantir que as regras de cálculo e validação dos valores estejam corretas; e o Atendimento ao Cliente, para alinhar mensagens de erro claras, como "O valor do pagamento deve ser maior que zero". Essa colaboração garante que o sistema reflita a realidade do negócio.
1.2 DDD Não é Sobre Arquitetura, é Sobre o Domínio
Embora o DDD influencie a estrutura do código, ele não é sobre arquitetura, mas sim sobre compreender e modelar o domínio do negócio. Seu foco principal é capturar a lógica e os processos do domínio de forma clara e coesa, garantindo que o software reflita as necessidades da empresa. A arquitetura é apenas um meio de implementar esses conceitos, mas o verdadeiro valor do DDD está em alinhar a linguagem e o modelo do sistema com a realidade do negócio.
1.3 Mapa de Contexto (Context Map)
O Mapa de Contexto é uma ferramenta essencial no DDD, utilizada para visualizar como os diferentes Bounded Contexts interagem entre si. Ele define as relações, dependências e formas de comunicação entre os contextos, garantindo que o sistema mantenha a coesão e a integridade do domínio.
No sistema de pagamentos, por exemplo, o contexto de Pedidos de Pagamento poderia se relacionar com o contexto de Gestão Financeira para validar transações e gerar relatórios, enquanto o contexto de Atendimento ao Cliente acessa informações de pedidos para prestar suporte. Essas interações devem ser documentadas em um Mapa de Contexto, utilizando termos da Linguagem Onipresente, facilitando a comunicação entre equipes e garantindo que o design do sistema esteja alinhado às operações do negócio.
Para este exemplo, focaremos apenas no contexto de Pedidos de Pagamento para simplificar a implementação e destacar os principais conceitos do DDD sem a complexidade das interações entre múltiplos contextos. Essa abordagem facilita o entendimento inicial, permitindo aprofundar-se em mapas de contexto em cenários mais avançados.
1.4 Simplicidade e Clareza
Por fim, o DDD valoriza a simplicidade e a clareza do domínio, evitando abstrações desnecessárias e focando no que realmente importa para o negócio. No contexto de Pedidos de Pagamento, isso significa usar uma estrutura direta, com poucas camadas e apenas os componentes necessários para manter o domínio coeso. Repositórios, fábricas e serviços devem ser utilizados apenas quando agregam valor ao design.
2. Implementação
2.0 Entidade Pagamento
A classe Pagamento é uma entidade que possui um identificador único (Id), o qual garante sua distinção dentro do agregado. O construtor da entidade assegura a consistência do domínio ao exigir um valor positivo e um objeto válido do Value Object MetodoPagamento, evitando a criação de objetos em estado inválido. A propriedade Valor representa o montante da transação, enquanto a propriedade Metodo incorpora o Value Object que descreve o método de pagamento utilizado. Como parte do agregado PedidoDePagamento, a entidade Pagamento não pode existir fora do contexto desse agregado, garantindo que todas as operações relacionadas ao seu ciclo de vida sejam controladas pelo Aggregate Root.
public class Pagamento
{
public Guid Id { get; private set; }
public decimal Valor { get; private set; }
public MetodoPagamento Metodo { get; private set; }
internal Pagamento(decimal valor, MetodoPagamento metodo)
{
if (valor <= 0)
throw new ArgumentException("O valor do pagamento deve ser maior que zero.");
Id = Guid.NewGuid();
Valor = valor;
Metodo = metodo ?? throw new ArgumentNullException(nameof(metodo));
}
}
2.1 Entidade PedidoDePagamento
A classe PedidoDePagamento é um Aggregate Root que possui um identificador único (Id), controla a adição de objetos do tipo Pagamento através do método AdicionarPagamento, e expõe apenas uma coleção de leitura (IReadOnlyCollection), garantindo que os pagamentos só possam ser modificados dentro do próprio agregado, mantendo a integridade do domínio.
public class PedidoDePagamento
{
public Guid Id { get; private set; }
private readonly List<Pagamento> _pagamentos;
public IReadOnlyCollection<Pagamento> Pagamentos => _pagamentos.AsReadOnly();
internal PedidoDePagamento()
{
Id = Guid.NewGuid();
_pagamentos = new List<Pagamento>();
}
internal void AdicionarPagamento(decimal valor, MetodoPagamento metodo)
{
var pagamento = new Pagamento(valor, metodo);
_pagamentos.Add(pagamento);
}
}
2.2 Value Object MetodoPagamento
A classe MetodoPagamento é um Value Object, não possui um identificador único, sendo caracterizada pelo valor da propriedade Nome, cuja imutabilidade é garantida pelo modificador somente leitura (get). Sua consistência é assegurada pelo construtor, que impede a criação de objetos com valores nulos ou em branco, garantindo que o objeto esteja sempre em um estado válido. Além disso, a classe existe apenas para descrever o meio de pagamento dentro de um contexto maior, como o agregado PedidoDePagamento, evidenciando que seu valor é mais relevante do que sua identidade.
public sealed class MetodoPagamento
{
public string Nome { get; }
public MetodoPagamento(string nome)
{
Nome = string.IsNullOrWhiteSpace(nome)
? throw new ArgumentNullException(nameof(nome), "Método de pagamento não pode ser nulo ou vazio.")
: nome;
}
}
2.3 Fábrica PedidoDePagamentoFactory
Obs: Segundo o DDD, as fábricas são necessárias apenas para a criação de objetos complexos. Como PedidoDePagamento não é tão complexo, a fábrica pode ser dispensada, adicionei apenas como exemplo.
A classe PedidoDePagamentoFactory é uma Factory expõe o método estático CriarPedidoDePagamento, que simplifica o processo de criação de um PedidoDePagamento ao inicializar a entidade com um pagamento já adicionado. Esse método garante que o objeto seja criado em um estado válido, utilizando a classe MetodoPagamento para validar o nome do método de pagamento, promovendo a coesão e ocultando detalhes de implementação do processo de construção.
public static class PedidoDePagamentoFactory
{
public static PedidoDePagamento CriarPedidoDePagamento(decimal valor, string metodo)
{
var pedido = new PedidoDePagamento();
pedido.AdicionarPagamento(valor, new MetodoPagamento(metodo));
return pedido;
}
}
2.4 Repositório IPedidoDePagamentoRepository
O Repositório é um padrão utilizado para abstrair o acesso aos dados, fornecendo uma interface que permite recuperar e armazenar objetos de domínio sem expor os detalhes da infraestrutura. A interface IPedidoDePagamentoRepository é um repositório que define o contrato para a persistência da entidade PedidoDePagamento, garantindo que o domínio permaneça independente da tecnologia de armazenamento. O método Adicionar permite incluir um novo pedido no repositório, enquanto o método ObterPorId possibilita a recuperação de um pedido existente a partir do seu identificador único (Guid), assegurando o encapsulamento do acesso aos dados e mantendo a coesão do domínio.
public interface IPedidoDePagamentoRepository
{
void Adicionar(PedidoDePagamento pedido);
PedidoDePagamento ObterPorId(Guid id);
}
2.5 Serviço de Domínio ProcessadorDePedido
Obs: segundo o DDD, o serviço de domínio deve conter lógica que não pode ser atribuída a nenhuma entidade. Se o ProcessadorDePedido apenas delega para a fábrica e repositório, pode ser desnecessário, adicionei apenas como parte do exemplo, assim como a Factory.
A classe ProcessadorDePedido é um serviço de domínio que coordena o processo de criação e persistência de um PedidoDePagamento, sem violar o princípio de responsabilidade única. Ela recebe uma instância de IPedidoDePagamentoRepository via injeção de dependência, promovendo a desacoplagem entre o domínio e a infraestrutura de dados. O método Processar utiliza a Factory PedidoDePagamentoFactory para garantir a criação consistente do pedido, e em seguida delega ao repositório a responsabilidade de armazená-lo.
public class ProcessadorDePedido
{
private readonly IPedidoDePagamentoRepository _repository;
public ProcessadorDePedido(IPedidoDePagamentoRepository repository)
{
_repository = repository;
}
public void Processar(decimal valor, string metodo)
{
var pedido = PedidoDePagamentoFactory.CriarPedidoDePagamento(valor, metodo);
_repository.Adicionar(pedido);
}
}
2.6 DbContext PagamentosDbContext
A classe PagamentosDbContext define os conjuntos de dados para as entidades PedidoDePagamento e Pagamento, permitindo operações de leitura e gravação no banco. O método OnModelCreating garante o mapeamento correto das entidades, definindo as chaves primárias com o método HasKey, assegurando a integridade dos dados no nível do banco. O Value Object MetodoPagamento é configurado como um Owned Type, de acordo com os princípios do DDD, garantindo que ele não possua uma tabela própria, mas seja armazenado junto à entidade Pagamento. Além disso, o construtor recebe as opções de configuração necessárias, promovendo a flexibilidade e a integração com diferentes provedores de banco de dados, enquanto mantém o domínio desacoplado da infraestrutura.
public class PagamentosDbContext : DbContext
{
public PagamentosDbContext(DbContextOptions<PagamentosDbContext> options) : base(options) {}
public DbSet<PedidoDePagamento> PedidosDePagamento { get; set; }
public DbSet<Pagamento> Pagamentos { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PedidoDePagamento>().HasKey(p => p.Id);
modelBuilder.Entity<Pagamento>().HasKey(p => p.Id);
modelBuilder.Entity<Pagamento>().OwnsOne(p => p.Metodo, metodo =>
{
metodo.Property(m => m.Nome)
.IsRequired()
.HasMaxLength(100)
.HasColumnName("MetodoPagamentoNome");
});
base.OnModelCreating(modelBuilder);
}
}
2.7 Repositório Concreto PedidoDePagamentoRepository
A classe PedidoDePagamentoRepository é uma implementação concreta da interface IPedidoDePagamentoRepository, utilizando o PagamentosDbContext para realizar as operações de acesso ao banco de dados. O método Adicionar insere uma nova instância de PedidoDePagamento no contexto e salva as alterações no banco de dados através do método SaveChanges, garantindo a persistência imediata. O método ObterPorId busca uma instância de PedidoDePagamento com base no identificador único (Guid), utilizando o método Include para carregar a coleção de pagamentos associados, assegurando que o agregado seja recuperado em sua totalidade, conforme o princípio da consistência transacional do DDD. Dessa forma, o repositório mantém a integridade do domínio ao isolar os detalhes da persistência, promovendo um código mais limpo e coeso.
public class PedidoDePagamentoRepository : IPedidoDePagamentoRepository
{
private readonly PagamentosDbContext _context;
public PedidoDePagamentoRepository(PagamentosDbContext context)
{
_context = context;
}
public void Adicionar(PedidoDePagamento pedido)
{
_context.PedidosDePagamento.Add(pedido);
_context.SaveChanges();
}
public PedidoDePagamento ObterPorId(Guid id)
{
return _context.PedidosDePagamento.Include(p => p.Pagamentos).FirstOrDefault(p => p.Id == id);
}
}
3. Estutura final da implementação
A camada Domain representa o núcleo do negócio, contendo as entidades Pagamento e PedidoDePagamento, o Value Object MetodoPagamento, a interface do repositório IPedidoDePagamentoRepository, o serviço de domínio ProcessadorDePedido e a fábrica PedidoDePagamentoFactory para a criação consistente dos agregados. Já a camada Infrastructure implementa os detalhes técnicos de persistência dos dados usando o EF Core, com o contexto PagamentosDbContext para configurar o banco de dados e o repositório concreto PedidoDePagamentoRepository, que realiza a interação com o banco seguindo o contrato definido na camada de domínio. Essa organização garante um sistema modular, coeso e de fácil manutenção, com o domínio livre de dependências externas.
PagamentoProjeto
│
├── PagamentoContext
│ ├── Domain
│ │ ├── Aggregates
│ │ │ ├── PedidoDePagamento
│ │ │ │ ├── PedidoDePagamento.cs
│ │ │ │ ├── Pagamento.cs
│ │ │ │ └── ValueObjects
│ │ │ │ └── MetodoPagamento.cs
│ │ ├── Factories
│ │ │ └── PedidoDePagamentoFactory.cs
│ │ ├── Repositories
│ │ │ └── IPedidoDePagamentoRepository.cs
│ │ └── Services
│ │ └── ProcessadorDePedido.cs
│ │
│ └── Infrastructure
│ ├── Data
│ │ ├── Context
│ │ │ └── PagamentosDbContext.cs
│ │ └── Repositories
│ │ └── PedidoDePagamentoRepository.cs
3. O Que Não Foi Utilizado do DDD Nesta Implementação
Essa implementação optou por não utilizar alguns elementos que, embora recomendados, não eram essenciais para o exemplo apresentado. Não foram aplicados os conceitos de Domínio Rico (Rich Domain) em sua plenitude, nem o uso extensivo de Domain Events, que são úteis para notificar outras partes do sistema sobre mudanças no domínio. A camada de Application Services, que poderia coordenar a interação entre o domínio e a interface do usuário, também foi omitida para manter a simplicidade. Além disso, não foi abordado o uso de Anti-Corruption Layers para integração com sistemas externos, nem o padrão Specification, que ajuda a encapsular regras de negócio reutilizáveis. Essas ausências, no entanto, não comprometem o entendimento dos fundamentos do DDD, pois o foco do post foi demonstrar a aplicação prática dos elementos mais essenciais da abordagem.
4. Conclusão
Em suma, esta implementação exemplifica de forma clara e prática como aplicar os princípios do Domain-Driven Design (DDD) conforme definidos no livro original de Eric Evans. A organização modular do código, com entidades, Value Objects, agregados, repositórios e serviços de domínio, demonstra como esses componentes colaboram para manter a integridade do domínio e a coesão do sistema. Além disso, a separação entre as camadas de domínio e infraestrutura garante um design desacoplado, facilitando a manutenção e evolução do projeto. Dessa forma, a abordagem apresentada mostra como o DDD pode contribuir para o desenvolvimento de sistemas mais robustos, compreensíveis e alinhados às necessidades do negócio.
Top comments (0)