Dlaczego Visitor pattern rozwiązuje realny problem
Wyobraź sobie system zarządzania dokumentami, gdzie masz różne typy dokumentów (PDF, Word, Excel), a każdy wymaga innych operacji: eksport do XML, obliczanie rozmiaru, walidacja. Tradycyjne podejście oznaczałoby dodawanie metod do każdej klasy dokumentu, co narusza zasadę otwarte/zamknięte (Open/Closed Principle).
Visitor pattern pozwala separować operacje od struktur danych, umożliwiając dodawanie nowych funkcjonalności bez modyfikacji istniejących klas.
Co się nauczysz:
- Jak zaimplementować Visitor pattern w praktycznych scenariuszach
- Kiedy używać Visitor pattern, a kiedy go unikać
- Double dispatch i jak działa w Javie
- Visitor pattern w przetwarzaniu AST i strukturach drzewiastych
- Performance implications i alternatywne rozwiązania
Struktura Visitor Pattern
Visitor składa się z dwóch głównych komponentów:
Element – interfejs z metodą accept() przyjmującą visitor
Podstawowa implementacja
// Element interface public interface DocumentElement { void accept(DocumentVisitor visitor); String getName(); } // Visitor interface public interface DocumentVisitor { void visit(PdfDocument pdf); void visit(WordDocument word); void visit(ExcelDocument excel); } // Concrete Elements public class PdfDocument implements DocumentElement { private final String fileName; private final int pageCount; public PdfDocument(String fileName, int pageCount) { this.fileName = fileName; this.pageCount = pageCount; } @Override public void accept(DocumentVisitor visitor) { visitor.visit(this); // Double dispatch! } @Override public String getName() { return fileName; } public int getPageCount() { return pageCount; } }
Concrete Visitors – operacje biznesowe
// Size calculation visitor public class DocumentSizeVisitor implements DocumentVisitor { private long totalSize = 0; @Override public void visit(PdfDocument pdf) { // PDF: ~200KB per page + metadata totalSize += pdf.getPageCount() * 200 * 1024 + 50 * 1024; System.out.println(pdf.getName() + ": ~" + (pdf.getPageCount() * 200 + 50) + "KB"); } @Override public void visit(WordDocument word) { // Word: ~100KB per page + formatting totalSize += word.getWordCount() / 250 * 100 * 1024 + 100 * 1024; System.out.println(word.getName() + ": ~" + (word.getWordCount() / 250 * 100 + 100) + "KB"); } @Override public void visit(ExcelDocument excel) { // Excel: zależny od liczby arkuszy i komórek long sheetSize = excel.getSheetCount() * 50 * 1024; long cellSize = excel.getCellCount() * 20; totalSize += sheetSize + cellSize; System.out.println(excel.getName() + ": ~" + (sheetSize + cellSize) / 1024 + "KB"); } public long getTotalSize() { return totalSize; } } // Export visitor public class XmlExportVisitor implements DocumentVisitor { private final StringBuilder xmlOutput = new StringBuilder(); @Override public void visit(PdfDocument pdf) { xmlOutput.append("") .append(" "); } @Override public void visit(WordDocument word) { xmlOutput.append("").append(pdf.getName()).append(" ") .append("").append(pdf.getPageCount()).append(" ") .append("portable ") .append("") .append(" "); } @Override public void visit(ExcelDocument excel) { xmlOutput.append("").append(word.getName()).append(" ") .append("").append(word.getWordCount()).append(" ") .append("true ") .append("") .append(" "); } public String getXml() { return xmlOutput.toString(); } }").append(excel.getName()).append(" ") .append("").append(excel.getSheetCount()).append(" ") .append("").append(excel.getCellCount()).append(" ") .append("
Visitor w strukturach drzewiastych
Visitor pattern świetnie sprawdza się w przetwarzaniu Abstract Syntax Trees (AST), gdzie mamy hierarchię węzłów reprezentujących różne konstrukcje językowe.
// AST Node hierarchy public abstract class AstNode { public abstract void accept(AstVisitor visitor); } public class BinaryOperationNode extends AstNode { private final AstNode left; private final AstNode right; private final String operator; public BinaryOperationNode(AstNode left, String operator, AstNode right) { this.left = left; this.operator = operator; this.right = right; } @Override public void accept(AstVisitor visitor) { visitor.visit(this); } // getters... public AstNode getLeft() { return left; } public AstNode getRight() { return right; } public String getOperator() { return operator; } } public class NumberNode extends AstNode { private final double value; public NumberNode(double value) { this.value = value; } @Override public void accept(AstVisitor visitor) { visitor.visit(this); } public double getValue() { return value; } }
AST Visitor – interpreter wzorców
public interface AstVisitor { void visit(BinaryOperationNode node); void visit(NumberNode node); } // Calculator visitor - oblicza wartość wyrażenia public class CalculatorVisitor implements AstVisitor { private double result = 0; @Override public void visit(BinaryOperationNode node) { // Recursive descent - odwiedzamy dzieci node.getLeft().accept(this); double leftValue = result; node.getRight().accept(this); double rightValue = result; // Wykonujemy operację switch (node.getOperator()) { case "+": result = leftValue + rightValue; break; case "-": result = leftValue - rightValue; break; case "*": result = leftValue * rightValue; break; case "/": if (rightValue == 0) throw new ArithmeticException("Division by zero"); result = leftValue / rightValue; break; default: throw new UnsupportedOperationException("Unknown operator: " + node.getOperator()); } } @Override public void visit(NumberNode node) { result = node.getValue(); } public double getResult() { return result; } } // Pretty printer visitor - generuje czytelną reprezentację public class PrettyPrintVisitor implements AstVisitor { private final StringBuilder output = new StringBuilder(); @Override public void visit(BinaryOperationNode node) { output.append("("); node.getLeft().accept(this); output.append(" ").append(node.getOperator()).append(" "); node.getRight().accept(this); output.append(")"); } @Override public void visit(NumberNode node) { output.append(node.getValue()); } public String getOutput() { return output.toString(); } }
Praktyczne użycie – system zarządzania dokumentami
public class DocumentManager { private final Listdocuments = new ArrayList<>(); public void addDocument(DocumentElement document) { documents.add(document); } public void processDocuments(DocumentVisitor visitor) { for (DocumentElement document : documents) { document.accept(visitor); } } public static void main(String[] args) { DocumentManager manager = new DocumentManager(); // Dodajemy dokumenty manager.addDocument(new PdfDocument("report.pdf", 15)); manager.addDocument(new WordDocument("proposal.docx", 2500)); manager.addDocument(new ExcelDocument("budget.xlsx", 3, 450)); // Obliczamy rozmiary System.out.println("=== ROZMIARY DOKUMENTÓW ==="); DocumentSizeVisitor sizeVisitor = new DocumentSizeVisitor(); manager.processDocuments(sizeVisitor); System.out.println("Łączny rozmiar: " + sizeVisitor.getTotalSize() / 1024 + "KB\n"); // Eksportujemy do XML System.out.println("=== EKSPORT XML ==="); XmlExportVisitor xmlVisitor = new XmlExportVisitor(); manager.processDocuments(xmlVisitor); System.out.println(xmlVisitor.getXml()); // Testujemy AST System.out.println("\n=== AST CALCULATOR ==="); // Tworzymy wyrażenie: (3 + 4) * 2 AstNode expression = new BinaryOperationNode( new BinaryOperationNode( new NumberNode(3), "+", new NumberNode(4) ), "*", new NumberNode(2) ); CalculatorVisitor calculator = new CalculatorVisitor(); expression.accept(calculator); System.out.println("Wynik: " + calculator.getResult()); PrettyPrintVisitor printer = new PrettyPrintVisitor(); expression.accept(printer); System.out.println("Wyrażenie: " + printer.getOutput()); } }
Wyzwania i ograniczenia
Problem z dodawaniem nowych typów elementów
Największą wadą Visitor pattern jest trudność w dodawaniu nowych typów elementów. Każdy nowy typ wymaga modyfikacji wszystkich istniejących visitors.
Performance considerations
// Benchmark porównujący visitor vs instanceof public class PerformanceBenchmark { private static final int ITERATIONS = 1_000_000; // Tradycyjne podejście z instanceof public static double calculateSizeTraditional(DocumentElement element) { if (element instanceof PdfDocument) { PdfDocument pdf = (PdfDocument) element; return pdf.getPageCount() * 200 + 50; } else if (element instanceof WordDocument) { WordDocument word = (WordDocument) element; return word.getWordCount() / 250 * 100 + 100; } return 0; } // Visitor approach public static double calculateSizeVisitor(DocumentElement element) { DocumentSizeVisitor visitor = new DocumentSizeVisitor(); element.accept(visitor); return visitor.getTotalSize() / 1024.0; } // Wyniki testów (przybliżone dla 2018): // instanceof: ~15ms dla 1M operacji // visitor: ~22ms dla 1M operacji // Overhead: ~47% (ale lepsze OOP design) }
Visitor vs alternatywne rozwiązania
Podejście | Zalety | Wady | Kiedy używać |
---|---|---|---|
Visitor Pattern | Separacja operacji, łatwe dodawanie nowych operacji | Trudne dodawanie typów, większa złożoność | Stabilna hierarchia, częste nowe operacje |
instanceof + casting | Prostsze, szybsze | Narusza OOP, trudne w utrzymaniu | Proste przypadki, jednorazowe operacje |
Method dispatch | Naturalne OOP, czytelne | Mieszanie logiki z danymi | Operacje związane z domeną obiektu |
Reflection | Bardzo elastyczne | Wolne, prone to errors | Frameworki, generic processing |
Modern Java alternatives
Java 8+ oferuje alternatywy używając functional programming:
// Functional approach z Java 8 public class FunctionalDocumentProcessor { private final Map, Function > processors = new HashMap<>(); public FunctionalDocumentProcessor() { processors.put(PdfDocument.class, doc -> { PdfDocument pdf = (PdfDocument) doc; return "PDF: " + pdf.getName() + " (" + pdf.getPageCount() + " pages)"; }); processors.put(WordDocument.class, doc -> { WordDocument word = (WordDocument) doc; return "Word: " + word.getName() + " (" + word.getWordCount() + " words)"; }); } public String process(DocumentElement document) { Function processor = processors.get(document.getClass()); return processor != null ? processor.apply(document) : "Unknown document type"; } }
Best practices
1. Używaj generic visitors dla większej elastyczności
public interface GenericVisitor{ T visit(PdfDocument pdf); T visit(WordDocument word); T visit(ExcelDocument excel); } public class DocumentSizeVisitor implements GenericVisitor { @Override public Long visit(PdfDocument pdf) { return (long) (pdf.getPageCount() * 200 * 1024 + 50 * 1024); } // ... inne implementacje }
2. Rozważ composite visitor dla złożonych operacji
public class CompositeDocumentVisitor implements DocumentVisitor { private final Listvisitors = new ArrayList<>(); public void addVisitor(DocumentVisitor visitor) { visitors.add(visitor); } @Override public void visit(PdfDocument pdf) { visitors.forEach(visitor -> visitor.visit(pdf)); } // ... pozostałe metody }
Visitor pattern gdy masz stabilną hierarchię klas ale często dodajesz nowe operacje. instanceof dla prostych, jednorazowych operacji lub gdy performance jest krytyczne.
Częściowo tak – visitor musi mieć dostęp do internal state elementów. Można to ograniczyć przez gettery lub package-protected metody, ale trade-off między enkapsulacją a separacją concerns zawsze istnieje.
Double dispatch to mechanizm wyboru metody na podstawie typów dwóch obiektów (element i visitor). Java ma tylko single dispatch, więc Visitor pattern symuluje double dispatch przez metodę accept().
Możesz użyć checked exceptions w interfejsie visitor, Optional return types, lub visitor z error handling state. Najlepiej jest jednolite podejście w całej hierarchii.
Tak, ale wymaga ostrożności. Podklasy muszą override metodę accept() i wywołać odpowiednią metodę visit(). Alternatywnie można użyć refleksji do automatycznego dispatching.
Około 30-50% overhead vs instanceof w 2018. Dwa virtual method calls vs jeden instanceof + cast. JVM optymalizacje mogą to poprawić w hot spots.
Tak! Visitor + Composite dla hierarchii obiektów, Visitor + Strategy dla różnych algorytmów, Visitor + Factory dla tworzenia visitors. Bardzo elastyczny wzorzec.
Przydatne zasoby:
- Java 8 API Documentation
- Java Design Patterns – GitHub
- Refactoring Guru – Visitor Pattern
- Gang of Four Patterns in Java
🚀 Zadanie dla Ciebie
Zaimplementuj system analizy kodu źródłowego używając Visitor pattern:
- Stwórz hierarchię AST nodes: ClassNode, MethodNode, VariableNode
- Zaimplementuj visitors: LineCounterVisitor, ComplexityAnalyzerVisitor, CodeFormatterVisitor
- Dodaj obsługę błędów i logging
- Porównaj performance z prostym instanceof approach
- Napisz testy jednostkowe dla wszystkich visitors
Bonus: Dodaj generic visitor interface i composite visitor dla łączenia operacji.
Czy już używałeś Visitor pattern w swoich projektach? Jakie były największe wyzwania i korzyści? Podziel się doświadczeniami w komentarzach!