Padrão Comportamental (GoF)

Visitor

Separa um algoritmo dos objetos sobre os quais ele opera, permitindo adicionar novas operações a uma hierarquia de classes sem modificá-las — por meio de double dispatch, cada elemento "aceita" o visitor e invoca o método correto para o seu próprio tipo.

Intenção

Representar uma operação a ser executada sobre os elementos de uma hierarquia de objetos. O Visitor permite definir uma nova operação sem alterar as classes dos elementos sobre os quais ela opera. Cada classe concreta da hierarquia implementa um método accept(visitor) que chama o método do visitor específico para aquele tipo — o chamado double dispatch.

Catalogado pelo GoF (1994) como padrão comportamental, o Visitor é o padrão indicado quando uma hierarquia de objetos é estável mas você precisa adicionar novas operações com frequência. É amplamente utilizado em compiladores (AST com visitors de type-checking, geração de código e otimização), ferramentas de análise estática, serialização de árvores e processamento de documentos estruturados.

Problema

Imagine uma árvore sintática abstrata (AST) com três tipos de nó: Numero, Soma e Multiplicacao. Você precisa adicionar duas operações independentes: imprimir a expressão como string e calcular o resultado numérico. A abordagem direta coloca as operações dentro das classes:

// Abordagem ingênua — NÃO faça isso:
class Soma {
  constructor(readonly esquerda: IExpressao, readonly direita: IExpressao) {}

  imprimir(): string {
    // Lógica de impressão acoplada ao nó da AST.
    return `(${this.esquerda.imprimir()} + ${this.direita.imprimir()})`;
  }

  calcular(): number {
    // Lógica de cálculo também acoplada.
    return this.esquerda.calcular() + this.direita.calcular();
  }
  // Ao adicionar uma terceira operação (ex.: serializar, otimizar, compilar),
  // é preciso abrir e modificar TODAS as classes da hierarquia.
}

Cada nova operação exige abrir e modificar todas as classes da hierarquia — Numero, Soma, Multiplicacao — ao mesmo tempo. Isso viola o Princípio Aberto/Fechado: as classes de domínio deveriam estar fechadas para modificação. Em compiladores reais, a AST pode ter dezenas de tipos de nó e dúzias de passes (optimizações, verificações de tipos, geração de bytecode) — misturar tudo nas classes de nó torna o código ininteligível.

O Visitor resolve isso extraindo cada operação para uma classe própria (o ConcreteVisitor). As classes da hierarquia ficam estáveis; novos comportamentos são adicionados sem tocá-las.

Solução

O Visitor organiza o código em quatro participantes:

  1. Element (interface): cada classe da hierarquia declara accept(visitor: IVisitor): void. A implementação é sempre a mesma: visitor.visitTipoConcreto(this). Esse padrão é o double dispatch — o método correto do visitor é escolhido em tempo de execução com base no tipo concreto do elemento.
  2. ConcreteElement: as classes concretas da hierarquia (Numero, Soma, Multiplicacao). Cada uma implementa accept() chamando o método visit correspondente do visitor. Expõem seus dados internos (via getters ou campos readonly) para que os visitors possam acessá-los.
  3. Visitor (interface): declara um método visit para cada ConcreteElement da hierarquia. Ex.: visitNumero(no: Numero): void, visitSoma(no: Soma): void. Quando um novo tipo de nó é adicionado à hierarquia, todos os visitors precisam ser atualizados — o custo inverso ao da abordagem ingênua.
  4. ConcreteVisitor: implementa a operação específica para cada tipo de elemento. Cada visitor é uma classe coesa com uma única responsabilidade: imprimir, calcular, serializar, otimizar. Pode acumular estado interno durante a visitação (ex.: uma pilha para calcular, uma lista de partes para imprimir).

O mecanismo central é o double dispatch: a linguagem resolve o método correto em dois passos — primeiro despacha para o accept() do elemento correto (escolhido pelo polimorfismo da hierarquia); dentro do accept(), o elemento chama visitor.visitTipo(this), resolvendo o método do visitor pelo tipo concreto de this. Sem esse segundo despacho, o visitor precisaria de instanceof/type-switch para detectar o tipo — quebrando o polimorfismo.

Estrutura

        «interface»
        IExpressao
  ┌──────────────────────────────────┐
  │ + accept(v: IVisitor): void      │
  └──────────────────────────────────┘
               ▲
   ┌───────────┼──────────────────┐
   │           │                  │
Numero       Soma          Multiplicacao
(Terminal) (NonTerminal)  (NonTerminal)
  │             │                  │
  │accept:      │accept:           │accept:
  │v.visitNum.. │v.visitSoma(this) │v.visitMult..(this)
  ▼             ▼                  ▼
        «interface»
         IVisitor
  ┌─────────────────────────────────────────────┐
  │ + visitNumero(no: Numero): void             │
  │ + visitSoma(no: Soma): void                 │
  │ + visitMultiplicacao(no: Multiplicacao):void│
  └─────────────────────────────────────────────┘
               ▲
   ┌───────────┴──────────────────┐
   │                              │
ImprimirVisitor          CalcularVisitor
  │ partes: string[]       pilha: number[]
  │ visitNumero → push     visitNumero → push
  │ visitSoma → parênteses visitSoma → pop+pop+soma
  │ resultado(): string    resultado(): number
  └──────────────────────────────┘


Double dispatch — fluxo passo a passo:

  arvore.accept(calculador)
    │
    ├─ arvore é Multiplicacao → Multiplicacao.accept(calculador)
    │    └─ calculador.visitMultiplicacao(this)
    │         ├─ this.esquerda.accept(calculador)   // Soma
    │         │    └─ calculador.visitSoma(this)
    │         │         ├─ this.esquerda.accept(calculador) // Numero(2)
    │         │         │    └─ calculador.visitNumero(this) → pilha:[2]
    │         │         └─ this.direita.accept(calculador)  // Numero(3)
    │         │              └─ calculador.visitNumero(this) → pilha:[2,3]
    │         │         → pop 3, pop 2, push 5       pilha:[5]
    │         └─ this.direita.accept(calculador)    // Numero(4)
    │              └─ calculador.visitNumero(this)  → pilha:[5,4]
    │         → pop 4, pop 5, push 20               pilha:[20]
    resultado(): 20

Exemplos de código

Exemplo 1 — AST de expressões com ImprimirVisitor e CalcularVisitor

Implementação completa com a hierarquia de expressões (Numero, Soma, Multiplicacao), a interface IVisitor e dois visitors concretos. Note como cada classe da hierarquia implementa accept() com exatamente uma linha — toda a lógica da operação fica no visitor, sem tocar nos nós da AST.

// ── Visitor interface ─────────────────────────────────────────
// Declarado ANTES dos elementos porque eles referenciam IVisitor.
// Em TS, forward references funcionam via interface — sem problema de ordem.
interface IVisitor {
  visitNumero(no: Numero): void;
  visitSoma(no: Soma): void;
  visitMultiplicacao(no: Multiplicacao): void;
}

// ── Element interface ─────────────────────────────────────────
interface IExpressao {
  accept(visitor: IVisitor): void;
}

// ── ConcreteElements (nós da AST) ─────────────────────────────
class Numero implements IExpressao {
  constructor(readonly valor: number) {}

  // Double dispatch: chama o método correto do visitor para este tipo.
  accept(visitor: IVisitor): void {
    visitor.visitNumero(this);
  }
}

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

  accept(visitor: IVisitor): void {
    visitor.visitSoma(this);
  }
}

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

  accept(visitor: IVisitor): void {
    visitor.visitMultiplicacao(this);
  }
}

// ── ConcreteVisitor 1: Imprimir ───────────────────────────────
// Monta a representação textual da expressão com parênteses explícitos.
class ImprimirVisitor implements IVisitor {
  private partes: string[] = [];

  visitNumero(no: Numero): void {
    this.partes.push(String(no.valor));
  }

  visitSoma(no: Soma): void {
    this.partes.push('(');
    no.esquerda.accept(this);   // visita o filho esquerdo
    this.partes.push(' + ');
    no.direita.accept(this);    // visita o filho direito
    this.partes.push(')');
  }

  visitMultiplicacao(no: Multiplicacao): void {
    this.partes.push('(');
    no.esquerda.accept(this);
    this.partes.push(' * ');
    no.direita.accept(this);
    this.partes.push(')');
  }

  resultado(): string {
    return this.partes.join('');
  }
}

// ── ConcreteVisitor 2: Calcular ───────────────────────────────
// Avalia a expressão usando uma pilha — sem nenhuma alteração nos nós.
class CalcularVisitor implements IVisitor {
  private pilha: number[] = [];

  visitNumero(no: Numero): void {
    this.pilha.push(no.valor);
  }

  visitSoma(no: Soma): void {
    no.esquerda.accept(this);
    no.direita.accept(this);
    const b = this.pilha.pop()!;
    const a = this.pilha.pop()!;
    this.pilha.push(a + b);
  }

  visitMultiplicacao(no: Multiplicacao): void {
    no.esquerda.accept(this);
    no.direita.accept(this);
    const b = this.pilha.pop()!;
    const a = this.pilha.pop()!;
    this.pilha.push(a * b);
  }

  resultado(): number {
    return this.pilha[0] ?? 0;
  }
}

// ── Uso ──────────────────────────────────────────────────────
// AST que representa: (2 + 3) * 4
const arvore: IExpressao = new Multiplicacao(
  new Soma(new Numero(2), new Numero(3)),
  new Numero(4)
);

// Operação 1: imprimir (sem tocar nos nós da AST)
const imprimir = new ImprimirVisitor();
arvore.accept(imprimir);
console.log(imprimir.resultado());   // ((2 + 3) * 4)

// Operação 2: calcular (sem tocar nos nós da AST)
const calcular = new CalcularVisitor();
arvore.accept(calcular);
console.log(calcular.resultado());   // 20

// A AST pode ser visitada múltiplas vezes por visitors diferentes.
// Nenhuma classe de nó foi modificada para suportar essas operações.

Exemplo 2 — Adicionando novas operações sem tocar na hierarquia

O principal benefício do Visitor: adicionar operações é aberto para extensão. Reutilizando o mesmo AST do Exemplo 1, implementamos dois visitors adicionais — ContarNosVisitor (conta quantos nós tem a AST) e SerializarJsonVisitor (serializa a árvore para JSON) — sem alterar uma única linha de Numero, Soma ou Multiplicacao.

// Reutiliza IVisitor, IExpressao, Numero, Soma e Multiplicacao do Exemplo 1.

// ── ConcreteVisitor 3: Contar nós ────────────────────────────
// Conta o total de nós da AST — folhas (Numero) e internos (Soma, Multiplicacao).
class ContarNosVisitor implements IVisitor {
  private contagem = 0;

  visitNumero(_no: Numero): void {
    this.contagem++;
  }

  visitSoma(no: Soma): void {
    this.contagem++;
    no.esquerda.accept(this);
    no.direita.accept(this);
  }

  visitMultiplicacao(no: Multiplicacao): void {
    this.contagem++;
    no.esquerda.accept(this);
    no.direita.accept(this);
  }

  resultado(): number {
    return this.contagem;
  }
}

// ── ConcreteVisitor 4: Serializar para JSON ───────────────────
// Serializa a AST para uma string JSON sem usar JSON.stringify na raiz.
// Usa a pilha implícita da recursão: cada visit constrói e armazena
// o fragmento JSON do sub-nó antes de compor o nó pai.
class SerializarJsonVisitor implements IVisitor {
  private json = '';

  visitNumero(no: Numero): void {
    this.json = `{"tipo":"Numero","valor":${no.valor}}`;
  }

  visitSoma(no: Soma): void {
    no.esquerda.accept(this);
    const esq = this.json;
    no.direita.accept(this);
    const dir = this.json;
    this.json = `{"tipo":"Soma","esq":${esq},"dir":${dir}}`;
  }

  visitMultiplicacao(no: Multiplicacao): void {
    no.esquerda.accept(this);
    const esq = this.json;
    no.direita.accept(this);
    const dir = this.json;
    this.json = `{"tipo":"Mult","esq":${esq},"dir":${dir}}`;
  }

  resultado(): string {
    return this.json;
  }
}

// ── Uso ──────────────────────────────────────────────────────
// Mesma AST: (2 + 3) * 4
const arvore: IExpressao = new Multiplicacao(
  new Soma(new Numero(2), new Numero(3)),
  new Numero(4)
);

// Visitor 3: contar
const contar = new ContarNosVisitor();
arvore.accept(contar);
console.log(contar.resultado());
// 5  (Multiplicacao + Soma + Numero(2) + Numero(3) + Numero(4))

// Visitor 4: serializar
const serializar = new SerializarJsonVisitor();
arvore.accept(serializar);
console.log(serializar.resultado());
// {"tipo":"Mult",
//   "esq":{"tipo":"Soma",
//     "esq":{"tipo":"Numero","valor":2},
//     "dir":{"tipo":"Numero","valor":3}},
//   "dir":{"tipo":"Numero","valor":4}}

// Zero linhas alteradas em Numero, Soma e Multiplicacao.

Quando usar

  • Quando você precisa adicionar operações frequentes a uma hierarquia estável: se a hierarquia (os tipos de nó) muda raramente mas novas operações (visitors) são adicionadas com frequência, o Visitor mantém o código das operações coeso e as classes de domínio intocadas.
  • Em compiladores e ferramentas de análise: a AST tem tipos de nó estáveis (expressões, declarações, literais), mas os passes (type-checking, inlining, geração de código, análise de fluxo) são adicionados incrementalmente. Cada passe é um Visitor.
  • Para operações que acessam dados de múltiplos tipos sem relacionamento de herança: um visitor de serialização ou de auditoria pode acessar campos internos de tipos completamente diferentes e produzir um resultado unificado — sem precisar que esses tipos compartilhem comportamento.
  • Para separar preocupações em hierarquias complexas: quando misturar múltiplas operações nas classes torna o código difícil de entender, o Visitor concentra cada operação numa única classe coesa.

Quando evitar

  • Quando a hierarquia muda com frequência: adicionar um novo tipo de elemento (ex.: um novo nó de AST) exige atualizar a interface IVisitor e todos os visitors existentes. Se a hierarquia é volátil, o Visitor gera mais trabalho do que economiza — o Open/Closed fica invertido para elementos.
  • Quando as classes precisam proteger seus internos: o Visitor precisa acessar os dados internos dos elementos (campos readonly ou getters). Se o encapsulamento é crítico e os dados não devem ser expostos, o padrão é inadequado — ele essencialmente força cada elemento a ser um DTO para os visitors.
  • Para hierarquias pequenas e poucas operações: com dois tipos de elemento e uma operação, criar interface de visitor, dois accept() e uma classe concreta é overengineering. Um método direto na classe resolve com menos código.

Prós e contras

Prós

  • Aberto para novas operações: adicionar um visitor não toca nas classes da hierarquia.
  • Concentra a lógica de cada operação numa única classe coesa — fácil de localizar, testar e manter.
  • Permite acumular estado durante a visitação (contagem, pilha, buffer de saída) sem poluir os elementos.
  • Pode percorrer hierarquias compostas (Composite) de forma ordenada e recursiva, aplicando transformações complexas.
  • Visitação múltipla: a mesma estrutura pode ser visitada por diferentes visitors de forma independente.

Contras

  • Fechado para novos elementos: adicionar um tipo à hierarquia exige atualizar todos os visitors — o custo inverso ao da abordagem ingênua.
  • Quebra parcialmente o encapsulamento: os elementos precisam expor seus dados internos para que os visitors os acessem.
  • Double dispatch é não-intuitivo para quem não conhece o padrão — o fluxo de execução é menos óbvio do que uma chamada direta.
  • Aumenta o número de classes: cada operação se torna uma classe; hierarquias grandes com muitas operações geram muitos visitors.

Armadilhas comuns

1. Double dispatch obscuro — usando instanceof no lugar de accept()

A tentação mais comum ao aprender o Visitor: implementar o visitor com um bloco if/instanceof em vez de usar accept(). Isso derrota o propósito do padrão e cria um acoplamento explícito ao tipo:

// ERRADO — visitor com instanceof (não é Visitor real):
class CalcularErrado {
  calcular(no: IExpressao): number {
    if (no instanceof Numero)        return no.valor;
    if (no instanceof Soma)          return this.calcular(no.esquerda) + this.calcular(no.direita);
    if (no instanceof Multiplicacao) return this.calcular(no.esquerda) * this.calcular(no.direita);
    throw new Error('Tipo desconhecido');
    // Problema: adicionar um novo tipo de nó não gera erro de compilação —
    // o código simplesmente lança em runtime, sem aviso estático.
  }
}

// CORRETO — o compilador garante que todos os tipos são cobertos:
// Se Divisao for adicionada a IVisitor, todo ConcreteVisitor que
// não implementar visitDivisao() gerará erro de compilação.

Com o double dispatch real, o compilador força todos os visitors a implementar o método para cada novo tipo — o contrato da interface IVisitor atua como checklist estático de cobertura.

2. Hierarquia instável — Open/Closed invertido para elementos

O Visitor troca o eixo do Open/Closed: fica aberto para novas operações (visitors) mas fechado para novos tipos na hierarquia. Quando um projeto está na fase de modelagem e a hierarquia de elementos ainda está mudando, cada novo tipo exige atualizar todos os visitors existentes — o oposto do que o padrão promete. Aplique o Visitor somente quando a hierarquia de elementos for razoavelmente estável.

3. Visitor com estado mutável compartilhado entre visitações

Quando um visitor acumula estado interno (pilha, partes, contagem), ele só pode ser usado uma vez por visitação. Reutilizar a mesma instância em duas árvores diferentes produz resultados incorretos porque o estado da primeira visitação contamina a segunda.

Regra prática: crie uma nova instância do visitor para cada visitação. Se um visitor precisa ser reutilizável, implemente um método reset() que zera o estado interno — e documente esse requisito explicitamente.

4. Visitor vs Strategy — distinção de intenção

A confusão mais frequente entre os dois padrões. O Strategy encapsula um algoritmo intercambiável por objeto: o contexto tem uma estratégia ativa que pode ser trocada, mas cada objeto tem a sua própria. O Visitor encapsula uma operação que percorre uma hierarquia inteira: um único visitor atravessa todos os elementos, chamando o método correto em cada tipo — o algoritmo é distribuído pelos métodos visit*, não por objetos isolados. Use Strategy quando um único objeto precisa de um comportamento intercambiável; use Visitor quando uma operação precisa atravessar e processar múltiplos tipos em uma hierarquia.

Padrões relacionados

O Visitor trabalha em conjunto com padrões que organizam hierarquias e percorrimentos:

O Composite é o parceiro natural do Visitor: o Composite organiza os objetos em estruturas de árvore; o Visitor percorre essa árvore aplicando uma operação em cada nó. O método accept() no nó composto tipicamente delega para os filhos, que por sua vez delegam para os seus filhos — a recursão do Composite e o double dispatch do Visitor se combinam naturalmente. O Iterator é uma alternativa mais simples quando a operação é apenas percorrer e entregar elementos ao chamador, sem processar por tipo; quando é necessário processar cada tipo de elemento diferentemente, o Visitor é mais adequado. O Command encapsula uma ação discreta sobre um receptor específico; o Visitor encapsula uma operação que se distribui por toda uma hierarquia — são padrões complementares quando um sistema tem tanto ações discretas (Command) quanto passes de análise ou transformação sobre estruturas de dados (Visitor).