DDD - Mantenha suas entidades simples


    Manter as entidades simples vai ajudar o seu código a ficar mais limpo vai simplificar sua arquitetura e tornar o seu código mais fácil de manter

As Entidades infladas (ou bloated entities) são classes de domínio que acumularam lógica excessiva além de sua responsabilidade principal de proteger suas próprias regras internas (invariants).



Elas são um sintoma de design fraco, pois acabam lidando com regras de negócio externas (como verificações de crédito) ou orquestrando a lógica de múltiplos agregados. Isso reduz drasticamente a testabilidade, aumenta o acoplamento do código e viola os princípios do Domain-Driven Design (DDD), tornando o sistema difícil de manter e escala

Consequências de ter entidade infladas:

- Entidades infladas prejudicam sua base de código ao:
- Reduz a testabilidade: Você precisa de mocks para serviços externos.
- Aumenta o acoplamento: Entidades dependem de agregados não relacionados.
- Quebra princípios do DDD: Entidades devem apenas impor suas próprias invariantes.

Além disso, misturar lógica externa em entidades torna o teste de unidade um pesadelo — evite isso isolando regras em serviços ou políticas.

A seguir veremos três motivos que afetam suas entidades:

1- Misturar Lógica Externa: Entidades que chamam serviços externos (por exemplo, verificações de crédito) aumentam o acoplamento.
2- Sobrecarga de Invariantes: Entidades que lidam com regras entre agregados violam os princípios do DDD.
3- Negligenciar a Testabilidade: Entidades infladas exigem mocks complexos, tornando os testes frágeis.

Para ilustrar vamos analisar o seguinte cenário:

Você construiu uma entidade chamada Emprestimo  que começou simples — até que as verificações de crédito e as regras de histórico de empréstimos a transformaram em uma confusão inflada e não testável.

Isso cria pesadelos de manutenção em projetos .NET, mas nem tudo esta perdido...

Veremos a seguir como Serviços de Domínio (Domain Services) e Objetos de Política (Policy Objects) podem simplificar seus modelos de domínio para testabilidade e escalabilidade.

Entidade Emprestimo

public class Emprestimo 
{
    public decimal Valor { get; private set; }             
    public decimal TaxaDeJuros { get; private set; }       
    public DateTime DataDeVencimento { get; private set; } 
    public void Aprovar(decimal valorRequisitado, Cliente cliente) 
    {
        if (valorRequisitado <= 0)
            throw new InvalidOperationException("Valor deve ser maior que zero");
        if (!cliente.TemBomScoreDeCredito()) 
            throw new InvalidOperationException("Score de crédito do cliente muito baixo");
        if (cliente.TemEmprestimosPendentes()) 
            throw new InvalidOperationException("Cliente tem empréstimos pendentes");
        Valor = valorRequisitado; 
    }
}

Os problemas desta entidade:

A entidade Emprestimo está validando regras externas ao seu próprio agregado (regra do Cliente).

A entidade Emprestimo não deveria depender da história de outro agregado (o próprio Cliente ou um histórico de Emprestimos).

A refatoração se justificada pois a entidade esta acoplada a agregados ou serviços externos.

Refatorando o código usando serviços de domínio

Os Serviços de Domínio (Domain Services) são classes stateless (sem estado) usadas em DDD para encapsular a lógica de negócio que não pertence naturalmente a nenhuma Entidade ou Objeto de Valor específico. Eles coordenam operações que envolvem múltiplos agregados ou realizam cálculos importantes no domínio. Seu papel é garantir que as regras de negócio complexas e transversais permaneçam no coração do domínio, mantendo as entidades focadas em suas próprias invariantes.

Sugestão de Melhoria para o Serviço de Domínio

Para manter o Serviço de Domínio (Domain Service) o mais puro possível, ele deve ser uma ferramenta de cálculo/avaliação de regras, e não uma orquestrador de chamadas externas de infraestrutura.

Ideia Central: O ServicoDeAprovacaoDeEmprestimo deve receber os dados necessários já obtidos pela Camada de Aplicação e focar apenas na regra de negócio:

Camada de Aplicação (ServicoDeAplicacaoDeEmprestimo): Responsável por chamar ICreditService para obter o score e o IRepository para obter o histórico de empréstimos.

Domínio (IServicoDeAprovacaoDeEmprestimo): Responsável por calcular a aprovação com base nos dados fornecidos.

// 1. O Contrato do Serviço de Domínio é Puro (Não recebe dependências de infra)
public interface IServicoDeAprovacaoDeEmprestimo 
{
    // Recebe apenas dados já obtidos pela Camada de Aplicação
    bool PodeAprovar(decimal valorRequisitado, bool temBomCredito, bool temEmprestimosPendentes);
}
public class ServicoDeAprovacaoDeEmprestimo : IServicoDeAprovacaoDeEmprestimo
{
    // Apenas lógica pura do domínio
    public bool PodeAprovar(decimal valorRequisitado, bool temBomCredito, bool temEmprestimosPendentes)
    {
        if (valorRequisitado <= 0) return false;
        if (!temBomCredito) return false;
        if (temEmprestimosPendentes) return false;
        return true;
    }
}

Assim podemos inicialmente simplificar a entidade:

public class Emprestimo
{
    public decimal Valor { get; private set; } 
    public bool EstaAprovado { get; private set; } 
    
    internal void Aprovar(decimal valorRequisitado) 
    {
        // Apenas muta o estado interno, mantendo a entidade pura
        Valor = valorRequisitado; 
        EstaAprovado = true; 
    }
}

Este código não esta completo e iremos ajustá-lo adiante mas mostra como a entidade já ficou mais simples.

Como Emprestimo Usa ServicoDeAprovacaoDeEmprestimo (via Camada de Aplicação)

public class ServicoDeAplicacaoDeEmprestimo 
{
    private readonly IServicoDeAprovacaoDeEmprestimo _servicoDeAprovacao;    
    public ServicoDeAplicacaoDeEmprestimo(IServicoDeAprovacaoDeEmprestimo servicoDeAprovacao)
    {
        _servicoDeAprovacao = servicoDeAprovacao;
    }

    public Emprestimo RequisitarEmprestimo(Cliente cliente, decimal valorRequisitado)
    {
        if (!_servicoDeAprovacao.PodeAprovarEmprestimo(cliente, valorRequisitado)) 
            throw new InvalidOperationException("Empréstimo não pode ser aprovado.");       
        var emprestimo = new Emprestimo(); 
        emprestimo.Aprovar(valorRequisitado); 
        return emprestimo; 
    }
}

Elevando o Nível com Objetos de Política

Os objetos de Política (Policy Objects) são um padrão de design em DDD (Domain-Driven Design) usado para lidar com regras de negócio complexas, voláteis ou que envolvem múltiplas condições.

Eles servem como uma alternativa ou um complemento aos Serviços de Domínio quando a lógica principal é uma decisão de sim/não baseada em um conjunto de regras que podem ser alteradas ou combinadas.

Objetos de Política (Policy Objects) são mais adequados quando as regras são muitas, combináveis e propensas a mudanças.

Construindo objetos de política para o nosso exemplo:

public interface IPoliciaDeAprovacaoDeEmprestimo 
{
    bool EstaSatisfeitaPor(Cliente cliente, decimal valorRequisitado); 
}
public class PoliticaDeValorPositivo : IPoliciaDeAprovacaoDeEmprestimo 
{
    public bool EstaSatisfeitaPor(Cliente cliente, decimal valorRequisitado) 
        => valorRequisitado > 0; 
}
public class PoliticaDeBomCredito : IPoliciaDeAprovacaoDeEmprestimo 
{
    private readonly IServicoDeCredito _servicoDeCredito;     
    public PoliticaDeBomCredito(IServicoDeCredito servicoDeCredito) 
                            => _servicoDeCredito = servicoDeCredito; 
    
    public bool EstaSatisfeitaPor(Cliente cliente, decimal valorRequisitado) 
        => _servicoDeCredito.TemBomCredito(cliente);
}
public class PoliticaDeSemEmprestimosPendentes : IPoliciaDeAprovacaoDeEmprestimo 
{
    public bool EstaSatisfeitaPor(Cliente cliente, decimal valorRequisitado) 
        => !cliente.TemEmprestimosPendentes();
}

Combinando Políticas com Composição:

public class PoliticaDeAprovacaoDeEmprestimoComposta : IPoliciaDeAprovacaoDeEmprestimo 
{
    private readonly IEnumerable<IPoliciaDeAprovacaoDeEmprestimo> _politicas;    
    public PoliticaDeAprovacaoDeEmprestimoComposta(IEnumerable<IPoliciaDeAprovacaoDeEmprestimo> politicas) 
    {
        _politicas = politicas; 
    }    
    public bool EstaSatisfeitaPor(Cliente cliente, decimal valorRequisitado) => 
        _politicas.All(p => p.EstaSatisfeitaPor(cliente, valorRequisitado)); 
}

Uso:

var politicas = new List<IPoliciaDeAprovacaoDeEmprestimo> 
{
    new PoliticaDeValorPositivo(), 
    new PoliticaDeBomCredito(servicoDeCredito), 
    new PoliticaDeSemEmprestimosPendentes() 
};
var politicaDeAprovacao = new PoliticaDeAprovacaoDeEmprestimoComposta(politicas); 
if (politicaDeAprovacao.EstaSatisfeitaPor(cliente, valorRequisitado)) 
{
    emprestimo.Aprovar(valorRequisitado); 
}

Use Objetos de Política quando as regras mudarem com frequência, pois são mais fáceis de trocar ou estender sem modificar a lógica central.

Serviços de Domínio vs. Objetos de Política

Aspecto Serviço de Domínio (Domain Service) Objeto de Política (Policy Object)
Escopo Coordena múltiplas entidades ou serviços   Encapsula uma única regra
Granularidade Mais grosseira (Coarse-grained)   Mais fina (Fine-grained)
Reutilização Média   Alta (fácil de conectar/desconectar regras)
Complexidade Centralizada, mas pode crescer muito  Mais classes, mas altamente modular

O uso de Policy Objects é uma abordagem de alto nível para lidar com regras complexas e mutáveis, e a implementação apresentada está tecnicamente correta para este propósito.

Código completo da entidade Emprestimo:

public class Emprestimo 
{
    // Apenas propriedades internas (estado do próprio Empréstimo)
    public Guid Id { get; private set; }
    public Guid ClienteId { get; private set; }
    public decimal ValorRequisitado { get; private set; } 
    public StatusEmprestimo Status { get; private set; } 
    // Construtor: Força a criação inicial com um estado válido
    public Emprestimo(Guid clienteId, decimal valor) 
    {
        if (valor <= 0)
            throw new ArgumentException("O valor do empréstimo deve ser positivo."); 
        
        this.Id = Guid.NewGuid();
        this.ClienteId = clienteId;
        this.ValorRequisitado = valor;
        this.Status = StatusEmprestimo.AguardandoAprovacao;
    }
    // Comportamento (Método) Focado no Domínio Interno
    public void Aprovar() 
    {
        if (this.Status != StatusEmprestimo.AguardandoAprovacao) 
        {
            throw new InvalidOperationException("Não é possível aprovar um empréstimo 
                                                     que não está aguardando."); 
        }
        this.Status = StatusEmprestimo.Aprovado;
    }
    public void Rejeitar() 
    {
        if (this.Status != StatusEmprestimo.AguardandoAprovacao) 
        {
            throw new InvalidOperationException("Não é possível rejeitar um empréstimo 
                                                        que não está aguardando."); 
        }
        this.Status = StatusEmprestimo.Rejeitado;
    }
}

O que mudou e por quê?

A grande mudança é a remoção total de chamadas a serviços externos (IServicoCredito ou IRepository) dentro da entidade.

Antes (Entidade Inflada) Depois (Entidade Pura)
   
Tinha o método TentarAprovar(ICreditService service) Agora tem apenas Aprovar() e Rejeitar()
   
Sabia sobre score de crédito e outros empréstimos. Não sabe de regras externas; apenas muda seu próprio status.
   
Quebrava a regra do Agregado: acoplada a outros agregados e à infraestrutura. Protege suas Invariantes (Valor > 0).
A decisão de aprovação é feita externamente (na camada de Aplicação/Serviço de Domínio).

E estamos conversados...  

"Bendito o Deus e Pai de nosso Senhor Jesus Cristo, o qual nos abençoou com todas as bênçãos espirituais nos lugares celestiais em Cristo;"
Efésios 1:3

Referências:


José Carlos Macoratti