Padrão Comportamental (GoF)

Interpreter

Define uma representação para a gramática de uma linguagem e um interpretador que usa essa representação para processar sentenças — cada regra da gramática vira uma classe, e a árvore sintática é construída compondo essas classes.

Intenção

Dada uma linguagem, definir uma representação para a sua gramática e um interpretador que usa essa representação para processar sentenças da linguagem. Cada regra da gramática é mapeada para uma classe; expressões complexas são formadas compondo objetos dessas classes em árvores sintáticas — que são, por isso, Composites.

Catalogado pelo GoF (1994) como padrão comportamental, o Interpreter é mais didático do que amplamente aplicado em projetos modernos. Ele é útil para linguagens simples e pequenas DSLs onde a gramática tem poucas regras e a expressividade manual de classes compensa o custo. Para gramáticas reais — com tokens complexos, precedência de operadores, tratamento de erros léxicos e sintáticos — use ferramentas dedicadas: parser generators (ANTLR, PEG.js, Tree-sitter), combinadores de parser (Parsimmon, Chevrotain) ou gramáticas PEG/BNF. O padrão de classes do Interpreter se torna inviável rapidamente à medida que a gramática cresce.

Problema

Imagine um sistema de configuração que aceita filtros escritos como expressões matemáticas com variáveis: a + b * 3, onde a e b vêm de um contexto (registro, sessão, configuração). A abordagem ingênua avalia a expressão como string em runtime — insegura e inflexível:

// Abordagem ingênua — NÃO faça isso:
function avaliar(expressao: string, contexto: Record<string, number>): number {
  // eval() executa código arbitrário — risco de injeção de código.
  // Não há validação, sem tipagem, sem mensagens de erro úteis.
  return eval(expressao.replace(/([a-z]+)/g, (_, v) => String(contexto[v] ?? 0)));
}

console.log(avaliar('a + b * 3', { a: 5, b: 4 }));   // 17 (mas perigoso!)
// Pior: avaliar('process.exit(1)', {}) derruba o processo.

O Interpreter resolve isso mapeando cada regra da gramática para uma classe. A expressão é representada como uma árvore de objetos — construída manualmente ou por um parser simples. Cada nó sabe como se interpretar dado um contexto, e a avaliação é feita por chamadas polimórficas na árvore — sem eval(), sem código arbitrário.

Solução

O Interpreter organiza o código em quatro participantes:

  1. AbstractExpression (interface): declara o método interpretar(contexto): T que todas as expressões devem implementar. O tipo de retorno depende da linguagem: número, boolean, string. O contexto carrega o estado externo necessário para a avaliação (valores de variáveis, registros de dados, configurações).
  2. TerminalExpression: implementa a interpretação de um símbolo terminal da gramática — um símbolo que não se expande em outros. Exemplos: Numero (retorna um valor literal) e Variavel (consulta o contexto). São as folhas da árvore sintática.
  3. NonTerminalExpression: implementa uma regra da gramática que referencia outras expressões. Exemplos: Soma, Subtracao, Multiplicacao. Cada NonTerminalExpression contém referências às sub-expressões (esquerda e direita) e delega a interpretação a elas antes de combinar os resultados. Como cada NonTerminalExpression contém outras IExpressao, a estrutura é um Composite.
  4. Context: carrega a informação global necessária para a interpretação — tipicamente os valores das variáveis. Em TypeScript, pode ser um simples Record<string, number>; em sistemas mais complexos, um objeto com métodos de consulta e estado acumulado.

A avaliação é feita chamando expressaoRaiz.interpretar(contexto): o método percorre recursivamente a árvore, dos nós internos até as folhas, e retorna o resultado combinado. A gramática está implícita na estrutura das classes — não há um parser generator, apenas objetos compostos.

Estrutura

         «interface»
         IExpressao
  ┌────────────────────────────────────────┐
  │ + interpretar(ctx: Contexto): number   │
  └────────────────────────────────────────┘
               ▲
   ┌───────────┼────────────────────────────────┐
   │           │           │           │        │
Numero    Variavel        Soma    Subtracao  Multiplicacao
(Terminal)(Terminal) (NonTerminal)(NonTerminal)(NonTerminal)
   │           │
   │ retorna   │ consulta ctx[nome]
   │ this.valor│
   │           │
   └─ não referencia outras IExpressao (folha)

Soma / Subtracao / Multiplicacao:
  ┌───────────────────────────────────────────────┐
  │ - esquerda: IExpressao                        │
  │ - direita: IExpressao                         │
  │ + interpretar(ctx): number                    │
  │   return esquerda.interpretar(ctx)            │
  │        OP direita.interpretar(ctx)            │
  └───────────────────────────────────────────────┘
  (Composite interno — cada nó pode conter outros nós)


Contexto:
  { a: 5, b: 4 }   (Record<string, number>)


Fluxo para "a + (b * 3)", ctx = {a:5, b:4}:

  Soma
  ├── Variavel('a')  → interpretar(ctx) = 5
  └── Multiplicacao
       ├── Variavel('b') → interpretar(ctx) = 4
       └── Numero(3)     → interpretar(ctx) = 3
       → 4 * 3 = 12
  → 5 + 12 = 17

Exemplos de código

Exemplo 1 — Expressões matemáticas com variáveis e contexto

Implementação completa com a interface IExpressao, duas expressões terminais (Numero e Variavel) e três não-terminais (Soma, Subtracao, Multiplicacao). O contexto é um simples mapa de variáveis. A árvore é construída manualmente — em sistemas reais, um parser faria esse trabalho.

// ── Contexto ──────────────────────────────────────────────────
// Mapa de variáveis disponíveis durante a interpretação.
type Contexto = Record<string, number>;

// ── AbstractExpression ────────────────────────────────────────
interface IExpressao {
  interpretar(ctx: Contexto): number;
}

// ── TerminalExpressions ───────────────────────────────────────

// Numero: folha que retorna um valor literal.
class Numero implements IExpressao {
  constructor(private readonly valor: number) {}

  interpretar(_ctx: Contexto): number {
    return this.valor;
  }
}

// Variavel: folha que consulta o contexto pelo nome.
class Variavel implements IExpressao {
  constructor(private readonly nome: string) {}

  interpretar(ctx: Contexto): number {
    if (!(this.nome in ctx)) {
      throw new Error(`Variável '${this.nome}' não definida no contexto`);
    }
    return ctx[this.nome];
  }
}

// ── NonTerminalExpressions ────────────────────────────────────

class Soma implements IExpressao {
  constructor(
    private readonly esquerda: IExpressao,
    private readonly direita: IExpressao
  ) {}

  interpretar(ctx: Contexto): number {
    return this.esquerda.interpretar(ctx) + this.direita.interpretar(ctx);
  }
}

class Subtracao implements IExpressao {
  constructor(
    private readonly esquerda: IExpressao,
    private readonly direita: IExpressao
  ) {}

  interpretar(ctx: Contexto): number {
    return this.esquerda.interpretar(ctx) - this.direita.interpretar(ctx);
  }
}

class Multiplicacao implements IExpressao {
  constructor(
    private readonly esquerda: IExpressao,
    private readonly direita: IExpressao
  ) {}

  interpretar(ctx: Contexto): number {
    return this.esquerda.interpretar(ctx) * this.direita.interpretar(ctx);
  }
}

// ── Uso ──────────────────────────────────────────────────────
// Expressão: a + (b * 3) - 1
// AST construída manualmente — em produção, um parser faria isso.
const expressao: IExpressao = new Subtracao(
  new Soma(
    new Variavel('a'),
    new Multiplicacao(new Variavel('b'), new Numero(3))
  ),
  new Numero(1)
);

// Contexto 1
const ctx1: Contexto = { a: 5, b: 4 };
console.log(expressao.interpretar(ctx1));
// 5 + (4 * 3) - 1 = 16

// Contexto 2 — mesma expressão, outros valores
const ctx2: Contexto = { a: 10, b: 2 };
console.log(expressao.interpretar(ctx2));
// 10 + (2 * 3) - 1 = 15

// Expressão composta: (a - b) * (a + b)  → diferença de quadrados
const difQuadrados: IExpressao = new Multiplicacao(
  new Subtracao(new Variavel('a'), new Variavel('b')),
  new Soma(new Variavel('a'), new Variavel('b'))
);
// a=5, b=3 → (5-3)*(5+3) = 2*8 = 16
console.log(difQuadrados.interpretar({ a: 5, b: 3 }));   // 16

Exemplo 2 — Mini-DSL de filtros booleanos

O Interpreter também é adequado para pequenas DSLs de consulta ou filtro. Neste exemplo, a linguagem tem três regras: Igual (terminal — verifica se um campo de um registro tem determinado valor), E (AND) e Ou (OR) como não-terminais. O contexto é o registro sendo avaliado. A expressão filtra uma lista de registros sem eval() e com composição tipo-segura.

// ── Contexto: um registro de dados ───────────────────────────
type Registro = Record<string, string | number>;

// ── AbstractExpression ────────────────────────────────────────
interface ICondicao {
  avaliar(registro: Registro): boolean;
}

// ── TerminalExpression: campo = valor ─────────────────────────
class Igual implements ICondicao {
  constructor(
    private readonly campo: string,
    private readonly valor: string | number
  ) {}

  avaliar(registro: Registro): boolean {
    return registro[this.campo] === this.valor;
  }
}

// ── NonTerminalExpression: E (AND) ────────────────────────────
class E implements ICondicao {
  constructor(
    private readonly esquerda: ICondicao,
    private readonly direita: ICondicao
  ) {}

  avaliar(registro: Registro): boolean {
    // Short-circuit: se esquerda for falsa, direita não é avaliada.
    return this.esquerda.avaliar(registro) && this.direita.avaliar(registro);
  }
}

// ── NonTerminalExpression: Ou (OR) ────────────────────────────
class Ou implements ICondicao {
  constructor(
    private readonly esquerda: ICondicao,
    private readonly direita: ICondicao
  ) {}

  avaliar(registro: Registro): boolean {
    return this.esquerda.avaliar(registro) || this.direita.avaliar(registro);
  }
}

// ── Uso ──────────────────────────────────────────────────────
// DSL: status = 'ativo' AND (plano = 'pro' OR plano = 'enterprise')
const filtro: ICondicao = new E(
  new Igual('status', 'ativo'),
  new Ou(
    new Igual('plano', 'pro'),
    new Igual('plano', 'enterprise')
  )
);

const registros: Registro[] = [
  { status: 'ativo',   plano: 'pro' },
  { status: 'ativo',   plano: 'free' },
  { status: 'inativo', plano: 'pro' },
  { status: 'ativo',   plano: 'enterprise' },
];

const resultado = registros.filter(r => filtro.avaliar(r));
console.log(resultado);
// [{ status: 'ativo', plano: 'pro' }, { status: 'ativo', plano: 'enterprise' }]

// A árvore de filtro pode ser construída dinamicamente a partir de
// uma configuração JSON — sem eval(), com total controle sobre o que
// a linguagem pode expressar.

Quando usar

  • Para DSLs pequenas e estáveis: filtros de consulta, expressões de regras de negócio, mini-linguagens de configuração ou templates com poucas construções. O número de classes de expressão deve ser pequeno — até dez regras é razoável; além disso, prefira um parser generator.
  • Quando a gramática é bem conhecida e raramente muda: o Interpreter é fácil de estender com novos operadores (novas classes), mas qualquer mudança na estrutura central da gramática pode cascatear por todas as classes existentes.
  • Para fins didáticos e prototipagem: o padrão torna a estrutura de uma gramática explícita no código de classes — útil para aprender como parsers funcionam internamente antes de usar ferramentas automatizadas.
  • Quando segurança é crítica: uma DSL baseada em Interpreter executa apenas as operações que você definiu — sem risco de code injection via eval(). Sistemas de regras de negócio, configurações de filtragem e engines de validação se beneficiam desse controle explícito.

Quando evitar

  • Gramáticas complexas: cada regra vira uma classe; uma gramática com 20 regras gera 20 classes — e a interação entre elas fica rapidamente difícil de manter. Use ANTLR, PEG.js, Chevrotain ou Tree-sitter para qualquer linguagem com precedência de operadores não-trivial, recuperação de erros ou tokens complexos.
  • Quando desempenho é crítico: a recursão por toda a árvore a cada avaliação pode ser lenta para expressões complexas ou avaliadas em alta frequência. Compiladores reais não interpretam a AST diretamente — compilam para bytecode ou código nativo.
  • Quando o padrão já existe na linguagem: muitas linguagens têm formas idiomáticas de compor predicados — closures, combinadores funcionais, expressões regulares. O Interpreter baseado em classes frequentemente é mais verboso do que uma solução funcional.

Prós e contras

Prós

  • A gramática fica explícita no código — cada regra é uma classe com nome descritivo, fácil de localizar.
  • Aberto para novas regras: adicionar um operador é criar uma nova classe que implementa a interface — sem modificar as expressões existentes.
  • Avaliação segura e controlada: sem eval(), sem code injection, sem acesso a APIs não autorizadas.
  • A árvore de expressão pode ser construída dinamicamente (a partir de JSON, banco de dados ou UI) — permitindo DSLs configuráveis em runtime.
  • Separação clara entre a estrutura da expressão e o mecanismo de avaliação.

Contras

  • Explosão de classes para gramáticas não-triviais — uma classe por regra é inviável acima de ~10-15 regras.
  • Sem suporte nativo a erros léxicos ou sintáticos: construir a árvore manualmente não detecta sentenças inválidas — exige um parser separado.
  • Desempenho inferior a bytecode ou código compilado — a recursão interpretada na AST é ordens de magnitude mais lenta que código nativo.
  • Dificuldade em implementar precedência de operadores, associatividade e agrupamento sem um parser dedicado.

Armadilhas comuns

1. Confundir Interpreter com Composite

O Interpreter usa o Composite internamente — a árvore sintática é um Composite onde as NonTerminalExpressions são os nós compostos e as TerminalExpressions são as folhas. A diferença é de intenção: o Composite organiza estruturas parte-todo e trata folhas e compostos uniformemente; o Interpreter interpreta sentenças de uma gramática específica. Quando alguém adiciona um método interpretar() a um Composite existente, ele está essencialmente aplicando o Interpreter sobre a estrutura do Composite.

2. Crescimento descontrolado de classes

A armadilha de escala mais crítica: cada nova regra da gramática exige uma nova classe. Uma DSL de filtros que começa com Igual, E e Ou parece razoável. Mas ao adicionar Maior, Menor, Contém, Começa, Nao, Entre, In — o número de classes cresce rapidamente. Quando esse ponto é atingido, migre para um combinador de parsers ou um parser generator.

Regra prática: se a gramática tem mais de 8 a 10 classes de expressão, avalie seriamente o uso de uma biblioteca de parsing dedicada. O Interpreter como padrão de classes é para DSLs genuinamente pequenas — não para linguagens reais.

3. Ausência de tratamento de erros na construção da AST

Construir a árvore manualmente não valida a sentença. Uma expressão com new Soma(null, new Numero(3)) compila sem erros mas explode em runtime com um NullPointerException. Em TypeScript, use tipos não-anuláveis e strict mode para mitigar; em PHP, declare tipos na interface. Para validação completa de sentenças de entrada externa (strings digitadas pelo usuário), um parser com mensagens de erro úteis é indispensável.

4. Interpreter vs Composite — quando usar qual

Use o Composite quando o foco é a estrutura hierárquica parte-todo e a operação principal é uniforme sobre todos os nós (ex.: calcular tamanho total de uma pasta). Use o Interpreter quando o foco é avaliar sentenças de uma linguagem com regras gramaticais explícitas — onde cada tipo de nó tem semântica diferente de avaliação (Soma soma, Multiplicacao multiplica, Variavel consulta o contexto). Na prática, o Interpreter é sempre um Composite; mas nem todo Composite é um Interpreter.

Padrões relacionados

O Interpreter se relaciona diretamente com padrões estruturais e comportamentais que organizam e percorrem hierarquias:

O Composite é a fundação estrutural do Interpreter: a árvore sintática abstrata (AST) é um Composite onde as NonTerminalExpressions são nós compostos e as TerminalExpressions são folhas. O método interpretar() percorre essa árvore de forma recursiva — exatamente como o método operation() percorre um Composite. O Visitor é o complemento natural do Interpreter: enquanto o Interpreter define uma única operação (interpretar()) diretamente em cada classe de expressão, o Visitor separa as operações das classes — permitindo adicionar novos comportamentos (imprimir, otimizar, compilar) à mesma AST sem modificá-la. Em compiladores reais, a AST é construída com o Composite, interpretada com o Interpreter em fases iniciais e depois percorrida por múltiplos passes de análise e transformação implementados como Visitors.