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:
-
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. -
ConcreteElement: as classes concretas da hierarquia
(
Numero,Soma,Multiplicacao). Cada uma implementaaccept()chamando o métodovisitcorrespondente do visitor. Expõem seus dados internos (via getters ou camposreadonly) para que os visitors possam acessá-los. -
Visitor (interface): declara um método
visitpara 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. - 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.
<?php
// ── Interfaces (forward declarations não são necessárias em PHP) ──
interface IVisitor
{
public function visitNumero(Numero $no): void;
public function visitSoma(Soma $no): void;
public function visitMultiplicacao(Multiplicacao $no): void;
}
interface IExpressao
{
public function accept(IVisitor $visitor): void;
}
// ── ConcreteElements ──────────────────────────────────────────
class Numero implements IExpressao
{
public function __construct(public readonly float $valor) {}
public function accept(IVisitor $visitor): void
{
$visitor->visitNumero($this);
}
}
class Soma implements IExpressao
{
public function __construct(
public readonly IExpressao $esquerda,
public readonly IExpressao $direita
) {}
public function accept(IVisitor $visitor): void
{
$visitor->visitSoma($this);
}
}
class Multiplicacao implements IExpressao
{
public function __construct(
public readonly IExpressao $esquerda,
public readonly IExpressao $direita
) {}
public function accept(IVisitor $visitor): void
{
$visitor->visitMultiplicacao($this);
}
}
// ── ConcreteVisitor 1: Imprimir ───────────────────────────────
class ImprimirVisitor implements IVisitor
{
/** @var string[] */
private array $partes = [];
public function visitNumero(Numero $no): void
{
$this->partes[] = (string) $no->valor;
}
public function visitSoma(Soma $no): void
{
$this->partes[] = '(';
$no->esquerda->accept($this);
$this->partes[] = ' + ';
$no->direita->accept($this);
$this->partes[] = ')';
}
public function visitMultiplicacao(Multiplicacao $no): void
{
$this->partes[] = '(';
$no->esquerda->accept($this);
$this->partes[] = ' * ';
$no->direita->accept($this);
$this->partes[] = ')';
}
public function resultado(): string
{
return implode('', $this->partes);
}
}
// ── ConcreteVisitor 2: Calcular ───────────────────────────────
class CalcularVisitor implements IVisitor
{
/** @var float[] */
private array $pilha = [];
public function visitNumero(Numero $no): void
{
$this->pilha[] = $no->valor;
}
public function visitSoma(Soma $no): void
{
$no->esquerda->accept($this);
$no->direita->accept($this);
$b = array_pop($this->pilha);
$a = array_pop($this->pilha);
$this->pilha[] = $a + $b;
}
public function visitMultiplicacao(Multiplicacao $no): void
{
$no->esquerda->accept($this);
$no->direita->accept($this);
$b = array_pop($this->pilha);
$a = array_pop($this->pilha);
$this->pilha[] = $a * $b;
}
public function resultado(): float
{
return $this->pilha[0] ?? 0.0;
}
}
// ── Uso ──────────────────────────────────────────────────────
// AST: (2 + 3) * 4
$arvore = new Multiplicacao(
new Soma(new Numero(2), new Numero(3)),
new Numero(4)
);
$imprimir = new ImprimirVisitor();
$arvore->accept($imprimir);
echo $imprimir->resultado() . "\n"; // ((2 + 3) * 4)
$calcular = new CalcularVisitor();
$arvore->accept($calcular);
echo $calcular->resultado() . "\n"; // 20
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.
<?php
// Reutiliza IVisitor, IExpressao, Numero, Soma e Multiplicacao do Exemplo 1.
// ── ConcreteVisitor 3: Contar nós ────────────────────────────
class ContarNosVisitor implements IVisitor
{
private int $contagem = 0;
public function visitNumero(Numero $no): void
{
$this->contagem++;
}
public function visitSoma(Soma $no): void
{
$this->contagem++;
$no->esquerda->accept($this);
$no->direita->accept($this);
}
public function visitMultiplicacao(Multiplicacao $no): void
{
$this->contagem++;
$no->esquerda->accept($this);
$no->direita->accept($this);
}
public function resultado(): int
{
return $this->contagem;
}
}
// ── ConcreteVisitor 4: Serializar para JSON ───────────────────
class SerializarJsonVisitor implements IVisitor
{
private string $json = '';
public function visitNumero(Numero $no): void
{
$this->json = sprintf('{"tipo":"Numero","valor":%s}', $no->valor);
}
public function visitSoma(Soma $no): void
{
$no->esquerda->accept($this);
$esq = $this->json;
$no->direita->accept($this);
$dir = $this->json;
$this->json = sprintf('{"tipo":"Soma","esq":%s,"dir":%s}', $esq, $dir);
}
public function visitMultiplicacao(Multiplicacao $no): void
{
$no->esquerda->accept($this);
$esq = $this->json;
$no->direita->accept($this);
$dir = $this->json;
$this->json = sprintf('{"tipo":"Mult","esq":%s,"dir":%s}', $esq, $dir);
}
public function resultado(): string
{
return $this->json;
}
}
// ── Uso ──────────────────────────────────────────────────────
$arvore = new Multiplicacao(
new Soma(new Numero(2), new Numero(3)),
new Numero(4)
);
// Visitor 3: contar
$contar = new ContarNosVisitor();
$arvore->accept($contar);
echo $contar->resultado() . "\n";
// 5 (Multiplicacao + Soma + Numero(2) + Numero(3) + Numero(4))
// Visitor 4: serializar
$serializar = new SerializarJsonVisitor();
$arvore->accept($serializar);
echo $serializar->resultado() . "\n";
// {"tipo":"Mult",
// "esq":{"tipo":"Soma",
// "esq":{"tipo":"Numero","valor":2},
// "dir":{"tipo":"Numero","valor":3}},
// "dir":{"tipo":"Numero","valor":4}}
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
IVisitore 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
readonlyou 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).