Introduction
The Visitor Pattern is a behavioral design pattern that lets you separate algorithms from the objects on which they operate. It allows you to add new operations to existing object structures without modifying the structures themselves.
Core Concept
Definition
Visitor Pattern represents an operation to be performed on elements of an object structure. It lets you define a new operation without changing the classes of the elements on which it operates.
When to Use
- When you need to perform operations on objects of different types in a complex object structure
- When operations need to be added frequently without modifying object classes
- When you want to keep related operations together
- When the object structure is stable but operations change frequently
Basic Implementation
1. Element Interface and Concrete Elements
// Element interface public interface Shape { void accept(ShapeVisitor visitor); } // Concrete elements public class Circle implements Shape { private double radius; private String name; public Circle(double radius, String name) { this.radius = radius; this.name = name; } public double getRadius() { return radius; } public String getName() { return name; } @Override public void accept(ShapeVisitor visitor) { visitor.visitCircle(this); } } public class Rectangle implements Shape { private double width; private double height; private String name; public Rectangle(double width, double height, String name) { this.width = width; this.height = height; this.name = name; } public double getWidth() { return width; } public double getHeight() { return height; } public String getName() { return name; } @Override public void accept(ShapeVisitor visitor) { visitor.visitRectangle(this); } } public class Triangle implements Shape { private double base; private double height; private String name; public Triangle(double base, double height, String name) { this.base = base; this.height = height; this.name = name; } public double getBase() { return base; } public double getHeight() { return height; } public String getName() { return name; } @Override public void accept(ShapeVisitor visitor) { visitor.visitTriangle(this); } } 2. Visitor Interface and Concrete Visitors
// Visitor interface public interface ShapeVisitor { void visitCircle(Circle circle); void visitRectangle(Rectangle rectangle); void visitTriangle(Triangle triangle); } // Concrete visitor for area calculation public class AreaCalculator implements ShapeVisitor { private double totalArea = 0; @Override public void visitCircle(Circle circle) { double area = Math.PI * circle.getRadius() * circle.getRadius(); System.out.printf("Circle '%s' area: %.2f%n", circle.getName(), area); totalArea += area; } @Override public void visitRectangle(Rectangle rectangle) { double area = rectangle.getWidth() * rectangle.getHeight(); System.out.printf("Rectangle '%s' area: %.2f%n", rectangle.getName(), area); totalArea += area; } @Override public void visitTriangle(Triangle triangle) { double area = 0.5 * triangle.getBase() * triangle.getHeight(); System.out.printf("Triangle '%s' area: %.2f%n", triangle.getName(), area); totalArea += area; } public double getTotalArea() { return totalArea; } } // Concrete visitor for perimeter calculation public class PerimeterCalculator implements ShapeVisitor { private double totalPerimeter = 0; @Override public void visitCircle(Circle circle) { double perimeter = 2 * Math.PI * circle.getRadius(); System.out.printf("Circle '%s' perimeter: %.2f%n", circle.getName(), perimeter); totalPerimeter += perimeter; } @Override public void visitRectangle(Rectangle rectangle) { double perimeter = 2 * (rectangle.getWidth() + rectangle.getHeight()); System.out.printf("Rectangle '%s' perimeter: %.2f%n", rectangle.getName(), perimeter); totalPerimeter += perimeter; } @Override public void visitTriangle(Triangle triangle) { // For simplicity, assuming equilateral triangle double perimeter = 3 * triangle.getBase(); System.out.printf("Triangle '%s' perimeter: %.2f%n", triangle.getName(), perimeter); totalPerimeter += perimeter; } public double getTotalPerimeter() { return totalPerimeter; } } 3. Object Structure and Client Code
import java.util.*; // Object structure public class ShapeCollection { private List<Shape> shapes = new ArrayList<>(); public void addShape(Shape shape) { shapes.add(shape); } public void removeShape(Shape shape) { shapes.remove(shape); } public void accept(ShapeVisitor visitor) { for (Shape shape : shapes) { shape.accept(visitor); } } public int size() { return shapes.size(); } } // Client code public class VisitorPatternDemo { public static void main(String[] args) { // Create shape collection ShapeCollection shapes = new ShapeCollection(); shapes.addShape(new Circle(5.0, "Big Circle")); shapes.addShape(new Rectangle(4.0, 6.0, "Medium Rectangle")); shapes.addShape(new Triangle(3.0, 4.0, "Small Triangle")); shapes.addShape(new Circle(2.0, "Tiny Circle")); System.out.println("=== Area Calculation ==="); AreaCalculator areaCalculator = new AreaCalculator(); shapes.accept(areaCalculator); System.out.printf("Total area: %.2f%n%n", areaCalculator.getTotalArea()); System.out.println("=== Perimeter Calculation ==="); PerimeterCalculator perimeterCalculator = new PerimeterCalculator(); shapes.accept(perimeterCalculator); System.out.printf("Total perimeter: %.2f%n%n", perimeterCalculator.getTotalPerimeter()); System.out.println("=== Drawing Shapes ==="); ShapeDrawer drawer = new ShapeDrawer(); shapes.accept(drawer); } } // Another visitor for drawing public class ShapeDrawer implements ShapeVisitor { @Override public void visitCircle(Circle circle) { System.out.printf("🔵 Drawing circle '%s' with radius %.1f%n", circle.getName(), circle.getRadius()); } @Override public void visitRectangle(Rectangle rectangle) { System.out.printf("🟦 Drawing rectangle '%s' (%.1f x %.1f)%n", rectangle.getName(), rectangle.getWidth(), rectangle.getHeight()); } @Override public void visitTriangle(Triangle triangle) { System.out.printf("🔺 Drawing triangle '%s' (base: %.1f, height: %.1f)%n", triangle.getName(), triangle.getBase(), triangle.getHeight()); } } Advanced Implementation
1. Generic Visitor with Return Types
// Generic visitor interface with return type public interface GenericShapeVisitor<T> { T visitCircle(Circle circle); T visitRectangle(Rectangle rectangle); T visitTriangle(Triangle triangle); } // Generic area calculator returning Double public class GenericAreaCalculator implements GenericShapeVisitor<Double> { @Override public Double visitCircle(Circle circle) { return Math.PI * circle.getRadius() * circle.getRadius(); } @Override public Double visitRectangle(Rectangle rectangle) { return rectangle.getWidth() * rectangle.getHeight(); } @Override public Double visitTriangle(Triangle triangle) { return 0.5 * triangle.getBase() * triangle.getHeight(); } } // Generic JSON exporter returning String public class JsonExporter implements GenericShapeVisitor<String> { @Override public String visitCircle(Circle circle) { return String.format( "{\"type\": \"circle\", \"name\": \"%s\", \"radius\": %.2f}", circle.getName(), circle.getRadius() ); } @Override public String visitRectangle(Rectangle rectangle) { return String.format( "{\"type\": \"rectangle\", \"name\": \"%s\", \"width\": %.2f, \"height\": %.2f}", rectangle.getName(), rectangle.getWidth(), rectangle.getHeight() ); } @Override public String visitTriangle(Triangle triangle) { return String.format( "{\"type\": \"triangle\", \"name\": \"%s\", \"base\": %.2f, \"height\": %.2f}", triangle.getName(), triangle.getBase(), triangle.getHeight() ); } } // Enhanced shape interface with generic accept public interface GenericShape { <T> T accept(GenericShapeVisitor<T> visitor); } // Updated concrete elements public class GenericCircle extends Circle implements GenericShape { public GenericCircle(double radius, String name) { super(radius, name); } @Override public <T> T accept(GenericShapeVisitor<T> visitor) { return visitor.visitCircle(this); } } // Enhanced collection public class GenericShapeCollection { private List<GenericShape> shapes = new ArrayList<>(); public void addShape(GenericShape shape) { shapes.add(shape); } public <T> List<T> accept(GenericShapeVisitor<T> visitor) { List<T> results = new ArrayList<>(); for (GenericShape shape : shapes) { results.add(shape.accept(visitor)); } return results; } public <T> T accept(GenericShapeVisitor<T> visitor, T initial, java.util.function.BinaryOperator<T> accumulator) { T result = initial; for (GenericShape shape : shapes) { result = accumulator.apply(result, shape.accept(visitor)); } return result; } } // Demo for generic visitors public class GenericVisitorDemo { public static void main(String[] args) { GenericShapeCollection shapes = new GenericShapeCollection(); shapes.addShape(new GenericCircle(5.0, "Big Circle")); shapes.addShape(new GenericRectangle(4.0, 6.0, "Medium Rectangle")); System.out.println("=== Individual Areas ==="); GenericAreaCalculator areaCalc = new GenericAreaCalculator(); List<Double> areas = shapes.accept(areaCalc); areas.forEach(area -> System.out.printf("Area: %.2f%n", area)); System.out.println("\n=== Total Area ==="); Double totalArea = shapes.accept(areaCalc, 0.0, Double::sum); System.out.printf("Total area: %.2f%n", totalArea); System.out.println("\n=== JSON Export ==="); JsonExporter jsonExporter = new JsonExporter(); List<String> jsonObjects = shapes.accept(jsonExporter); jsonObjects.forEach(System.out::println); } } 2. Composite Pattern with Visitor
// Composite element public class ShapeGroup implements Shape { private String name; private List<Shape> children = new ArrayList<>(); public ShapeGroup(String name) { this.name = name; } public void addShape(Shape shape) { children.add(shape); } public void removeShape(Shape shape) { children.remove(shape); } public List<Shape> getChildren() { return new ArrayList<>(children); } public String getName() { return name; } @Override public void accept(ShapeVisitor visitor) { // If you have a specific visit method for groups if (visitor instanceof GroupAwareVisitor) { ((GroupAwareVisitor) visitor).visitShapeGroup(this); } else { // Default behavior: visit all children for (Shape shape : children) { shape.accept(visitor); } } } } // Enhanced visitor interface for groups public interface GroupAwareVisitor extends ShapeVisitor { void visitShapeGroup(ShapeGroup group); } // Composite-aware area calculator public class CompositeAreaCalculator implements GroupAwareVisitor { private double totalArea = 0; private int indentLevel = 0; @Override public void visitCircle(Circle circle) { double area = Math.PI * circle.getRadius() * circle.getRadius(); printIndented(String.format("Circle '%s' area: %.2f", circle.getName(), area)); totalArea += area; } @Override public void visitRectangle(Rectangle rectangle) { double area = rectangle.getWidth() * rectangle.getHeight(); printIndented(String.format("Rectangle '%s' area: %.2f", rectangle.getName(), area)); totalArea += area; } @Override public void visitTriangle(Triangle triangle) { double area = 0.5 * triangle.getBase() * triangle.getHeight(); printIndented(String.format("Triangle '%s' area: %.2f", triangle.getName(), area)); totalArea += area; } @Override public void visitShapeGroup(ShapeGroup group) { printIndented("Group: " + group.getName()); indentLevel++; for (Shape shape : group.getChildren()) { shape.accept(this); } indentLevel--; } private void printIndented(String message) { String indent = " ".repeat(indentLevel); System.out.println(indent + message); } public double getTotalArea() { return totalArea; } } // Demo for composite with visitor public class CompositeVisitorDemo { public static void main(String[] args) { // Create composite structure ShapeGroup mainGroup = new ShapeGroup("Main Group"); ShapeGroup subgroup1 = new ShapeGroup("Subgroup 1"); subgroup1.addShape(new Circle(3.0, "Circle in Subgroup 1")); subgroup1.addShape(new Rectangle(2.0, 4.0, "Rectangle in Subgroup 1")); ShapeGroup subgroup2 = new ShapeGroup("Subgroup 2"); subgroup2.addShape(new Triangle(3.0, 4.0, "Triangle in Subgroup 2")); subgroup2.addShape(new Circle(2.0, "Small Circle in Subgroup 2")); mainGroup.addShape(subgroup1); mainGroup.addShape(subgroup2); mainGroup.addShape(new Rectangle(5.0, 3.0, "Standalone Rectangle")); System.out.println("=== Composite Area Calculation ==="); CompositeAreaCalculator calculator = new CompositeAreaCalculator(); mainGroup.accept(calculator); System.out.printf("%nTotal area: %.2f%n", calculator.getTotalArea()); } } Real-World Examples
1. Abstract Syntax Tree (AST) Processing
// AST nodes for a simple expression language public interface ASTNode { <T> T accept(ASTVisitor<T> visitor); } public class NumberNode implements ASTNode { private final int value; public NumberNode(int value) { this.value = value; } public int getValue() { return value; } @Override public <T> T accept(ASTVisitor<T> visitor) { return visitor.visitNumber(this); } } public class BinaryOpNode implements ASTNode { private final ASTNode left; private final String operator; private final ASTNode right; public BinaryOpNode(ASTNode left, String operator, ASTNode right) { this.left = left; this.operator = operator; this.right = right; } public ASTNode getLeft() { return left; } public String getOperator() { return operator; } public ASTNode getRight() { return right; } @Override public <T> T accept(ASTVisitor<T> visitor) { return visitor.visitBinaryOp(this); } } public class VariableNode implements ASTNode { private final String name; public VariableNode(String name) { this.name = name; } public String getName() { return name; } @Override public <T> T accept(ASTVisitor<T> visitor) { return visitor.visitVariable(this); } } // AST Visitor interface public interface ASTVisitor<T> { T visitNumber(NumberNode node); T visitBinaryOp(BinaryOpNode node); T visitVariable(VariableNode node); } // Evaluator visitor public class ExpressionEvaluator implements ASTVisitor<Integer> { private final Map<String, Integer> variables; public ExpressionEvaluator(Map<String, Integer> variables) { this.variables = variables; } @Override public Integer visitNumber(NumberNode node) { return node.getValue(); } @Override public Integer visitBinaryOp(BinaryOpNode node) { int left = node.getLeft().accept(this); int right = node.getRight().accept(this); switch (node.getOperator()) { case "+": return left + right; case "-": return left - right; case "*": return left * right; case "/": return left / right; default: throw new IllegalArgumentException("Unknown operator: " + node.getOperator()); } } @Override public Integer visitVariable(VariableNode node) { if (!variables.containsKey(node.getName())) { throw new IllegalArgumentException("Unknown variable: " + node.getName()); } return variables.get(node.getName()); } } // Code generator visitor public class CodeGenerator implements ASTVisitor<String> { @Override public String visitNumber(NumberNode node) { return String.valueOf(node.getValue()); } @Override public String visitBinaryOp(BinaryOpNode node) { String left = node.getLeft().accept(this); String right = node.getRight().accept(this); return "(" + left + " " + node.getOperator() + " " + right + ")"; } @Override public String visitVariable(VariableNode node) { return node.getName(); } } // Type checker visitor public class TypeChecker implements ASTVisitor<String> { @Override public String visitNumber(NumberNode node) { return "int"; } @Override public String visitBinaryOp(BinaryOpNode node) { String leftType = node.getLeft().accept(this); String rightType = node.getRight().accept(this); if (!leftType.equals(rightType)) { throw new RuntimeException("Type mismatch: " + leftType + " vs " + rightType); } return leftType; // Binary operations preserve type } @Override public String visitVariable(VariableNode node) { // In a real implementation, this would look up in a symbol table return "int"; // Assume all variables are int for simplicity } } // AST Demo public class ASTDemo { public static void main(String[] args) { // Build AST for expression: (x + 5) * (y - 3) ASTNode expression = new BinaryOpNode( new BinaryOpNode( new VariableNode("x"), "+", new NumberNode(5) ), "*", new BinaryOpNode( new VariableNode("y"), "-", new NumberNode(3) ) ); // Set variable values Map<String, Integer> variables = new HashMap<>(); variables.put("x", 10); variables.put("y", 8); System.out.println("=== Expression Evaluation ==="); ExpressionEvaluator evaluator = new ExpressionEvaluator(variables); int result = expression.accept(evaluator); System.out.println("Result: " + result); System.out.println("\n=== Code Generation ==="); CodeGenerator codeGen = new CodeGenerator(); String code = expression.accept(codeGen); System.out.println("Generated code: " + code); System.out.println("\n=== Type Checking ==="); TypeChecker typeChecker = new TypeChecker(); String type = expression.accept(typeChecker); System.out.println("Expression type: " + type); } } 2. Document Processing System
// Document elements public interface DocumentElement { void accept(DocumentVisitor visitor); } public class Paragraph implements DocumentElement { private String text; private String style; public Paragraph(String text, String style) { this.text = text; this.style = style; } public String getText() { return text; } public String getStyle() { return style; } @Override public void accept(DocumentVisitor visitor) { visitor.visitParagraph(this); } } public class Heading implements DocumentElement { private String text; private int level; public Heading(String text, int level) { this.text = text; this.level = level; } public String getText() { return text; } public int getLevel() { return level; } @Override public void accept(DocumentVisitor visitor) { visitor.visitHeading(this); } } public class Image implements DocumentElement { private String src; private String altText; private int width; private int height; public Image(String src, String altText, int width, int height) { this.src = src; this.altText = altText; this.width = width; this.height = height; } public String getSrc() { return src; } public String getAltText() { return altText; } public int getWidth() { return width; } public int getHeight() { return height; } @Override public void accept(DocumentVisitor visitor) { visitor.visitImage(this); } } public class Table implements DocumentElement { private List<List<String>> data; private String caption; public Table(List<List<String>> data, String caption) { this.data = data; this.caption = caption; } public List<List<String>> getData() { return data; } public String getCaption() { return caption; } @Override public void accept(DocumentVisitor visitor) { visitor.visitTable(this); } } // Document visitor interface public interface DocumentVisitor { void visitParagraph(Paragraph paragraph); void visitHeading(Heading heading); void visitImage(Image image); void visitTable(Table table); } // HTML export visitor public class HtmlExporter implements DocumentVisitor { private StringBuilder html = new StringBuilder(); @Override public void visitParagraph(Paragraph paragraph) { html.append("<p class=\"").append(paragraph.getStyle()).append("\">") .append(paragraph.getText()) .append("</p>\n"); } @Override public void visitHeading(Heading heading) { html.append("<h").append(heading.getLevel()).append(">") .append(heading.getText()) .append("</h").append(heading.getLevel()).append(">\n"); } @Override public void visitImage(Image image) { html.append("<img src=\"").append(image.getSrc()) .append("\" alt=\"").append(image.getAltText()) .append("\" width=\"").append(image.getWidth()) .append("\" height=\"").append(image.getHeight()) .append("\">\n"); } @Override public void visitTable(Table table) { html.append("<table>\n"); if (table.getCaption() != null) { html.append("<caption>").append(table.getCaption()).append("</caption>\n"); } for (List<String> row : table.getData()) { html.append("<tr>"); for (String cell : row) { html.append("<td>").append(cell).append("</td>"); } html.append("</tr>\n"); } html.append("</table>\n"); } public String getHtml() { return html.toString(); } public void reset() { html = new StringBuilder(); } } // Word count visitor public class WordCountVisitor implements DocumentVisitor { private int wordCount = 0; @Override public void visitParagraph(Paragraph paragraph) { wordCount += countWords(paragraph.getText()); } @Override public void visitHeading(Heading heading) { wordCount += countWords(heading.getText()); } @Override public void visitImage(Image image) { // Images don't contribute to word count } @Override public void visitTable(Table table) { for (List<String> row : table.getData()) { for (String cell : row) { wordCount += countWords(cell); } } if (table.getCaption() != null) { wordCount += countWords(table.getCaption()); } } private int countWords(String text) { if (text == null || text.trim().isEmpty()) { return 0; } return text.trim().split("\\s+").length; } public int getWordCount() { return wordCount; } } // Document structure public class Document { private List<DocumentElement> elements = new ArrayList<>(); private String title; public Document(String title) { this.title = title; } public void addElement(DocumentElement element) { elements.add(element); } public void accept(DocumentVisitor visitor) { for (DocumentElement element : elements) { element.accept(visitor); } } public String getTitle() { return title; } } // Document processing demo public class DocumentProcessingDemo { public static void main(String[] args) { // Create a document Document document = new Document("Sample Document"); document.addElement(new Heading("Introduction", 1)); document.addElement(new Paragraph("This is a sample document demonstrating the Visitor pattern.", "normal")); document.addElement(new Paragraph("The pattern helps separate operations from document structure.", "normal")); document.addElement(new Heading("Data Summary", 2)); List<List<String>> tableData = Arrays.asList( Arrays.asList("Category", "Count", "Percentage"), Arrays.asList("Type A", "150", "30%"), Arrays.asList("Type B", "250", "50%"), Arrays.asList("Type C", "100", "20%") ); document.addElement(new Table(tableData, "Data Distribution")); document.addElement(new Image("chart.png", "Data distribution chart", 400, 300)); System.out.println("=== HTML Export ==="); HtmlExporter htmlExporter = new HtmlExporter(); document.accept(htmlExporter); System.out.println(htmlExporter.getHtml()); System.out.println("=== Word Count ==="); WordCountVisitor wordCounter = new WordCountVisitor(); document.accept(wordCounter); System.out.println("Total words: " + wordCounter.getWordCount()); } } Advanced Patterns with Visitor
1. Visitor with State Accumulation
// Stateful visitor for complex calculations public class StatisticalVisitor implements ShapeVisitor { private int shapeCount = 0; private double totalArea = 0; private double totalPerimeter = 0; private double minArea = Double.MAX_VALUE; private double maxArea = Double.MIN_VALUE; private Map<String, Integer> typeCount = new HashMap<>(); @Override public void visitCircle(Circle circle) { double area = Math.PI * circle.getRadius() * circle.getRadius(); double perimeter = 2 * Math.PI * circle.getRadius(); processShape("Circle", area, perimeter); } @Override public void visitRectangle(Rectangle rectangle) { double area = rectangle.getWidth() * rectangle.getHeight(); double perimeter = 2 * (rectangle.getWidth() + rectangle.getHeight()); processShape("Rectangle", area, perimeter); } @Override public void visitTriangle(Triangle triangle) { double area = 0.5 * triangle.getBase() * triangle.getHeight(); double perimeter = 3 * triangle.getBase(); // Equilateral assumption processShape("Triangle", area, perimeter); } private void processShape(String type, double area, double perimeter) { shapeCount++; totalArea += area; totalPerimeter += perimeter; minArea = Math.min(minArea, area); maxArea = Math.max(maxArea, area); typeCount.put(type, typeCount.getOrDefault(type, 0) + 1); } public void printStatistics() { System.out.println("=== Shape Statistics ==="); System.out.println("Total shapes: " + shapeCount); System.out.printf("Total area: %.2f%n", totalArea); System.out.printf("Total perimeter: %.2f%n", totalPerimeter); System.out.printf("Average area: %.2f%n", totalArea / shapeCount); System.out.printf("Min area: %.2f%n", minArea); System.out.printf("Max area: %.2f%n", maxArea); System.out.println("Shape type distribution: " + typeCount); } } 2. Visitor with External Dependencies
// Visitor that uses external services public class ExportVisitor implements ShapeVisitor { private Graphics2D graphics; private int xOffset = 0; private int yOffset = 0; public ExportVisitor(Graphics2D graphics) { this.graphics = graphics; } public void setOffset(int x, int y) { this.xOffset = x; this.yOffset = y; } @Override public void visitCircle(Circle circle) { int radius = (int) circle.getRadius(); int x = xOffset; int y = yOffset; // Simulate drawing System.out.printf("Drawing circle at (%d, %d) with radius %d%n", x, y, radius); graphics.drawOval(x - radius, y - radius, radius * 2, radius * 2); yOffset += radius * 2 + 10; } @Override public void visitRectangle(Rectangle rectangle) { int width = (int) rectangle.getWidth(); int height = (int) rectangle.getHeight(); int x = xOffset; int y = yOffset; // Simulate drawing System.out.printf("Drawing rectangle at (%d, %d) with size %dx%d%n", x, y, width, height); graphics.drawRect(x, y, width, height); yOffset += height + 10; } @Override public void visitTriangle(Triangle triangle) { int base = (int) triangle.getBase(); int height = (int) triangle.getHeight(); int x = xOffset; int y = yOffset; // Simulate drawing System.out.printf("Drawing triangle at (%d, %d) with base %d and height %d%n", x, y, base, height); int[] xPoints = {x, x + base, x + base / 2}; int[] yPoints = {y + height, y + height, y}; graphics.drawPolygon(xPoints, yPoints, 3); yOffset += height + 10; } } // Mock Graphics2D for demonstration class MockGraphics2D { public void drawOval(int x, int y, int width, int height) { // Mock implementation } public void drawRect(int x, int y, int width, int height) { // Mock implementation } public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) { // Mock implementation } } Testing Visitor Pattern
Unit Testing Visitors
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.BeforeEach; import static org.junit.jupiter.api.Assertions.*; public class VisitorTest { private ShapeCollection shapes; private Circle circle; private Rectangle rectangle; private Triangle triangle; @BeforeEach void setUp() { shapes = new ShapeCollection(); circle = new Circle(5.0, "Test Circle"); rectangle = new Rectangle(4.0, 6.0, "Test Rectangle"); triangle = new Triangle(3.0, 4.0, "Test Triangle"); shapes.addShape(circle); shapes.addShape(rectangle); shapes.addShape(triangle); } @Test void testAreaCalculator() { AreaCalculator calculator = new AreaCalculator(); shapes.accept(calculator); double expectedArea = (Math.PI * 25) + (4 * 6) + (0.5 * 3 * 4); assertEquals(expectedArea, calculator.getTotalArea(), 0.001); } @Test void testPerimeterCalculator() { PerimeterCalculator calculator = new PerimeterCalculator(); shapes.accept(calculator); double expectedPerimeter = (2 * Math.PI * 5) + (2 * (4 + 6)) + (3 * 3); assertEquals(expectedPerimeter, calculator.getTotalPerimeter(), 0.001); } @Test void testStatisticalVisitor() { StatisticalVisitor visitor = new StatisticalVisitor(); shapes.accept(visitor); // Should have counted all shapes // Additional assertions can be added for other statistics assertTrue(visitor.getShapeCount() > 0); } @Test void testGenericAreaCalculator() { GenericShapeCollection genericShapes = new GenericShapeCollection(); genericShapes.addShape(new GenericCircle(5.0, "Test Circle")); GenericAreaCalculator calculator = new GenericAreaCalculator(); List<Double> areas = genericShapes.accept(calculator); assertEquals(1, areas.size()); assertEquals(Math.PI * 25, areas.get(0), 0.001); } } // Mock visitor for testing class CountingVisitor implements ShapeVisitor { private int visitCount = 0; @Override public void visitCircle(Circle circle) { visitCount++; } @Override public void visitRectangle(Rectangle rectangle) { visitCount++; } @Override public void visitTriangle(Triangle triangle) { visitCount++; } public int getVisitCount() { return visitCount; } } Best Practices and Considerations
1. When to Use Visitor Pattern
// Good use cases: // 1. Performing operations on complex object structures public class Compiler { // AST visitors for type checking, code generation, optimization } // 2. Adding operations without modifying element classes public class DocumentProcessor { // Exporters, validators, transformers for document elements } // 3. Keeping related operations together public class AnalyticsEngine { // Statistical calculations on business objects } // 4. Operations that depend on concrete element types public class Renderer { // Different rendering for different UI components } 2. When NOT to Use Visitor Pattern
// Avoid when: // 1. The element hierarchy is unstable public class UnstableHierarchy { // Adding new element types requires modifying all visitors } // 2. Operations don't depend on concrete types public class GenericOperations { // If all elements can be treated uniformly } // 3. Performance is critical public class PerformanceCritical { // Double dispatch has some overhead } // 4. You need to frequently add new element types public class FrequentlyChangingElements { // Visitors become hard to maintain } 3. Combining with Other Patterns
// Visitor + Composite public class CompositeVisitorDemo { // As shown earlier - perfect combination } // Visitor + Interpreter public class ASTProcessing { // Visitors for evaluating, optimizing AST nodes } // Visitor + Builder public class ReportGenerator { // Builder creates structure, visitor processes it } // Visitor with Strategy public class ConfigurableVisitor<T> implements GenericShapeVisitor<T> { private Map<Class<?>, Function<Shape, T>> strategies = new HashMap<>(); public <S extends Shape> void addStrategy(Class<S> type, Function<S, T> strategy) { strategies.put(type, (Function<Shape, T>) strategy); } @Override public T visitCircle(Circle circle) { return strategies.get(Circle.class).apply(circle); } // Similar for other types... } Common Pitfalls and Solutions
1. Breaking Encapsulation
// Problem: Visitors might need to access private fields public class Circle { private double radius; // Solution: Provide appropriate getters public double getRadius() { return radius; } } // Or use package-private access public class Circle { double radius; // Package-private for visitors in same package public Circle(double radius) { this.radius = radius; } } 2. Adding New Element Types
// Problem: Adding new element breaks all existing visitors // Solution: Use default methods or abstract base classes public interface ShapeVisitor { void visitCircle(Circle circle); void visitRectangle(Rectangle rectangle); void visitTriangle(Triangle triangle); // Default method for unknown types default void visitUnknown(Shape shape) { System.out.println("Unknown shape type: " + shape.getClass().getSimpleName()); } } // Abstract base visitor with default implementations public abstract class AbstractShapeVisitor implements ShapeVisitor { @Override public void visitCircle(Circle circle) { // Default: do nothing or throw exception } @Override public void visitRectangle(Rectangle rectangle) { // Default implementation } @Override public void visitTriangle(Triangle triangle) { // Default implementation } } 3. Circular Dependencies
// Problem: Elements and visitors have circular dependencies // Solution: Use interfaces and separate packages // In package: com.company.elements public interface Shape { void accept(ShapeVisitor visitor); } // In package: com.company.visitors public interface ShapeVisitor { void visitCircle(Circle circle); void visitRectangle(Rectangle rectangle); } // Concrete elements in elements package // Concrete visitors in visitors package Conclusion
The Visitor Pattern is a powerful tool for separating operations from object structures:
Key Benefits:
- Operation Separation - Keeps algorithms separate from object structures
- Open/Closed Principle - Easy to add new operations
- Related Operation Grouping - Keeps related operations together
- Complex Operation Management - Handles operations that depend on concrete types
- External Algorithm Integration - Allows external algorithms to work with your objects
Trade-offs:
- Breaking Encapsulation - May require exposing internal state
- Hard to Add Elements - Adding new element types breaks visitors
- Complexity - Can overcomplicate simple scenarios
- Tight Coupling - Visitors know about all concrete element types
Use When:
- You have a complex object structure with multiple element types
- You need to perform many different operations on the structure
- The object structure is stable but operations change frequently
- Operations need to access different element types differently
The Visitor Pattern excels in domains like:
- Compiler construction (AST processing)
- Document processing systems
- UI framework rendering
- Business rule engines
- Data export/transformation systems
It's particularly effective when combined with the Composite Pattern for processing hierarchical structures.