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.