Como aplicar o padrão specification no domínio de negócio
1. Introdução ao Padrão Specification
O padrão Specification foi formalizado por Eric Evans em seu livro "Domain-Driven Design: Tackling Complexity in the Heart of Software" como uma forma de encapsular regras de negócio em objetos reutilizáveis. A ideia central é simples: em vez de espalhar lógica de validação e filtragem por repositórios, serviços e queries, você cria objetos especializados que representam uma condição de negócio.
O problema comum que o padrão resolve é a dispersão de regras. Em sistemas tradicionais, uma regra como "cliente pode receber desconto" aparece em múltiplos lugares: no serviço de pedidos, na query do banco, na validação de frontend. Qualquer alteração exige caça aos fragmentos.
Os benefícios são claros:
- Reuso: uma mesma Specification pode ser usada em validação, filtragem e autorização
- Testabilidade: cada regra é testada isoladamente
- Isolamento: a lógica de domínio fica protegida de detalhes de infraestrutura
2. Estrutura Básica de uma Specification
A estrutura fundamental consiste em uma interface genérica com um método IsSatisfiedBy e operadores de composição lógica.
// Interface base
interface Specification<T> {
boolean IsSatisfiedBy(T entity);
Specification<T> And(Specification<T> other);
Specification<T> Or(Specification<T> other);
Specification<T> Not();
}
// Implementação concreta
class ClienteAtivoSpecification implements Specification<Cliente> {
boolean IsSatisfiedBy(Cliente cliente) {
return cliente.getStatus() == Status.ATIVO
&& cliente.getDataCancelamento() == null;
}
}
// Composição
Specification<Cliente> clienteVipAtivo =
new ClienteAtivoSpecification()
.And(new ClienteVipSpecification());
A composição com operadores permite construir regras complexas a partir de blocos simples, seguindo o princípio de responsabilidade única.
3. Mapeando Regras de Negócio em Specifications
Para identificar regras candidatas, observe três categorias principais:
- Validações: "pedido não pode exceder limite de crédito"
- Filtros: "mostrar apenas produtos em estoque"
- Autorizações: "usuário pode cancelar pedido apenas nas primeiras 24 horas"
Exemplo em um domínio de e-commerce:
// Regras de elegibilidade para desconto
class ElegibilidadeDescontoSpecification implements Specification<Pedido> {
boolean IsSatisfiedBy(Pedido pedido) {
return pedido.getValorTotal() > 100.0
&& pedido.getCliente().isPrimeiroPedido() == false
&& pedido.getDataPedido().getMonth() == Month.DECEMBER;
}
}
// Separação entre regras mutáveis e fixas
class LimiteLegalSpecification implements Specification<Pedido> {
boolean IsSatisfiedBy(Pedido pedido) {
return pedido.getDescontoAplicado() <= 0.3 * pedido.getValorTotal();
}
}
// Regra sazonal (mutável)
class PromocaoBlackFridaySpecification implements Specification<Pedido> {
boolean IsSatisfiedBy(Pedido pedido) {
return pedido.getDataPedido().isAfter(blackFridayStart)
&& pedido.getDataPedido().isBefore(blackFridayEnd);
}
}
Regras fixas como limites legais raramente mudam, enquanto regras promocionais podem ser ativadas/desativadas sem tocar no código central.
4. Integração com Repositórios e ORMs
A integração com bancos de dados requer cuidado: avaliação in-memory vs. avaliação no banco. Para grandes volumes, a avaliação deve ocorrer no banco via LINQ, Criteria API ou Query Objects.
// Specification para consulta no banco (EF Core)
class PedidosVencidosSpecification implements Specification<Pedido> {
Expression<Func<Pedido, bool>> ToExpression() {
return pedido => pedido.getDataVencimento() < DateTime.Now
&& pedido.getStatus() == Status.PENDENTE;
}
}
// Repositório usando Specification para filtrar
class PedidoRepository {
List<Pedido> FindBySpecification(Specification<Pedido> spec) {
if (spec instanceof SqlSpecification) {
var expression = ((SqlSpecification) spec).ToExpression();
return dbContext.Pedidos.Where(expression).ToList();
} else {
return dbContext.Pedidos.ToList()
.Where(p => spec.IsSatisfiedBy(p))
.ToList();
}
}
}
// Uso prático
var vencidos = pedidoRepository.FindBySpecification(
new PedidosVencidosSpecification()
);
A diferença é crucial: usar IsSatisfiedBy em memória para 100 mil registros pode ser inviável. A solução é ter Specifications que expõem expressões traduzíveis para SQL.
5. Testabilidade e Isolamento da Lógica
Cada Specification deve ser testada isoladamente, sem dependências de banco ou serviços.
// Teste unitário puro
@Test
void clienteAtivoSpecification_DeveRetornarTrue_QuandoClienteAtivo() {
var cliente = new Cliente();
cliente.setStatus(Status.ATIVO);
cliente.setDataCancelamento(null);
var spec = new ClienteAtivoSpecification();
assertTrue(spec.IsSatisfiedBy(cliente));
}
// Teste de composição
@Test
void composicaoAnd_DeveExigirAmbasCondicoes() {
var cliente = new Cliente();
cliente.setStatus(Status.ATIVO);
cliente.setTotalCompras(1500.0);
var spec = new ClienteAtivoSpecification()
.And(new ClienteVipSpecification(1000.0));
assertTrue(spec.IsSatisfiedBy(cliente));
}
// Teste com data mockada
@Test
void promocaoSazonal_DeveConsiderarDataAtual() {
var pedido = new Pedido();
pedido.setDataPedido(LocalDate.of(2024, 12, 25));
var spec = new PromocaoNatalSpecification(
LocalDate.of(2024, 12, 20),
LocalDate.of(2024, 12, 31)
);
assertTrue(spec.IsSatisfiedBy(pedido));
}
Cenários de borda como composições aninhadas (A AND (B OR C)) devem ser testados explicitamente para garantir a precedência lógica correta.
6. Negociação com Stakeholders Usando Specifications
Specifications funcionam como "blocos de construção" visíveis para o negócio. Você pode mostrar ao stakeholder uma lista de Specifications e perguntar: "Quais regras devem valer agora?"
// Antes: regra de frete grátis hardcoded no serviço
class PedidoService {
void calcularFrete(Pedido pedido) {
if (pedido.getValorTotal() > 200
&& pedido.getCliente().isPrimeiraCompra()
&& pedido.getDataPedido().getDayOfWeek() == DayOfWeek.FRIDAY) {
pedido.setFrete(0.0);
}
}
}
// Depois: regra composta por Specifications independentes
class PedidoService {
void calcularFrete(Pedido pedido, Specification<Pedido> regraFreteGratis) {
if (regraFreteGratis.IsSatisfiedBy(pedido)) {
pedido.setFrete(0.0);
}
}
}
// O stakeholder pode mudar a regra sem tocar no serviço
var novaRegra = new ValorMinimoSpecification(150.0)
.And(new ClienteRecorrenteSpecification())
.Or(new PromocaoSextaFeiraSpecification());
Mudar a regra de frete grátis de "acima de R$200" para "acima de R$150" vira apenas alteração em uma Specification, sem reescrever lógica de pedido.
7. Armadilhas e Boas Práticas
Armadilha 1: Specification genérica demais
Evite criar uma Specification<T> que aceita expressões lambda arbitrárias. Isso vira um "god object" que perde o significado de negócio.
// Ruim: perde o significado
class GenericSpecification<T> implements Specification<T> {
private Predicate<T> predicate;
// ... sem contexto de negócio
}
// Bom: cada Specification tem nome e propósito claros
class ClienteComAltoRiscoSpecification implements Specification<Cliente> {
// regra específica: cliente com mais de 3 atrasos
}
Armadilha 2: Performance em avaliação em memória
Sempre avalie se a Specification será usada para filtrar milhares de registros. Nesse caso, implemente uma versão que gera expressões SQL.
Armadilha 3: Versionamento de regras
Regras de negócio mudam. Considere versionar Specifications para rastrear mudanças:
// Specification versionada
class LimiteCreditoSpecificationV2 implements Specification<Cliente> {
// Versão 2: aumentou limite de 5000 para 10000
boolean IsSatisfiedBy(Cliente cliente) {
return cliente.getLimiteDisponivel() >= 10000;
}
}
Boas práticas:
- Nomeie Specifications com linguagem ubíqua do negócio
- Mantenha Specifications pequenas (máximo 3-5 condições)
- Prefira composição a herança
- Documente o motivo de negócio de cada Specification
Referências
- Domain-Driven Design: Tackling Complexity in the Heart of Software (Eric Evans) — Livro fundamental que introduziu o padrão Specification no contexto de DDD
- Specification Pattern - Martin Fowler — Artigo seminal de Martin Fowler sobre o padrão e suas aplicações
- Specification Pattern in C# - Microsoft Docs — Documentação oficial da Microsoft sobre implementação do padrão em .NET
- Specification Pattern with JPA Criteria API - Baeldung — Tutorial prático de como usar Specifications com Spring Data JPA
- Implementing the Specification Pattern in Java - Reflectoring — Guia completo com exemplos de composição lógica e integração com repositórios
- Specification Pattern: When to Use It - CodeOpinion — Análise de cenários onde o padrão realmente agrega valor versus onde é overengineering