Cleibson Gomes

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:

  1. Visitor (Visitante): Interface que declara métodos de visita para cada tipo de elemento concreto.
  2. ConcreteVisitor (Visitante Concreto): Implementa operações específicas para cada tipo de elemento.
  3. Element (Elemento): Interface que declara um método accept que recebe um visitante.
  4. ConcreteElement (Elemento Concreto): Implementa o método accept e define a estrutura dos dados.
  5. 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

  1. Separação de Responsabilidades: Operações são separadas da estrutura dos objetos.
  2. Facilidade para Adicionar Operações: Novas operações podem ser adicionadas criando novos visitantes.
  3. Código Organizado: Operações relacionadas ficam agrupadas em uma classe visitante.
  4. Reutilização: Visitantes podem ser reutilizados em diferentes estruturas de objetos.
  5. 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!

blog@nosbielc.com

Made with ❤️ in Quebec, CA.