Pular para o conteúdo

O Visitor é um padrão de projeto comportamental usado para separar algoritmos dos objetos sobre os quais eles operam.

Ele se torna especialmente útil quando existe uma estrutura com vários tipos de elementos e o sistema precisa executar diferentes operações sobre essa estrutura, como exportar dados, validar regras, gerar relatórios ou calcular métricas. Em vez de colocar todos esses comportamentos dentro das classes principais, o Visitor desloca essas responsabilidades para objetos visitantes.

Esse padrão é importante porque ajuda a manter o modelo mais coeso. As classes centrais continuam focadas no que representam, enquanto comportamentos auxiliares podem ser adicionados depois com menos impacto na hierarquia principal.

Imagine um sistema acadêmico que representa um relatório em uma árvore de elementos. Alguns nós são:

  • Texto
  • Tabela
  • Grafico
  • Secao

Com o tempo, novas necessidades aparecem:

  • exportar o relatório para HTML
  • exportar o relatório para Markdown
  • contar palavras e tabelas
  • validar regras de formatação
  • gerar um resumo estatístico

Uma solução inicial seria adicionar métodos como exportarHtml(), exportarMarkdown(), validar() e gerarResumo() em cada classe da hierarquia.

O problema é que essa abordagem costuma criar vários efeitos colaterais:

  • as classes passam a acumular responsabilidades demais
  • cada novo comportamento exige alteração em muitas classes existentes
  • a lógica fica espalhada pela hierarquia
  • comportamentos auxiliares se misturam à regra principal do domínio
  • classes estáveis passam a mudar com frequência por motivos periféricos

O problema central é este: como adicionar novas operações sobre uma estrutura heterogênea de objetos sem transformar as classes da hierarquia em um conjunto inchado de métodos não relacionados ao seu papel principal?

O Visitor resolve isso encapsulando cada novo algoritmo em uma classe separada chamada visitante.

Em vez de o cliente descobrir qual método chamar com vários if ou instanceof, cada elemento oferece um método como aceitar(visitor). Dentro desse método, o próprio elemento redireciona a chamada para a operação correta do visitante.

Normalmente aparecem dois grupos principais:

  1. Elementos: os objetos da estrutura, como Texto, Tabela e Grafico
  2. Visitantes: os algoritmos que operam sobre esses elementos, como ExportadorHtmlVisitor e ValidadorVisitor

O fluxo típico é:

  1. o cliente cria um visitante concreto
  2. o cliente percorre a estrutura de elementos
  3. cada elemento recebe o visitante por meio de aceitar(visitor)
  4. o elemento chama o método específico do visitante, como visitarTexto(this)
  5. o visitante executa a lógica adequada para aquele tipo concreto

Esse mecanismo é conhecido por usar double dispatch. Na prática, a chamada final depende tanto do visitante quanto do tipo concreto do elemento visitado.

Pense em um agente de seguros visitando diferentes tipos de estabelecimento em um bairro.

Quando ele entra em uma residência, oferece um tipo de apólice. Quando visita um banco, oferece outro. Quando entra em uma cafeteria, adapta a proposta novamente. O agente continua sendo o mesmo visitante, mas o comportamento aplicado muda conforme o tipo de local visitado.

No Visitor, acontece algo parecido: o algoritmo é externo ao elemento, mas reage de forma diferente dependendo de qual elemento concreto está sendo visitado.

Use Visitor quando:

  • você precisa executar várias operações sobre uma estrutura de objetos com tipos diferentes
  • a hierarquia de elementos é relativamente estável
  • novos comportamentos surgem com mais frequência do que novos tipos de elementos
  • você quer retirar comportamentos auxiliares das classes de domínio
  • há necessidade de percorrer uma árvore, grafo ou coleção heterogênea aplicando lógica específica por tipo

Exemplos reais em software:

  • exportação de uma árvore de documentos para vários formatos
  • validação semântica de uma árvore sintática
  • geração de métricas em uma estrutura de componentes
  • serialização de objetos heterogêneos
  • relatórios sobre uma árvore Composite

O Visitor costuma ser mal utilizado quando a equipe tenta aplicá-lo em hierarquias que mudam o tempo todo.

Erros comuns:

  • usar Visitor quando existe apenas uma operação simples
  • criar visitantes enormes e pouco coesos
  • expor detalhes internos demais dos elementos só para o visitante funcionar
  • aplicar o padrão em uma hierarquia instável, exigindo atualização constante de todos os visitantes
  • usar Visitor quando um polimorfismo direto dentro da própria classe resolveria de forma mais simples

O principal alerta é este: Visitor favorece a adição de novas operações, mas dificulta a adição de novos tipos de elemento. Se a tendência do sistema é crescer em novos tipos concretos, o padrão pode gerar manutenção excessiva.

Uma implementação incremental pode seguir estes passos:

  1. identifique a hierarquia de elementos que receberá operações externas
  2. crie uma interface Visitor com um método para cada elemento concreto
  3. adicione um método como aceitar(Visitor visitor) na interface base dos elementos
  4. implemente aceitar em cada classe concreta redirecionando para o método correto do visitante
  5. crie visitantes concretos para cada novo comportamento desejado
  6. faça o cliente percorrer a estrutura chamando aceitar(visitor)
  7. se necessário, permita que o visitante acumule estado durante a travessia

Antes de implementar, vale perguntar: o sistema muda mais em operações novas ou em tipos novos de elementos? Se operações novas aparecem com mais frequência, Visitor pode ser uma boa escolha.

No exemplo abaixo, um conjunto de elementos de relatório aceita um visitante responsável por exportar o conteúdo em Markdown.

interface Visitor {
void visitarTexto(Texto texto);
void visitarTabela(Tabela tabela);
}
interface ElementoRelatorio {
void aceitar(Visitor visitor);
}
class Texto implements ElementoRelatorio {
private final String conteudo;
public Texto(String conteudo) {
this.conteudo = conteudo;
}
public String getConteudo() {
return conteudo;
}
@Override
public void aceitar(Visitor visitor) {
visitor.visitarTexto(this);
}
}
class Tabela implements ElementoRelatorio {
private final String titulo;
public Tabela(String titulo) {
this.titulo = titulo;
}
public String getTitulo() {
return titulo;
}
@Override
public void aceitar(Visitor visitor) {
visitor.visitarTabela(this);
}
}
class ExportadorMarkdownVisitor implements Visitor {
@Override
public void visitarTexto(Texto texto) {
System.out.println(texto.getConteudo());
}
@Override
public void visitarTabela(Tabela tabela) {
System.out.println("## Tabela: " + tabela.getTitulo());
}
}

Nesse código, os elementos sabem apenas aceitar um visitante. Toda a lógica de exportação fica concentrada em ExportadorMarkdownVisitor, sem contaminar as classes Texto e Tabela com detalhes de formato.

  • facilita adicionar novos comportamentos sem alterar os elementos existentes
  • melhora a separação de responsabilidades
  • reúne variações do mesmo algoritmo em uma única classe visitante
  • funciona bem sobre estruturas heterogêneas e árvores de objetos
  • pode acumular contexto durante a travessia da estrutura
  • adicionar um novo tipo de elemento exige atualizar todos os visitantes
  • pode aumentar o acoplamento entre visitante e classes concretas
  • às vezes força exposição extra de dados dos elementos
  • adiciona indireção e mais tipos ao desenho
  • pode virar excesso de engenharia em cenários simples

O Visitor se relaciona com outros padrões importantes:

  • Composite: é comum aplicar Visitor sobre árvores compostas por nós e folhas
  • Iterator: um iterador pode ajudar a percorrer a estrutura enquanto o Visitor aplica operações
  • Command: ambos encapsulam comportamento, mas Visitor aplica operações sobre vários tipos de objeto
  • Interpreter: árvores sintáticas frequentemente usam Visitor para validação, otimização ou geração de saída
  • Double Dispatch: é a técnica central que permite selecionar a operação correta sem cadeias de condicionais

Essas relações ajudam a perceber que o Visitor não existe para substituir todo polimorfismo, mas para organizar operações externas sobre estruturas com múltiplos tipos concretos.

Algumas variações comuns incluem:

  • visitantes que acumulam estado interno durante a travessia
  • visitantes que retornam valores em vez de apenas executar efeitos colaterais
  • BaseVisitor com implementações padrão vazias
  • variações acíclicas, em que nem todo visitante precisa conhecer todos os elementos
  • integração com estruturas Composite para percorrer filhos de forma recursiva

Em linguagens e frameworks diferentes, a mecânica pode variar, mas a ideia principal permanece: separar o algoritmo da estrutura visitada.

Agora veja um exemplo mais completo em um sistema acadêmico. Uma prova é composta por questões objetivas, questões discursivas e seções. Queremos gerar uma visão resumida da estrutura sem colocar esse comportamento dentro das classes do domínio.

import java.util.ArrayList;
import java.util.List;
interface Visitor {
void visitarQuestaoObjetiva(QuestaoObjetiva questao);
void visitarQuestaoDiscursiva(QuestaoDiscursiva questao);
void visitarSecao(SecaoProva secao);
}
interface ElementoAvaliacao {
void aceitar(Visitor visitor);
}
class QuestaoObjetiva implements ElementoAvaliacao {
private final String enunciado;
private final int alternativas;
public QuestaoObjetiva(String enunciado, int alternativas) {
this.enunciado = enunciado;
this.alternativas = alternativas;
}
public String getEnunciado() {
return enunciado;
}
public int getAlternativas() {
return alternativas;
}
@Override
public void aceitar(Visitor visitor) {
visitor.visitarQuestaoObjetiva(this);
}
}
class QuestaoDiscursiva implements ElementoAvaliacao {
private final String enunciado;
private final int linhasEsperadas;
public QuestaoDiscursiva(String enunciado, int linhasEsperadas) {
this.enunciado = enunciado;
this.linhasEsperadas = linhasEsperadas;
}
public String getEnunciado() {
return enunciado;
}
public int getLinhasEsperadas() {
return linhasEsperadas;
}
@Override
public void aceitar(Visitor visitor) {
visitor.visitarQuestaoDiscursiva(this);
}
}
class SecaoProva implements ElementoAvaliacao {
private final String titulo;
private final List<ElementoAvaliacao> itens = new ArrayList<>();
public SecaoProva(String titulo) {
this.titulo = titulo;
}
public String getTitulo() {
return titulo;
}
public void adicionar(ElementoAvaliacao item) {
itens.add(item);
}
public List<ElementoAvaliacao> getItens() {
return itens;
}
@Override
public void aceitar(Visitor visitor) {
visitor.visitarSecao(this);
for (ElementoAvaliacao item : itens) {
item.aceitar(visitor);
}
}
}
class GeradorResumoVisitor implements Visitor {
@Override
public void visitarQuestaoObjetiva(QuestaoObjetiva questao) {
System.out.println(
"Objetiva: " + questao.getEnunciado() +
" | alternativas=" + questao.getAlternativas()
);
}
@Override
public void visitarQuestaoDiscursiva(QuestaoDiscursiva questao) {
System.out.println(
"Discursiva: " + questao.getEnunciado() +
" | linhas esperadas=" + questao.getLinhasEsperadas()
);
}
@Override
public void visitarSecao(SecaoProva secao) {
System.out.println("Secao: " + secao.getTitulo());
}
}
public class Main {
public static void main(String[] args) {
SecaoProva prova = new SecaoProva("Avaliação de Design Patterns");
prova.adicionar(new QuestaoObjetiva("Visitor separa algoritmos de quê?", 4));
prova.adicionar(new QuestaoDiscursiva("Explique double dispatch.", 8));
GeradorResumoVisitor visitor = new GeradorResumoVisitor();
prova.aceitar(visitor);
}
}

Nesse exemplo, a estrutura da prova permanece focada em representar os elementos da avaliação. O comportamento de geração de resumo fica separado em GeradorResumoVisitor. Se amanhã o sistema precisar de um ExportadorMarkdownVisitor ou de um ValidadorDeRubricaVisitor, basta criar novos visitantes sem alterar a hierarquia existente.