ComportamentalAvançado7 min de leitura9 seções1994, Marco de referência: a ideia é mais velha que a data

Visitor

Separa 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.

Em 10 segundos

Cada operação vira uma classe com um método por tipo de nó, e os nós só sabem dizer 'me visite'. Adicionar 'imprimir' ou 'otimizar' não toca em nenhuma classe existente — mas adicionar um tipo novo de nó obriga a mexer em todos os visitantes.

  • #duplo-despacho
  • #arvore
  • #compilador
  • #gof
Analogia

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.

01Conceito

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.

02Conceito

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.

Como se encaixa
  1. Cliente
  2. 1º despacho: tipo do nóSoma.aceitar(v)
  3. 2º despacho: operaçãoImpressor.visitarSoma()
  4. recursão pela árvoreFilhos aceitam de novo
São dois saltos: o nó resolve quem ele é, o visitante resolve o que fazer. Essa dupla resolução é o que a linguagem não faz sozinha e o padrão fornece.
Sem o padrão × com o padrão

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.

Não existe lado vencedor: é o problema da expressão. A pergunta a responder antes de escolher é qual eixo vai crescer mais nos próximos anos, tipos ou operações.
03Anatomia

Navegue pelas camadas

Camada 1/3Nó

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.

04Estrutura

Estrutura

Diagrama de classes
05Implementação

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())); // 6
06No mundo real

Casos 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.

Passo a passoComo montar o Visitor
  1. 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. 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. 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. 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 instanceof se 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.

08Cuidado

Armadilhas

  1. 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.

  2. 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.

  3. 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.

09Decisão

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 chama visitor — 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