DDD: Specification Pattern - Encapsulando regras de negócio complexas (Parte 11)

DDD10 min de leitura

Este é o décimo primeiro artigo da série sobre Domain-Driven Design. Nos artigos anteriores exploramos padrões de integração e proteção do modelo de domínio. Agora vamos mergulhar em um padrão fundamental para lidar com regras de negócio complexas: o Specification Pattern.

O Specification Pattern é uma ferramenta poderosa que nos permite encapsular lógica de domínio complexa de forma elegante e reutilizável. É especialmente útil quando lidamos com combinações intrincadas de critérios e regras condicionais que, sem uma estrutura adequada, podem rapidamente se tornar código confuso e difícil de manter.

O que é Specification Pattern?

O Specification Pattern é um padrão de design que encapsula regras de negócio em objetos reutilizáveis que podem ser facilmente combinados usando operadores lógicos. Eric Evans descreve no livro original de DDD:

"Uma especificação é um predicado que determina se um objeto satisfaz alguns critérios."

Este padrão resolve três categorias principais de problemas no design de software:

Validação: Verificar se um objeto atende a determinados critérios antes de uma operação Seleção: Filtrar conjuntos de objetos baseados em regras específicas Construção: Especificar como criar objetos que satisfaçam determinados requisitos

O Contexto do Problema

Imagine um sistema de e-commerce onde você precisa determinar quais produtos são elegíveis para uma promoção sazonal. As regras podem incluir: produto deve estar ativo, ter estoque mínimo, pertencer a categorias específicas, estar numa faixa de preço, não ter estado em promoção recentemente, e assim por diante.

Sem uma estrutura adequada, essas regras frequentemente acabam espalhadas pelo código, duplicadas em vários lugares, difíceis de testar isoladamente e praticamente impossíveis de combinar de forma flexível.

O Problema: Regras de Negócio Espalhadas

Vamos examinar como essas regras complexas tipicamente aparecem no código quando não usamos o Specification Pattern. Este exemplo ilustra os problemas comuns que enfrentamos:

<?php

// ❌ PROBLEMA: Lógica espalhada e difícil de manter
class ProdutoService
{
    private ProdutoRepository $produtoRepository;
    
    public function __construct(ProdutoRepository $produtoRepository)
    {
        $this->produtoRepository = $produtoRepository;
    }
    
    public function buscarProdutosElegiveisPromocao(): array
    {
        $produtos = $this->produtoRepository->buscarTodos();
        $produtosElegiveis = [];
        
        foreach ($produtos as $produto) {
            // Regra 1: Produto deve estar ativo
            if (!$produto->isAtivo()) {
                continue;
            }
            
            // Regra 2: Deve ter estoque mínimo de 10 unidades
            if ($produto->getQuantidadeEstoque() <= 10) {
                continue;
            }
            
            // Regra 3: Categoria deve ser eletrônicos ou casa
            $categoriasElegiveis = ['eletronicos', 'casa'];
            if (!in_array(strtolower($produto->getCategoria()->getNome()), $categoriasElegiveis)) {
                continue;
            }
            
            // Regra 4: Preço entre R$ 50 e R$ 500
            $preco = $produto->getPreco()->getValor();
            if ($preco < 50 || $preco > 500) {
                continue;
            }
            
            // Regra 5: Não pode estar em promoção há menos de 30 dias
            if ($produto->getUltimaPromocao() !== null) {
                $diasSemPromocao = $produto->getUltimaPromocao()->diff(new DateTime())->days;
                if ($diasSemPromocao < 30) {
                    continue;
                }
            }
            
            $produtosElegiveis[] = $produto;
        }
        
        return $produtosElegiveis;
    }
    
    public function validarProdutoParaPromocao(Produto $produto): bool
    {
        // ❌ PROBLEMA: Duplicação da mesma lógica!
        if (!$produto->isAtivo()) {
            return false;
        }
        
        if ($produto->getQuantidadeEstoque() <= 10) {
            return false;
        }
        
        // ... repetir todas as regras novamente
        return true;
    }
}

Este código apresenta vários problemas sérios:

  • Duplicação: As mesmas regras aparecem em múltiplos métodos
  • Dificuldade de teste: É difícil testar regras individuais isoladamente
  • Baixa flexibilidade: Combinar regras de formas diferentes requer reescrever código
  • Violação do princípio Single Responsibility: O service está lidando com múltiplas responsabilidades

A Solução: Implementando Specification Pattern

O Specification Pattern resolve estes problemas encapsulando cada regra em uma classe específica que pode ser facilmente testada, reutilizada e combinada. Vamos implementar a solução em PHP:

1. Interface Base da Specification

Primeiro, criamos a interface que define o contrato básico de uma specification. Esta interface deve permitir tanto verificar se um objeto satisfaz os critérios quanto combinar specifications:

<?php

interface Specification
{
    public function isSatisfiedBy($item): bool;
    public function and(Specification $other): Specification;
    public function or(Specification $other): Specification;
    public function not(): Specification;
}

2. Classe Base Abstrata

Em seguida, criamos uma classe abstrata que implementa os operadores lógicos, permitindo que as specifications concretas foquem apenas em sua lógica específica:

<?php

abstract class AbstractSpecification implements Specification
{
    abstract public function isSatisfiedBy($item): bool;
    
    public function and(Specification $other): Specification
    {
        return new AndSpecification($this, $other);
    }
    
    public function or(Specification $other): Specification
    {
        return new OrSpecification($this, $other);
    }
    
    public function not(): Specification
    {
        return new NotSpecification($this);
    }
}

3. Specifications Específicas para Regras de Negócio

Agora implementamos cada regra de negócio como uma specification específica. Cada classe encapsula uma responsabilidade única e bem definida:

<?php

class ProdutoAtivoSpecification extends AbstractSpecification
{
    public function isSatisfiedBy($produto): bool
    {
        if (!$produto instanceof Produto) {
            throw new InvalidArgumentException('Item deve ser uma instância de Produto');
        }
        
        return $produto->isAtivo();
    }
}

class ProdutoComEstoqueSpecification extends AbstractSpecification
{
    private int $estoqueMinimo;
    
    public function __construct(int $estoqueMinimo = 10)
    {
        $this->estoqueMinimo = $estoqueMinimo;
    }
    
    public function isSatisfiedBy($produto): bool
    {
        if (!$produto instanceof Produto) {
            throw new InvalidArgumentException('Item deve ser uma instância de Produto');
        }
        
        return $produto->getQuantidadeEstoque() > $this->estoqueMinimo;
    }
}

class ProdutoCategoriaSpecification extends AbstractSpecification
{
    private array $categoriasPermitidas;
    
    public function __construct(array $categoriasPermitidas)
    {
        $this->categoriasPermitidas = array_map('strtolower', $categoriasPermitidas);
    }
    
    public function isSatisfiedBy($produto): bool
    {
        if (!$produto instanceof Produto) {
            throw new InvalidArgumentException('Item deve ser uma instância de Produto');
        }
        
        $categoriaProduto = strtolower($produto->getCategoria()->getNome());
        return in_array($categoriaProduto, $this->categoriasPermitidas);
    }
}

class ProdutoFaixaPrecoSpecification extends AbstractSpecification
{
    private float $precoMinimo;
    private float $precoMaximo;
    
    public function __construct(float $precoMinimo, float $precoMaximo)
    {
        if ($precoMinimo > $precoMaximo) {
            throw new InvalidArgumentException('Preço mínimo não pode ser maior que preço máximo');
        }
        
        $this->precoMinimo = $precoMinimo;
        $this->precoMaximo = $precoMaximo;
    }
    
    public function isSatisfiedBy($produto): bool
    {
        if (!$produto instanceof Produto) {
            throw new InvalidArgumentException('Item deve ser uma instância de Produto');
        }
        
        $preco = $produto->getPreco()->getValor();
        return $preco >= $this->precoMinimo && $preco <= $this->precoMaximo;
    }
}

class ProdutoSemPromocaoRecenteSpecification extends AbstractSpecification
{
    private int $diasMinimos;
    
    public function __construct(int $diasMinimos = 30)
    {
        $this->diasMinimos = $diasMinimos;
    }
    
    public function isSatisfiedBy($produto): bool
    {
        if (!$produto instanceof Produto) {
            throw new InvalidArgumentException('Item deve ser uma instância de Produto');
        }
        
        $ultimaPromocao = $produto->getUltimaPromocao();
        
        if ($ultimaPromocao === null) {
            return true;
        }
        
        $diasSemPromocao = $ultimaPromocao->diff(new DateTime())->days;
        return $diasSemPromocao >= $this->diasMinimos;
    }
}

4. Implementação dos Operadores Lógicos

Para permitir combinações flexíveis das specifications, implementamos os operadores lógicos básicos:

<?php

class AndSpecification extends AbstractSpecification
{
    private Specification $left;
    private Specification $right;
    
    public function __construct(Specification $left, Specification $right)
    {
        $this->left = $left;
        $this->right = $right;
    }
    
    public function isSatisfiedBy($item): bool
    {
        return $this->left->isSatisfiedBy($item) && $this->right->isSatisfiedBy($item);
    }
}

class OrSpecification extends AbstractSpecification
{
    private Specification $left;
    private Specification $right;
    
    public function __construct(Specification $left, Specification $right)
    {
        $this->left = $left;
        $this->right = $right;
    }
    
    public function isSatisfiedBy($item): bool
    {
        return $this->left->isSatisfiedBy($item) || $this->right->isSatisfiedBy($item);
    }
}

class NotSpecification extends AbstractSpecification
{
    private Specification $specification;
    
    public function __construct(Specification $specification)
    {
        $this->specification = $specification;
    }
    
    public function isSatisfiedBy($item): bool
    {
        return !$this->specification->isSatisfiedBy($item);
    }
}

5. Refatorando o Service com Specifications

Agora podemos refatorar nosso service original para usar as specifications. O código fica muito mais limpo, flexível e testável:

<?php

class ProdutoService
{
    private ProdutoRepository $produtoRepository;
    
    public function __construct(ProdutoRepository $produtoRepository)
    {
        $this->produtoRepository = $produtoRepository;
    }
    
    public function buscarProdutosElegiveisPromocao(): array
    {
        $specPromocao = $this->criarSpecificationPromocao();
        
        $produtos = $this->produtoRepository->buscarTodos();
        $produtosElegiveis = [];
        
        foreach ($produtos as $produto) {
            if ($specPromocao->isSatisfiedBy($produto)) {
                $produtosElegiveis[] = $produto;
            }
        }
        
        return $produtosElegiveis;
    }
    
    public function validarProdutoParaPromocao(Produto $produto): bool
    {
        $specPromocao = $this->criarSpecificationPromocao();
        return $specPromocao->isSatisfiedBy($produto);
    }
    
    private function criarSpecificationPromocao(): Specification
    {
        $ativo = new ProdutoAtivoSpecification();
        $comEstoque = new ProdutoComEstoqueSpecification(10);
        $categoriaElegivel = new ProdutoCategoriaSpecification(['eletronicos', 'casa']);
        $precoAdequado = new ProdutoFaixaPrecoSpecification(50, 500);
        $semPromocaoRecente = new ProdutoSemPromocaoRecenteSpecification(30);
        
        return $ativo
            ->and($comEstoque)
            ->and($categoriaElegivel)
            ->and($precoAdequado)
            ->and($semPromocaoRecente);
    }
    
    // Agora podemos facilmente criar outras combinações
    public function buscarProdutosPremium(): array
    {
        $specPremium = $this->criarSpecificationPremium();
        
        $produtos = $this->produtoRepository->buscarTodos();
        $produtosPremium = [];
        
        foreach ($produtos as $produto) {
            if ($specPremium->isSatisfiedBy($produto)) {
                $produtosPremium[] = $produto;
            }
        }
        
        return $produtosPremium;
    }
    
    private function criarSpecificationPremium(): Specification
    {
        $ativo = new ProdutoAtivoSpecification();
        $precoAlto = new ProdutoFaixaPrecoSpecification(300, 2000);
        $categoriaLuxo = new ProdutoCategoriaSpecification(['eletronicos', 'joias']);
        
        return $ativo->and($precoAlto)->and($categoriaLuxo);
    }
}

Vantagens do Specification Pattern

A implementação do Specification Pattern traz benefícios significativos para o código:

1. Testabilidade Melhorada

Cada specification pode ser testada isoladamente, tornando os testes mais focused e fáceis de escrever:

<?php

class ProdutoAtivoSpecificationTest extends PHPUnit\Framework\TestCase
{
    public function testProdutoAtivoDeveRetornarTrue(): void
    {
        $produto = $this->createMock(Produto::class);
        $produto->method('isAtivo')->willReturn(true);
        
        $spec = new ProdutoAtivoSpecification();
        
        $this->assertTrue($spec->isSatisfiedBy($produto));
    }
    
    public function testProdutoInativoDeveRetornarFalse(): void
    {
        $produto = $this->createMock(Produto::class);
        $produto->method('isAtivo')->willReturn(false);
        
        $spec = new ProdutoAtivoSpecification();
        
        $this->assertFalse($spec->isSatisfiedBy($produto));
    }
}

2. Reutilização e Composição

As specifications podem ser facilmente reutilizadas e combinadas de diferentes formas para atender a diversos cenários de negócio. Não precisamos duplicar lógica - simplesmente combinamos specifications existentes.

3. Expressividade do Código

O código resultante é muito mais expressivo e legível. Métodos como criarSpecificationPromocao() deixam claro exatamente quais critérios estão sendo aplicados.

4. Facilidade de Manutenção

Mudanças nas regras de negócio podem ser facilmente implementadas modificando specifications específicas, sem afetar outras partes do sistema.

Padrões Avançados com Specifications

Specifications Parametrizáveis

Podemos criar specifications mais flexíveis que aceitam parâmetros:

<?php

class ProdutoDescontoClienteEspecialSpecification extends AbstractSpecification
{
    private Cliente $cliente;
    
    public function __construct(Cliente $cliente)
    {
        $this->cliente = $cliente;
    }
    
    public function isSatisfiedBy($produto): bool
    {
        if (!$produto instanceof Produto) {
            throw new InvalidArgumentException('Item deve ser uma instância de Produto');
        }
        
        // Clientes VIP têm acesso a mais produtos em promoção
        if ($this->cliente->isVip()) {
            return true;
        }
        
        // Clientes regulares têm acesso limitado baseado na categoria
        $categoriasPermitidas = ['basico', 'intermediario'];
        $categoria = strtolower($produto->getCategoria()->getNome());
        
        return in_array($categoria, $categoriasPermitidas);
    }
}

Specifications com Validação Complexa

Para regras que envolvem múltiplos objetos ou validações complexas:

<?php

class ProdutoValidoParaPedidoSpecification extends AbstractSpecification
{
    private Pedido $pedido;
    
    public function __construct(Pedido $pedido)
    {
        $this->pedido = $pedido;
    }
    
    public function isSatisfiedBy($produto): bool
    {
        if (!$produto instanceof Produto) {
            throw new InvalidArgumentException('Item deve ser uma instância de Produto');
        }
        
        // Produto deve estar ativo
        if (!$produto->isAtivo()) {
            return false;
        }
        
        // Deve haver estoque suficiente
        $quantidadeNoPedido = $this->pedido->getQuantidadeProduto($produto);
        if ($produto->getQuantidadeEstoque() < $quantidadeNoPedido) {
            return false;
        }
        
        // Verificar restrições geográficas
        $enderecoEntrega = $this->pedido->getEnderecoEntrega();
        if (!$produto->podeSerEntregueEm($enderecoEntrega)) {
            return false;
        }
        
        return true;
    }
}

Quando Usar o Specification Pattern

O Specification Pattern é especialmente útil nos seguintes cenários:

Cenários Ideais

Regras de Validação Complexas: Quando você tem múltiplas condições que precisam ser verificadas antes de uma operação.

Filtros Dinâmicos: Quando usuários podem combinar diferentes critérios de busca de forma flexível.

Regras de Negócio Reutilizáveis: Quando as mesmas regras aparecem em múltiplos contextos.

Evolução Frequente de Regras: Quando as regras de negócio mudam com frequência e você precisa de flexibilidade.

Cuidados e Limitações

Complexidade Adicional: Para regras simples, o pattern pode adicionar complexidade desnecessária.

Performance: Se não implementado cuidadosamente, pode levar a múltiplas consultas ao banco de dados.

Curva de Aprendizado: Desenvolvedores precisam entender o padrão para usá-lo efetivamente.

Conclusão

O Specification Pattern é uma ferramenta poderosa para organizar e gerenciar regras de negócio complexas em aplicações Domain-Driven Design. Ele promove código mais limpo, testável e flexível, permitindo que regras de negócio sejam facilmente combinadas e reutilizadas.

A implementação cuidadosa deste padrão resulta em um código mais expressivo que reflete diretamente as regras do domínio, facilitando tanto a manutenção quanto a evolução do sistema. No próximo artigo da série, exploraremos CQRS e Event Sourcing, padrões que complementam muito bem o Specification Pattern em arquiteturas mais complexas.

Referências