Visitor Pattern em Java: Operações Flexíveis em Estruturas de Objetos
Aprenda como implementar o Visitor Pattern em Java para adicionar novas operações a estruturas de objetos sem modificar suas classes. Descubra como este padrão comportamental pode aumentar a flexibilidade e extensibilidade do seu código.
Introdução
O Visitor Pattern (Padrão Visitante) é um padrão de design comportamental que permite definir novas operações sem alterar as classes dos elementos sobre os quais elas operam. Este padrão é especialmente útil quando você precisa aplicar diferentes operações em uma estrutura de objetos complexa, mantendo o código organizado e seguindo o princípio da responsabilidade única.
Neste post, exploraremos o conceito do Visitor Pattern, suas vantagens e como implementá-lo em Java com exemplos práticos de processamento de documentos e cálculos em estruturas hierárquicas.
O que é o Visitor Pattern?
O Visitor Pattern resolve o problema de adicionar novas operações a uma hierarquia de classes existente sem modificar essas classes. Em vez de adicionar métodos diretamente nas classes dos elementos, o padrão encapsula cada operação em classes visitantes separadas.
Principais componentes:
- Visitor (Visitante): Interface que declara métodos de visita para cada tipo de elemento concreto.
- ConcreteVisitor (Visitante Concreto): Implementa operações específicas para cada tipo de elemento.
- Element (Elemento): Interface que declara um método
accept
que recebe um visitante. - ConcreteElement (Elemento Concreto): Implementa o método
accept
e define a estrutura dos dados. - ObjectStructure (Estrutura de Objetos): Coleção de elementos que podem ser visitados.
Quando usar o Visitor Pattern?
- Quando você precisa executar operações em objetos de uma estrutura complexa sem modificar suas classes.
- Para adicionar novas funcionalidades a uma hierarquia de classes existente.
- Quando você tem muitas operações relacionadas mas distintas para aplicar nos mesmos elementos.
- Para manter operações relacionadas juntas em uma classe visitante.
- Quando a estrutura de objetos é estável, mas você frequentemente adiciona novas operações.
Exemplo Prático em Java
Vamos implementar um sistema de processamento de documentos onde diferentes tipos de elementos podem ser processados por vários visitantes:
1. Criando a Interface Visitor
A interface DocumentVisitor
define as operações que podem ser realizadas em diferentes tipos de elementos:
public interface DocumentVisitor {
void visitParagraph(Paragraph paragraph);
void visitImage(Image image);
void visitTable(Table table);
}
2. Criando a Interface Element
A interface DocumentElement
define o contrato para elementos que podem ser visitados:
public interface DocumentElement {
void accept(DocumentVisitor visitor);
}
3. Implementando os Elementos Concretos
Cada tipo de elemento implementa a interface e define como aceitar um visitante:
// Elemento Parágrafo
public class Paragraph implements DocumentElement {
private String text;
private String fontFamily;
private int fontSize;
public Paragraph(String text, String fontFamily, int fontSize) {
this.text = text;
this.fontFamily = fontFamily;
this.fontSize = fontSize;
}
@Override
public void accept(DocumentVisitor visitor) {
visitor.visitParagraph(this);
}
// Getters
public String getText() { return text; }
public String getFontFamily() { return fontFamily; }
public int getFontSize() { return fontSize; }
}
// Elemento Imagem
public class Image implements DocumentElement {
private String src;
private int width;
private int height;
public Image(String src, int width, int height) {
this.src = src;
this.width = width;
this.height = height;
}
@Override
public void accept(DocumentVisitor visitor) {
visitor.visitImage(this);
}
// Getters
public String getSrc() { return src; }
public int getWidth() { return width; }
public int getHeight() { return height; }
}
// Elemento Tabela
public class Table implements DocumentElement {
private int rows;
private int columns;
private String[][] data;
public Table(int rows, int columns, String[][] data) {
this.rows = rows;
this.columns = columns;
this.data = data;
}
@Override
public void accept(DocumentVisitor visitor) {
visitor.visitTable(this);
}
// Getters
public int getRows() { return rows; }
public int getColumns() { return columns; }
public String[][] getData() { return data; }
}
4. Implementando os Visitantes Concretos
Agora vamos criar diferentes visitantes para diferentes operações:
// Visitante para exportar para HTML
public class HtmlExportVisitor implements DocumentVisitor {
private StringBuilder html = new StringBuilder();
@Override
public void visitParagraph(Paragraph paragraph) {
html.append("<p style=\"font-family: ")
.append(paragraph.getFontFamily())
.append("; font-size: ")
.append(paragraph.getFontSize())
.append("px;\">")
.append(paragraph.getText())
.append("</p>\n");
}
@Override
public void visitImage(Image image) {
html.append("<img src=\"")
.append(image.getSrc())
.append("\" width=\"")
.append(image.getWidth())
.append("\" height=\"")
.append(image.getHeight())
.append("\" />\n");
}
@Override
public void visitTable(Table table) {
html.append("<table border=\"1\">\n");
String[][] data = table.getData();
for (int i = 0; i < table.getRows(); i++) {
html.append(" <tr>\n");
for (int j = 0; j < table.getColumns(); j++) {
html.append(" <td>").append(data[i][j]).append("</td>\n");
}
html.append(" </tr>\n");
}
html.append("</table>\n");
}
public String getHtml() {
return html.toString();
}
}
// Visitante para calcular estatísticas do documento
public class StatisticsVisitor implements DocumentVisitor {
private int wordCount = 0;
private int imageCount = 0;
private int tableCount = 0;
@Override
public void visitParagraph(Paragraph paragraph) {
String[] words = paragraph.getText().split("\\s+");
wordCount += words.length;
}
@Override
public void visitImage(Image image) {
imageCount++;
}
@Override
public void visitTable(Table table) {
tableCount++;
// Contar palavras nas células da tabela
String[][] data = table.getData();
for (int i = 0; i < table.getRows(); i++) {
for (int j = 0; j < table.getColumns(); j++) {
if (data[i][j] != null) {
String[] words = data[i][j].split("\\s+");
wordCount += words.length;
}
}
}
}
// Getters para as estatísticas
public int getWordCount() { return wordCount; }
public int getImageCount() { return imageCount; }
public int getTableCount() { return tableCount; }
}
5. Criando a Estrutura de Objetos
Uma classe para gerenciar a coleção de elementos do documento:
import java.util.ArrayList;
import java.util.List;
public class Document {
private List<DocumentElement> elements = new ArrayList<>();
public void addElement(DocumentElement element) {
elements.add(element);
}
public void accept(DocumentVisitor visitor) {
for (DocumentElement element : elements) {
element.accept(visitor);
}
}
public List<DocumentElement> getElements() {
return elements;
}
}
6. Exemplo de Uso
public class VisitorPatternDemo {
public static void main(String[] args) {
// Criando um documento com diferentes elementos
Document document = new Document();
document.addElement(new Paragraph(
"Este é um parágrafo de exemplo.", "Arial", 14));
document.addElement(new Image("logo.png", 200, 100));
document.addElement(new Paragraph(
"Outro parágrafo do documento.", "Times New Roman", 12));
String[][] tableData = {
{"Nome", "Idade", "Cidade"},
{"João", "25", "São Paulo"},
{"Maria", "30", "Rio de Janeiro"}
};
document.addElement(new Table(3, 3, tableData));
// Exportando para HTML
HtmlExportVisitor htmlVisitor = new HtmlExportVisitor();
document.accept(htmlVisitor);
System.out.println("HTML Export:");
System.out.println(htmlVisitor.getHtml());
// Calculando estatísticas
StatisticsVisitor statsVisitor = new StatisticsVisitor();
document.accept(statsVisitor);
System.out.println("\nEstatísticas do Documento:");
System.out.println("Palavras: " + statsVisitor.getWordCount());
System.out.println("Imagens: " + statsVisitor.getImageCount());
System.out.println("Tabelas: " + statsVisitor.getTableCount());
}
}
Vantagens do Visitor Pattern
- Separação de Responsabilidades: Operações são separadas da estrutura dos objetos.
- Facilidade para Adicionar Operações: Novas operações podem ser adicionadas criando novos visitantes.
- Código Organizado: Operações relacionadas ficam agrupadas em uma classe visitante.
- Reutilização: Visitantes podem ser reutilizados em diferentes estruturas de objetos.
- Flexibilidade: Permite diferentes comportamentos sem modificar as classes existentes.
Quando evitar o Visitor Pattern?
- Quando a hierarquia de classes muda frequentemente (adição de novos tipos de elementos).
- Para estruturas simples onde o padrão adiciona complexidade desnecessária.
- Quando você tem poucas operações e muitos tipos de elementos.
- Se o desempenho é crítico e o overhead das chamadas polimórficas é significativo.
- Quando a estrutura de dados é instável ou evolui constantemente.
Padrões Relacionados
- Iterator: Para percorrer estruturas de objetos de forma sistemática.
- Composite: Frequentemente usado junto com Visitor para estruturas hierárquicas.
- Command: Ambos encapsulam operações, mas Command foca em solicitações.
- Strategy: Similar na separação de algoritmos, mas Strategy funciona com um algoritmo por vez.
Conclusão
O Visitor Pattern é uma solução poderosa para adicionar novas operações a estruturas de objetos complexas sem modificar as classes existentes. Ele promove a separação de responsabilidades e facilita a manutenção e extensão do código, sendo especialmente útil em sistemas que processam estruturas hierárquicas ou documentos.
Este padrão é fundamental em Java para sistemas de compiladores, processadores de documentos, análise de árvores sintáticas e qualquer aplicação que precise aplicar múltiplas operações em estruturas de dados complexas. Se você trabalha com hierarquias de objetos e precisa de flexibilidade para adicionar novas funcionalidades, o Visitor Pattern é uma ferramenta indispensável para manter seu código organizado e extensível.
Gostou deste post? Continue acompanhando para mais conteúdos sobre padrões de design e desenvolvimento em Java!