DDD: Specification Pattern - Encapsulando regras de negócio complexas (Parte 11)
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.