Blocos de montar com encaixe fixo
Numa caixa de blocos, poucas peças com encaixes compatíveis produzem infinitas construções. Você não precisa de uma peça nova para cada modelo — precisa das peças certas e das regras de encaixe. Uma expressão funciona assim: E, OU e NÃO são poucos tipos de peça, mas combinados descrevem qualquer condição que a linguagem permita.
O problema
Regras de negócio voláteis são um mau encaixe para código compilado. Quem decide 'clientes pro da região sul não bloqueados' é a área de negócio, e a espera pelo próximo deploy custa oportunidade.
As saídas fáceis são ruins: eval sobre texto do usuário é execução remota de código, e uma tabela de flags rígida não expressa combinações com parênteses e negação.
O Interpreter é, estruturalmente, um Composite especializado: terminais são folhas, não-terminais são compostos, e a avaliação é a travessia recursiva. Se você entendeu Composite, já entendeu metade daqui — o que muda é a intenção, que passa a ser representar uma gramática.
É importante separar duas coisas que costumam se confundir: parsing e interpretação. O padrão trata só da segunda — dada a árvore, como avaliá-la. Transformar o texto plano = pro E regiao = sul em objetos é trabalho de um parser, que o GoF deixa de fora e que na prática costuma ser a parte mais trabalhosa.
Também vale notar a alternativa quase sempre melhor para gramáticas maiores: separar a árvore da avaliação e usar Visitor. Assim a mesma árvore serve para avaliar, validar, otimizar e imprimir de volta como texto — em vez de acumular tudo dentro de cada classe de nó.
O limite prático do padrão
O Interpreter é o padrão menos usado do GoF, e por um motivo honesto: ele escala mal. Cada regra gramatical é uma classe, e gramáticas reais têm dezenas.
Com aritmética, precedência de operadores, funções, variáveis e tipos, a contagem de classes explode e as interações entre elas ficam difíceis de manter corretas. Nesse território, geradores de parser ou bibliotecas de expressão prontas ganham disparado.
A faixa em que ele brilha é estreita e real: gramáticas de cinco a dez regras, estáveis, com necessidade de auditar e explicar. Filtros booleanos, condições de alerta e critérios de segmentação cabem bem.
Antes de escrever o seu, considere uma linguagem de expressão pronta (CEL, JsonLogic, expressões de regra de mercado). Elas já resolveram parsing, segurança e desempenho — e o valor que você agrega raramente está em ter inventado a sintaxe.
Enão-terminal — exige os dois lados
plano = proterminal — compara e devolve
OUnão-terminal — basta um lado
regiao = sulterminal
NÃO (bloqueado = sim)não-terminal de um filho só
Regra como árvore de objetos
- a regra é dado: pode ser salva, versionada e revisada
- alterar não exige deploy
- sem eval — só os operadores que você definiu existem
- dá para instrumentar e explicar cada decisão
O custo é uma classe por regra gramatical e a necessidade de um parser se a entrada for texto — além de avaliação bem mais lenta que código compilado.
Navegue pelas camadas
Camada 1/3Expressão terminal
use ← →Camada 1
Expressão terminal
a folha que olha os dados
Implementa a regra mais simples da gramática — uma comparação, uma constante. É o único tipo que realmente consulta o contexto; todo o resto apenas combina resultados.
No código
class Campo { interpretar(ctx) { return ctx[this.nome] === this.valor } }Se violar: terminal que consulta banco ou chama API transforma a avaliação da árvore numa cascata de I/O imprevisível.
Estrutura
Código
type Contexto = Record<string, string>;
interface Expressao {
interpretar(ctx: Contexto): boolean;
}
// Terminal: a regra mais simples da gramatica
class Campo implements Expressao {
constructor(
private nome: string,
private valor: string,
) {}
interpretar(ctx: Contexto): boolean {
return ctx[this.nome] === this.valor;
}
}
// Nao-terminais: regras compostas por outras regras
class E implements Expressao {
constructor(private esq: Expressao, private dir: Expressao) {}
interpretar(ctx: Contexto): boolean {
return this.esq.interpretar(ctx) && this.dir.interpretar(ctx);
}
}
class Ou implements Expressao {
constructor(private esq: Expressao, private dir: Expressao) {}
interpretar(ctx: Contexto): boolean {
return this.esq.interpretar(ctx) || this.dir.interpretar(ctx);
}
}
class Nao implements Expressao {
constructor(private dentro: Expressao) {}
interpretar(ctx: Contexto): boolean {
return !this.dentro.interpretar(ctx);
}
}
// "plano = pro E (regiao = sul OU NAO(bloqueado = sim))"
const regra = new E(
new Campo("plano", "pro"),
new Ou(new Campo("regiao", "sul"), new Nao(new Campo("bloqueado", "sim"))),
);
console.log(regra.interpretar({ plano: "pro", regiao: "norte", bloqueado: "nao" })); // truefrom abc import ABC, abstractmethod
class Expressao(ABC):
@abstractmethod
def interpretar(self, ctx) -> bool: ...
# Terminal: a regra mais simples da gramatica
class Campo(Expressao):
def __init__(self, nome, valor):
self._nome, self._valor = nome, valor
def interpretar(self, ctx):
return ctx.get(self._nome) == self._valor
# Nao-terminais: regras compostas por outras regras
class E(Expressao):
def __init__(self, esq, dir):
self._esq, self._dir = esq, dir
def interpretar(self, ctx):
return self._esq.interpretar(ctx) and self._dir.interpretar(ctx)
class Ou(Expressao):
def __init__(self, esq, dir):
self._esq, self._dir = esq, dir
def interpretar(self, ctx):
return self._esq.interpretar(ctx) or self._dir.interpretar(ctx)
class Nao(Expressao):
def __init__(self, dentro):
self._dentro = dentro
def interpretar(self, ctx):
return not self._dentro.interpretar(ctx)
# "plano = pro E (regiao = sul OU NAO(bloqueado = sim))"
regra = E(Campo("plano", "pro"),
Ou(Campo("regiao", "sul"), Nao(Campo("bloqueado", "sim"))))
print(regra.interpretar({"plano": "pro", "regiao": "norte", "bloqueado": "nao"})) # TrueCasos de uso
Segmentação de clientes em ferramenta de marketing
Cenário
Analistas precisam montar públicos como 'plano pro E (região sul OU compra nos últimos 30 dias)' pela interface, sem pedir nada ao time de engenharia.
Como o padrão entra
A interface monta a árvore de expressões; o backend a avalia contra cada cliente. A regra é salva como JSON, versionada e reutilizável entre campanhas.
Trade-off: Avaliar a árvore por cliente não escala para milhões de registros: em algum momento a mesma árvore precisa ser traduzida para SQL, e manter os dois avaliadores coerentes vira trabalho permanente.
Condições de alerta em monitoramento
Cenário
Times definem alertas como 'erro acima de 5% E latência acima de 300ms por 5 minutos', e cada equipe tem critérios próprios.
Como o padrão entra
Cada condição é uma árvore avaliada a cada janela de métricas. Como a regra é objeto, o sistema mostra qual ramo disparou — informação que um booleano opaco não daria.
Trade-off: Regras temporais ('por 5 minutos') não cabem numa avaliação sem estado; a gramática precisa de operadores com memória, e a simplicidade original começa a se perder.
Filtros salvos em ferramenta interna
Cenário
Um back-office permite montar filtros compostos sobre pedidos e salvá-los como visões compartilhadas entre a equipe.
Como o padrão entra
O filtro é uma árvore serializada. Ele pode ser validado antes de salvar, exibido de volta na interface e traduzido para a consulta do banco.
Trade-off: Serializar a árvore cria um formato persistido: renomear um operador ou mudar sua semântica quebra os filtros que os usuários salvaram meses atrás.
- 1
Delimitar a gramática
Poucas regras estáveis (E, OU, NÃO, comparação). Se a linguagem tende a crescer sem freio, prefira uma engine pronta.
- 2
Uma classe por regra
Terminais leem o contexto; não-terminais combinam filhos. Cada operador novo é uma classe — não um ramo num switch.
- 3
Montar a árvore
A frase vira objetos aninhados (parser ou UI). Avaliar é chamar `interpretar` na raiz e deixar a recursão descer.
- 4
Manter terminais puros
Folhas só leem o contexto já carregado. I/O, banco ou API no terminal destroem previsibilidade e curto-circuito.
O terminal que consulta o banco
Compila, roda, passa no code review. E quebra depois.
A gramática existe (Campo, E, OU) — mas o terminal faz await db.ler a cada folha. A árvore deixou de ser avaliação previsível e virou cascata de I/O.
class Campo implements Expressao {
constructor(private nome: string, private valor: string) {}
async interpretar(ctx: Contexto): Promise<boolean> {
// "Terminal" que consulta o banco a cada folha
const atual = await db.ler(this.nome);
return atual === this.valor;
}
}
// Avaliar a arvore vira cascata de I/O; curto-circuito de E/OU
// muda quantas queries rodam. Terminal deve so ler o contexto.O que quebra
- No curto-circuitoE/OU muda quantas queries rodam conforme a ordem dos ramos — custo e side effects imprevisíveis.
- No testeAvaliar uma expressão exige banco (ou mock) em cada folha; a regra deixa de ser dado puro.
- Na auditoriaExplicar 'por que entrou' fica opaco: o resultado depende do estado do banco no instante da folha.
A saída: Carregue o contexto antes; terminais só comparam o que já está em memória. Se precisar de dados externos, resolva na borda — não dentro da árvore.
Armadilhas
A gramática cresce sem parar
Começa com E, OU e NÃO; em seis meses tem aritmética, funções de data, variáveis e precedência. Cada adição é uma classe e novas interações a testar. Se a gramática está crescendo todo mês, o padrão é a ferramenta errada — um parser gerado ou uma linguagem de expressão pronta escala muito melhor.
Achar que o padrão inclui o parser
O Interpreter avalia a árvore; ele não transforma texto em árvore. Times descobrem isso depois de adotar o padrão e acabam escrevendo um parser à mão, com todos os problemas clássicos de precedência e mensagens de erro. Se a entrada for texto livre, planeje o parser desde o início.
Terminal com efeito colateral
Um terminal que consulta banco parece prático e destrói as propriedades do padrão: a avaliação deixa de ser previsível, o custo passa a depender do formato da árvore e curtos-circuitos de E/OU fazem o número de consultas variar de forma misteriosa. Terminais devem ler apenas o contexto já carregado.
Quando usar × quando evitar
Use quando
3- Existe uma mini-linguagem estável de regras que muda sem deploy.
- As regras precisam ser guardadas, versionadas e auditadas como dado.
- Você precisa explicar por que uma regra deu determinado resultado.
Evite quando
3- A gramática é grande ou cresce constantemente.
- Desempenho é crítico e o volume é alto.
- As regras são poucas e estáveis — funções bastam.
Testar o que ficou
3 armadilhas de Interpreter viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
Engines de regexA expressão é uma linguagem própria, compilada numa árvore que depois é avaliada contra o texto.Template engines{{ nome }}é uma gramática pequena com um avaliador que a resolve contra um contexto.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Árvore da linguagem → avaliação.
const n = parse("a AND b").avaliar(ctx);O que ele cobra
2 saltos a mais entre a chamada e o efeito.
- Uma classe por regra gramatical, que cresce rápido conforme a linguagem ganha construções
- Interpretar a árvore a cada execução costuma ser mais lento que compilar ou usar uma engine pronta
Não vale se: a linguagem é complexa ou muda muito — aí um parser gerado ou uma engine existente supera a gramática feita à mão.
Onde isto aparece
Continue por aqui
- CompositeCompõe objetos em árvores e faz o cliente tratar um item isolado e um grupo inteiro exatamente do mesmo jeito, através de uma interface comum.
- VisitorSepara operações da estrutura de objetos sobre a qual elas atuam, permitindo adicionar comportamentos novos sem modificar as classes existentes — ao custo de congelar o conjunto de tipos.
- StrategyEncapsula uma família de algoritmos intercambiáveis por trás de uma interface comum, deixando o cliente trocar a estratégia em tempo de execução.