DEV Community

André N. Darcie
André N. Darcie

Posted on

Entenda DDD de uma vez por todas, na implementação [PT-BR]

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)