O perito que visita a fábrica
Uma fábrica tem tornos, prensas e esteiras. Em vez de ensinar cada máquina a se autoavaliar para segurança, contabilidade e eficiência energética, chegam peritos: o de segurança sabe o que olhar em cada tipo de máquina, o contador sabe outra coisa. Contratar um perito novo não exige modificar máquina nenhuma — mas comprar um tipo de máquina inédito exige treinar todos os peritos.
O problema
Estruturas heterogêneas — a árvore sintática de uma linguagem, os blocos de um documento, os nós de um relatório — atraem operações. Cada nova exigência de negócio quer percorrer a mesma árvore fazendo algo diferente.
Se cada operação vira um método nos nós, as classes crescem sem parar e passam a misturar representação com dezenas de comportamentos não relacionados.
O padrão inverte a direção da extensibilidade, e entender isso é entender o Visitor. Um método por nó facilita adicionar TIPOS (a classe nova traz seus métodos) e dificulta adicionar OPERAÇÕES (é preciso editar todas as classes). O Visitor faz o oposto: operações entram de graça, tipos custam caro.
Esse dilema tem nome — o problema da expressão. Nenhuma das duas organizações resolve os dois lados ao mesmo tempo em linguagens orientadas a objetos convencionais. Escolher entre método e visitante é escolher qual eixo você espera que cresça.
O mecanismo que viabiliza tudo é o duplo despacho. Linguagens comuns despacham por um tipo só (o do receptor). Como precisamos escolher o comportamento pela combinação nó × operação, o aceitar faz o primeiro salto — o nó, que conhece o próprio tipo, chama o método específico do visitante. É um truque para simular algo que a linguagem não oferece nativamente.
Quando a linguagem já resolve
Boa parte da cerimônia do Visitor existe para contornar limitações que linguagens modernas não têm mais. Vale checar antes de adotá-lo.
Com uniões discriminadas e casamento de padrões (TypeScript com switch sobre um campo tipo, Rust, Kotlin com sealed, Scala), uma operação nova é uma função com um match — sem aceitar, sem interface de visitante, e ainda com verificação de exaustividade do compilador, que avisa quando um tipo novo não foi tratado.
Essa checagem de exaustividade é exatamente a garantia que o Visitor oferece por outro caminho: se você adiciona um método à interface do visitante, todos os visitantes deixam de compilar até tratá-lo. A diferença é a quantidade de código necessária para chegar lá.
A recomendação prática: em linguagens com união fechada e casamento de padrões, prefira a função com match. Reserve o Visitor para linguagens sem esse recurso, ou quando os visitantes precisam carregar estado próprio e configuração, caso em que a classe se paga.
- Cliente
- 1º despacho: tipo do nóSoma.aceitar(v)
- 2º despacho: operaçãoImpressor.visitarSoma()
- recursão pela árvoreFilhos aceitam de novo
Operação como visitante
- adicionar uma operação é uma classe nova, sem tocar em nada
- a lógica de 'imprimir' fica inteira num arquivo
- os nós ficam magros, só com representação
- adicionar um tipo obriga a mexer em todos os visitantes
Bom quando os tipos são estáveis e as operações crescem — compiladores, interpretadores e processadores de documento vivem disso.
Navegue pelas camadas
Camada 1/3Nó
use ← →Camada 1
Nó
representa, e aceita visitas
Guarda os dados e implementa `aceitar`, cuja única função é chamar o método do visitante correspondente ao seu tipo. Não conhece nenhuma operação concreta.
No código
aceitar<T>(v: Visitante<T>): T { return v.visitarSoma(this) }Se violar: nó com lógica de negócio dentro do `aceitar` recria o acoplamento que o padrão veio eliminar.
Estrutura
Código
interface Visitante<T> {
visitarNumero(n: Numero): T;
visitarSoma(s: Soma): T;
}
interface No {
aceitar<T>(v: Visitante<T>): T;
}
class Numero implements No {
constructor(readonly valor: number) {}
// duplo despacho: o no sabe QUAL metodo do visitante chamar
aceitar<T>(v: Visitante<T>): T {
return v.visitarNumero(this);
}
}
class Soma implements No {
constructor(readonly esq: No, readonly dir: No) {}
aceitar<T>(v: Visitante<T>): T {
return v.visitarSoma(this);
}
}
// Operacao 1: calcular — nenhuma mudanca nas classes de no
class Avaliador implements Visitante<number> {
visitarNumero(n: Numero): number {
return n.valor;
}
visitarSoma(s: Soma): number {
return s.esq.aceitar(this) + s.dir.aceitar(this);
}
}
// Operacao 2: imprimir — de novo, sem tocar nos nos
class Impressor implements Visitante<string> {
visitarNumero(n: Numero): string {
return String(n.valor);
}
visitarSoma(s: Soma): string {
return `(${s.esq.aceitar(this)} + ${s.dir.aceitar(this)})`;
}
}
const expr = new Soma(new Numero(1), new Soma(new Numero(2), new Numero(3)));
console.log(expr.aceitar(new Impressor())); // (1 + (2 + 3))
console.log(expr.aceitar(new Avaliador())); // 6from abc import ABC, abstractmethod
class No(ABC):
@abstractmethod
def aceitar(self, v): ...
class Numero(No):
def __init__(self, valor):
self.valor = valor
# duplo despacho: o no sabe QUAL metodo chamar
def aceitar(self, v):
return v.visitar_numero(self)
class Soma(No):
def __init__(self, esq, dir):
self.esq, self.dir = esq, dir
def aceitar(self, v):
return v.visitar_soma(self)
# Operacao 1: calcular — sem tocar nas classes de no
class Avaliador:
def visitar_numero(self, n):
return n.valor
def visitar_soma(self, s):
return s.esq.aceitar(self) + s.dir.aceitar(self)
# Operacao 2: imprimir — de novo, sem tocar nos nos
class Impressor:
def visitar_numero(self, n):
return str(n.valor)
def visitar_soma(self, s):
return f"({s.esq.aceitar(self)} + {s.dir.aceitar(self)})"
expr = Soma(Numero(1), Soma(Numero(2), Numero(3)))
print(expr.aceitar(Impressor())) # (1 + (2 + 3))
print(expr.aceitar(Avaliador())) # 6Casos de uso
Compilador sobre a árvore sintática
Cenário
Uma linguagem tem uns quarenta tipos de nó sintático, estáveis desde a especificação, e precisa de análise de tipos, otimização, geração de código e formatação.
Como o padrão entra
Cada fase é um visitante. Adicionar uma otimização nova é criar uma classe, sem tocar na definição da árvore — e cada fase fica legível inteira, de cima a baixo.
Trade-off: Um recurso novo na linguagem (um tipo de nó inédito) obriga a atualizar todos os visitantes de uma vez. Em compiladores maduros isso é raro; numa linguagem em desenho, é constante e doloroso.
Exportação de documento em vários formatos
Cenário
Um editor representa o documento como árvore de blocos (parágrafo, lista, tabela, imagem) e precisa exportar para HTML, Markdown e PDF.
Como o padrão entra
Um visitante por formato de saída. Cada um sabe converter todos os tipos de bloco, e o código de HTML não se mistura com o de PDF.
Trade-off: Um tipo de bloco novo (um embed de vídeo) quebra a compilação de todos os exportadores — o que é bom (nada passa esquecido) e ruim (uma feature pequena vira mudança em vários arquivos).
Análise estática de configuração
Cenário
Uma ferramenta de infraestrutura precisa validar segurança, estimar custo e detectar recursos órfãos sobre a mesma árvore de configuração.
Como o padrão entra
Cada análise é um visitante que acumula seus achados durante a travessia. Rodar só a de segurança ou todas juntas é escolha de quem chama.
Trade-off: Visitantes com estado acumulado não são reutilizáveis entre execuções nem seguros em paralelo — cada análise precisa de uma instância nova, o que é fácil de esquecer.
- 1
Congelar os tipos de nó
O padrão só se paga com hierarquia estável. Se tipos novos entram todo mês, prefira método nos nós ou casamento de padrões.
- 2
Dar aceitar a cada nó
O nó só sabe chamar o método certo do visitante (`v.visitarSoma(this)`). Sem lógica de negócio dentro do aceitar.
- 3
Um método por tipo no visitante
A interface do visitante lista todos os tipos. Operação nova = classe nova; tipo novo = alterar todos os visitantes.
- 4
Percorrer pela recursão
O visitante (ou o nó) decide a ordem dos filhos. Cada operação fica inteira num arquivo — sem espalhar 'imprimir' por dez classes.
O Visitor que virou instanceof
Compila, roda, passa no code review. E quebra depois.
Existe uma interface Visitante e um Avaliador — mas em vez do duplo despacho, o visitante faz cadeia de instanceof. A cerimônia ficou; a verificação do compilador sumiu.
interface Visitante {
visitar(no: No): number;
}
class Avaliador implements Visitante {
visitar(no: No): number {
if (no instanceof Numero) return no.valor;
if (no instanceof Soma)
return this.visitar(no.esq) + this.visitar(no.dir);
throw new Error("tipo desconhecido");
}
}
// Tipo novo passa despercebido ate runtime. O duplo despacho
// foi abandonado — e a verificacao do compilador junto.O que quebra
- Ao adicionar um tipo de nóNada quebra na compilação — o erro só aparece em runtime com 'tipo desconhecido'.
- Na revisãoO
instanceofse multiplica em cada visitante; cada um trata um subconjunto diferente de tipos. - No testeCobrir um nó novo exige lembrar de atualizar cada cadeia manualmente — exatamente o que o padrão deveria impedir.
A saída: Volte ao duplo despacho: aceitar no nó chama visitarNumero / visitarSoma. Tipo novo na interface força todos os visitantes a compilar de novo.
Armadilhas
Adotar com tipos instáveis
É o erro que torna o padrão odiado. Se a estrutura ainda está sendo desenhada e tipos entram todo mês, cada um deles vira uma alteração em todos os visitantes existentes. O Visitor só se paga depois que o conjunto de tipos assentou — antes disso, ele multiplica o custo de cada mudança.
Cair no instanceof
Quando o duplo despacho incomoda, aparece a tentação de escrever um visitante genérico com uma cadeia de
instanceof. Isso devolve toda a cerimônia sem nenhum dos ganhos: some a verificação do compilador, e um tipo novo passa despercebido até dar erro em produção.Quebrar o encapsulamento dos nós
Para o visitante fazer seu trabalho, ele precisa ler os dados do nó — o que empurra tudo para público. A estrutura acaba virando um saco de dados anêmico. É um custo real do padrão, aceitável em ASTs (que são dados mesmo) e problemático em modelos de domínio com invariantes a proteger.
Quando usar × quando evitar
Use quando
3- Os tipos são estáveis e as operações crescem.
- Você quer cada operação inteira num arquivo só.
- As operações não pertencem ao domínio dos nós.
Evite quando
3- Tipos novos aparecem com frequência.
- A linguagem tem união fechada e casamento de padrões.
- São poucas operações, naturais aos objetos.
Testar o que ficou
3 armadilhas de Visitor 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.
Plugins do BabelO objeto que você exporta se chamavisitor— um método por tipo de nó da árvore sintática.Regras do ESLintCada regra visita os nós que lhe interessam sem que o parser saiba que a regra existe.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Operação nova sem mudar os elementos.
no.accept(visitorTypeCheck);O que ele cobra
2 saltos a mais entre a chamada e o efeito.
- Adicionar um tipo novo à hierarquia obriga a alterar todos os visitantes existentes
- O double dispatch entre elemento e visitante confunde quem lê o padrão pela primeira vez
Não vale se: a hierarquia de tipos muda com frequência — aí o Visitor inverte o custo e cada tipo novo dói em todo visitante.
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.
- IteratorOferece uma forma de percorrer os elementos de uma coleção sequencialmente sem expor como ela é construída por dentro — e permite mais de uma travessia simultânea.
- StateTransforma cada estado de um objeto em uma classe própria: o comportamento muda conforme o estado atual, e é o próprio estado que decide a próxima transição.