Visitor Pattern

The Visitor Pattern is a behavioral design pattern that lets you add new operations to existing class hierarchies without modifying them. Instead of putting the new behavior inside the classes, you move it into a separate "visitor" object that travels through the structure.
Think of a tax auditor visiting different types of businesses—a restaurant, a factory, a shop. Each business type is taxed differently, but the auditor knows how to handle each. The businesses don't change; the auditor brings the logic. That's the Visitor Pattern.
The Problem
Imagine you have a hierarchy of shapes—Circle, Rectangle, Triangle. You need to add operations like area calculation, rendering, and XML export—but you don't want to bloat the shape classes:
// Without Visitor Pattern - bloated classes
class Circle {
double radius;
double calculateArea() { return Math.PI * radius * radius; }
// Now add rendering - bloats Circle
void render() { System.out.println("Rendering circle..."); }
// Now add XML export - even more bloat
String toXML() { return "<circle radius='" + radius + "'/>"; }
// Every new operation forces changes to Circle (and every other shape)!
}
Problems:
Open/Closed Violation: Every new operation forces changes to every class in the hierarchy
Class Bloat: Shape classes accumulate unrelated operations
Hard to maintain: Adding
toJSON(),serialize(),validate()means modifying every shapeScattered logic: Related operation logic (e.g., all XML exports) is spread across classes
The Solution: Visitor Pattern
The Visitor Pattern extracts operations into visitor objects. Each shape just accepts a visitor and delegates the operation to it.
The pattern separates:
What you traverse (Elements — shapes, nodes, files)
What you do (Visitors — operations like export, calculate, render)
How It Solves Each Problem
| Problem | How Visitor Pattern Solves It |
|---|---|
| Open/Closed Violation | Add new operations by adding new Visitor classes—no element changes needed. |
| Class Bloat | Operations live in Visitor classes, not in shape classes. |
| Hard to maintain | All logic for one operation is centralized in one Visitor. |
| Scattered logic | Related operation code (e.g., all XML exports) lives together. |
Key Components
Visitor: Interface with a
visitmethod for each element typeConcreteVisitor: Implements the operation for each element type
Element: Interface declaring
accept(visitor)methodConcreteElement: Calls the correct
visitmethod on the visitorObject Structure: Collection of elements the visitor traverses
How It Solves the Problem
Real-World Implementation
Example 1: Shape Operations (Area, XML Export, Rendering)
import java.util.List;
// Visitor Interface
interface ShapeVisitor {
void visitCircle(Circle circle);
void visitRectangle(Rectangle rectangle);
void visitTriangle(Triangle triangle);
}
// Element Interface
interface Shape {
void accept(ShapeVisitor visitor);
}
// Concrete Elements
class Circle implements Shape {
double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public void accept(ShapeVisitor visitor) {
visitor.visitCircle(this);
}
}
class Rectangle implements Shape {
double width;
double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public void accept(ShapeVisitor visitor) {
visitor.visitRectangle(this);
}
}
class Triangle implements Shape {
double base;
double height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
@Override
public void accept(ShapeVisitor visitor) {
visitor.visitTriangle(this);
}
}
// Concrete Visitor 1 - Area Calculator
class AreaCalculator implements ShapeVisitor {
private double totalArea = 0;
@Override
public void visitCircle(Circle circle) {
double area = Math.PI * circle.radius * circle.radius;
totalArea += area;
System.out.printf("Circle area: %.2f%n", area);
}
@Override
public void visitRectangle(Rectangle rectangle) {
double area = rectangle.width * rectangle.height;
totalArea += area;
System.out.printf("Rectangle area: %.2f%n", area);
}
@Override
public void visitTriangle(Triangle triangle) {
double area = 0.5 * triangle.base * triangle.height;
totalArea += area;
System.out.printf("Triangle area: %.2f%n", area);
}
public double getTotalArea() {
return totalArea;
}
}
// Concrete Visitor 2 - XML Exporter
class XMLExporter implements ShapeVisitor {
private StringBuilder xml = new StringBuilder();
@Override
public void visitCircle(Circle circle) {
xml.append(String.format("<circle radius='%.1f'/>%n", circle.radius));
}
@Override
public void visitRectangle(Rectangle rectangle) {
xml.append(String.format("<rectangle width='%.1f' height='%.1f'/>%n",
rectangle.width, rectangle.height));
}
@Override
public void visitTriangle(Triangle triangle) {
xml.append(String.format("<triangle base='%.1f' height='%.1f'/>%n",
triangle.base, triangle.height));
}
public String getXML() {
return "<shapes>\n" + xml + "</shapes>";
}
}
// Concrete Visitor 3 - Renderer
class ShapeRenderer implements ShapeVisitor {
@Override
public void visitCircle(Circle circle) {
System.out.println("Rendering circle with radius " + circle.radius);
}
@Override
public void visitRectangle(Rectangle rectangle) {
System.out.println("Rendering rectangle " + rectangle.width + "x" + rectangle.height);
}
@Override
public void visitTriangle(Triangle triangle) {
System.out.println("Rendering triangle base=" + triangle.base);
}
}
// Client
public class ShapeDemo {
public static void main(String[] args) {
List<Shape> shapes = List.of(
new Circle(5),
new Rectangle(4, 6),
new Triangle(3, 8)
);
System.out.println("=== Area Calculation ===");
AreaCalculator calculator = new AreaCalculator();
shapes.forEach(s -> s.accept(calculator));
System.out.printf("Total area: %.2f%n%n", calculator.getTotalArea());
System.out.println("=== XML Export ===");
XMLExporter exporter = new XMLExporter();
shapes.forEach(s -> s.accept(exporter));
System.out.println(exporter.getXML());
System.out.println("=== Rendering ===");
ShapeRenderer renderer = new ShapeRenderer();
shapes.forEach(s -> s.accept(renderer));
}
}
Example 2: AST (Abstract Syntax Tree) Evaluator
// Visitor for AST nodes
interface ExpressionVisitor {
int visitNumber(NumberNode node);
int visitAdd(AddNode node);
int visitMultiply(MultiplyNode node);
}
// Element Interface
interface ExpressionNode {
int accept(ExpressionVisitor visitor);
}
// Concrete Nodes
class NumberNode implements ExpressionNode {
int value;
public NumberNode(int value) {
this.value = value;
}
@Override
public int accept(ExpressionVisitor visitor) {
return visitor.visitNumber(this);
}
}
class AddNode implements ExpressionNode {
ExpressionNode left;
ExpressionNode right;
public AddNode(ExpressionNode left, ExpressionNode right) {
this.left = left;
this.right = right;
}
@Override
public int accept(ExpressionVisitor visitor) {
return visitor.visitAdd(this);
}
}
class MultiplyNode implements ExpressionNode {
ExpressionNode left;
ExpressionNode right;
public MultiplyNode(ExpressionNode left, ExpressionNode right) {
this.left = left;
this.right = right;
}
@Override
public int accept(ExpressionVisitor visitor) {
return visitor.visitMultiply(this);
}
}
// Concrete Visitor - Evaluator
class Evaluator implements ExpressionVisitor {
@Override
public int visitNumber(NumberNode node) {
return node.value;
}
@Override
public int visitAdd(AddNode node) {
return node.left.accept(this) + node.right.accept(this);
}
@Override
public int visitMultiply(MultiplyNode node) {
return node.left.accept(this) * node.right.accept(this);
}
}
// Concrete Visitor - Printer
class ExpressionPrinter implements ExpressionVisitor {
@Override
public int visitNumber(NumberNode node) {
System.out.print(node.value);
return node.value;
}
@Override
public int visitAdd(AddNode node) {
System.out.print("(");
node.left.accept(this);
System.out.print(" + ");
node.right.accept(this);
System.out.print(")");
return 0;
}
@Override
public int visitMultiply(MultiplyNode node) {
System.out.print("(");
node.left.accept(this);
System.out.print(" * ");
node.right.accept(this);
System.out.print(")");
return 0;
}
}
// Client
public class ASTDemo {
public static void main(String[] args) {
// Expression: (2 + 3) * 4
ExpressionNode tree = new MultiplyNode(
new AddNode(new NumberNode(2), new NumberNode(3)),
new NumberNode(4)
);
System.out.print("Expression: ");
tree.accept(new ExpressionPrinter());
System.out.println();
int result = tree.accept(new Evaluator());
System.out.println("Result: " + result);
}
}
/* Output:
Expression: ((2 + 3) * 4)
Result: 20
*/
Example 3: File System Visitor
import java.util.ArrayList;
import java.util.List;
// Visitor
interface FileSystemVisitor {
void visitFile(FileNode file);
void visitDirectory(DirectoryNode directory);
}
// Elements
interface FileSystemNode {
String getName();
void accept(FileSystemVisitor visitor);
}
class FileNode implements FileSystemNode {
private String name;
private long sizeKB;
public FileNode(String name, long sizeKB) {
this.name = name;
this.sizeKB = sizeKB;
}
@Override
public String getName() { return name; }
public long getSizeKB() { return sizeKB; }
@Override
public void accept(FileSystemVisitor visitor) {
visitor.visitFile(this);
}
}
class DirectoryNode implements FileSystemNode {
private String name;
private List<FileSystemNode> children = new ArrayList<>();
public DirectoryNode(String name) {
this.name = name;
}
public void add(FileSystemNode node) {
children.add(node);
}
public List<FileSystemNode> getChildren() { return children; }
@Override
public String getName() { return name; }
@Override
public void accept(FileSystemVisitor visitor) {
visitor.visitDirectory(this);
for (FileSystemNode child : children) {
child.accept(visitor);
}
}
}
// Visitor 1 - Size Calculator
class SizeCalculator implements FileSystemVisitor {
private long totalSize = 0;
@Override
public void visitFile(FileNode file) {
totalSize += file.getSizeKB();
}
@Override
public void visitDirectory(DirectoryNode directory) {
System.out.println("Scanning directory: " + directory.getName());
}
public long getTotalSizeKB() { return totalSize; }
}
// Visitor 2 - File Lister
class FileLister implements FileSystemVisitor {
private int depth = 0;
@Override
public void visitFile(FileNode file) {
System.out.println(" ".repeat(depth) + "- " + file.getName()
+ " (" + file.getSizeKB() + " KB)");
}
@Override
public void visitDirectory(DirectoryNode directory) {
System.out.println(" ".repeat(depth) + "[" + directory.getName() + "]");
depth++;
}
}
// Client
public class FileSystemDemo {
public static void main(String[] args) {
DirectoryNode root = new DirectoryNode("root");
DirectoryNode src = new DirectoryNode("src");
DirectoryNode test = new DirectoryNode("test");
src.add(new FileNode("Main.java", 12));
src.add(new FileNode("Service.java", 34));
test.add(new FileNode("MainTest.java", 8));
root.add(src);
root.add(test);
root.add(new FileNode("README.md", 5));
System.out.println("=== File Listing ===");
root.accept(new FileLister());
System.out.println("\n=== Total Size ===");
SizeCalculator calculator = new SizeCalculator();
root.accept(calculator);
System.out.println("Total: " + calculator.getTotalSizeKB() + " KB");
}
}
Workflow Diagram
Real-World Use Cases
Compilers: AST traversal for code generation, optimization, type checking
Document Processing: Export Word/PDF/HTML from the same document model
File Systems: Size calculation, search, permissions audit
Game Dev: Entity processing with different operations (render, update, physics)
Tax Engines: Different tax rules applied to different product/transaction types
Static Analysis Tools: Rules applied across a codebase's AST
XML/JSON Processors: Transforming or validating node trees
When to Use the Visitor Pattern
✅ Use When
Stable Hierarchy: Element classes rarely change, but operations change often
Multiple Operations: You need to perform many unrelated operations on elements
Avoid Pollution: You don't want to add operation logic into element classes
Centralized Logic: You want all related operation code in one place
❌ Avoid When
Frequent Hierarchy Changes: Adding a new element type forces updating every visitor
Simple Operations: A single method on each class is simpler
Private State: Visitors need access to element internals, which can be awkward
Benefits
Open/Closed Principle: Add new operations without touching element classes
Single Responsibility: Each visitor handles one concern
Related Logic Centralized: All XML export code lives in
XMLExporterDouble Dispatch: Runtime resolution of both element and visitor type
Drawbacks
Element Hierarchy is Closed: Adding a new element requires updating all visitors
Breaks Encapsulation: Visitors need access to element internals
Complexity: Boilerplate
accept()methods on every element
Visitor vs Strategy Pattern
| Aspect | Visitor | Strategy |
|---|---|---|
| Scope | Object structure (many types) | Single object |
| Dispatch | Double dispatch | Single dispatch |
| Purpose | Add operations externally | Swap algorithms |
| Element Changes | Adds accept() once |
No changes needed |
Best Practices
Keep Elements Stable: Visitor shines when the element hierarchy is frozen
One Concern Per Visitor: Don't mix unrelated operations in one visitor
Minimize Element Exposure: Keep visitor access to the minimum needed
Accumulate State: Use visitor fields to collect results (e.g., total area)
Combine with Composite: Visitor works beautifully with Composite pattern for tree traversal
Conclusion
The Visitor Pattern is the go-to solution when you need to add operations to a stable class hierarchy without touching those classes. It's the engine behind compilers, document exporters, and static analysis tools.
Remember: Let the visitor do the work so the elements stay clean! 🧳
🎯 Key Takeaway
The Visitor Pattern is about adding behavior without modifying. When your class hierarchy is stable but your operations keep growing—visit it!
A developer was asked in a tech interview: "Explain the Visitor Pattern." She said, "It lets you add operations to classes without modifying them." The interviewer nodded. "Can you give a real-world example?" She replied, "Sure—it's how my manager keeps adding features to production without touching the design doc." 😄 Circle sighed, "Welcome to the pattern." 😄
Happy Visiting! 🧳✨






