Visitor
Introdução
Seção intitulada “Introdução”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.
Problema
Seção intitulada “Problema”Imagine um sistema acadêmico que representa um relatório em uma árvore de elementos. Alguns nós são:
TextoTabelaGraficoSecao
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?
Solução
Seção intitulada “Solução”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:
- Elementos: os objetos da estrutura, como
Texto,TabelaeGrafico - Visitantes: os algoritmos que operam sobre esses elementos, como
ExportadorHtmlVisitoreValidadorVisitor
O fluxo típico é:
- o cliente cria um visitante concreto
- o cliente percorre a estrutura de elementos
- cada elemento recebe o visitante por meio de
aceitar(visitor) - o elemento chama o método específico do visitante, como
visitarTexto(this) - 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.
Analogia
Seção intitulada “Analogia”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.
Aplicabilidade
Seção intitulada “Aplicabilidade”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
Anti-padrão / Mau uso
Seção intitulada “Anti-padrão / Mau uso”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.
Como implementar
Seção intitulada “Como implementar”Uma implementação incremental pode seguir estes passos:
- identifique a hierarquia de elementos que receberá operações externas
- crie uma interface
Visitorcom um método para cada elemento concreto - adicione um método como
aceitar(Visitor visitor)na interface base dos elementos - implemente
aceitarem cada classe concreta redirecionando para o método correto do visitante - crie visitantes concretos para cada novo comportamento desejado
- faça o cliente percorrer a estrutura chamando
aceitar(visitor) - 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.
Exemplo em código
Seção intitulada “Exemplo em código”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
Contras
Seção intitulada “Contras”- 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
Relações com outros padrões/conceitos
Seção intitulada “Relações com outros padrões/conceitos”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.
Implementações alternativas (quando aplicável)
Seção intitulada “Implementações alternativas (quando aplicável)”Algumas variações comuns incluem:
- visitantes que acumulam estado interno durante a travessia
- visitantes que retornam valores em vez de apenas executar efeitos colaterais
BaseVisitorcom 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.
Exemplo completo
Seção intitulada “Exemplo completo”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.