23. Visitor
The 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. The key mechanism is double dispatch: the element calls back to the visitor with its type.
Intent and Motivation
Intent: Represent an operation to be performed on elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.
Motivation: A compiler’s AST has NumberNode, AddNode, VariableNode classes. You need operations: evaluate(), print(), typeCheck(), optimize(). Adding each operation as a method on every node class leads to bloated node classes and violates Single Responsibility. Visitor extracts each operation into a separate visitor class (EvaluateVisitor, PrintVisitor). Each node implements accept(Visitor v) which calls v.visitThisNode(this) — double dispatch ensures the correct visitor method is invoked. Adding a new operation means adding a new visitor class — no node classes change.
The trade-off: adding new node types requires updating all visitors.
Structure (UML-like)
┌──────────────┐ accept() ┌──────────────┐
│ Element │ ───────────► │ Visitor │ (interface)
├──────────────┤ ├──────────────┤
│ + accept(v) │ │ + visitElemA │
└──────▲───────┘ │ + visitElemB │
│ └──────▲───────┘
│ implements │ implements
┌──────┴───────┐ ┌───────┴───────┐
│ ConcreteElem │ │ ConcreteVisitor│
│ A, B, C │ │ (Export, Eval) │
└──────────────┘ └───────────────┘
Double dispatch flow:
element.accept(visitor)
→ visitor.visitConcreteElement(element)
Participants:
- Visitor — declares a
visitmethod for each concrete element type. - ConcreteVisitor — implements operations for each element type.
- Element — declares
accept(Visitor)method. - ConcreteElement — implements
accept()by calling the matchingvisitmethod on the visitor. - ObjectStructure — collection of elements that visitors traverse.
Java Example
// Visitor
interface ShapeVisitor {
void visitCircle(Circle c);
void visitRectangle(Rectangle r);
}
// Elements
interface Shape {
void accept(ShapeVisitor visitor);
}
class Circle implements Shape {
double radius;
Circle(double radius) { this.radius = radius; }
public void accept(ShapeVisitor v) { v.visitCircle(this); }
}
class Rectangle implements Shape {
double width, height;
Rectangle(double w, double h) { this.width = w; this.height = h; }
public void accept(ShapeVisitor v) { v.visitRectangle(this); }
}
// Concrete Visitor — area calculation
class AreaVisitor implements ShapeVisitor {
private double totalArea = 0;
public void visitCircle(Circle c) {
double area = Math.PI * c.radius * c.radius;
totalArea += area;
System.out.println("Circle area: " + area);
}
public void visitRectangle(Rectangle r) {
double area = r.width * r.height;
totalArea += area;
System.out.println("Rectangle area: " + area);
}
double getTotalArea() { return totalArea; }
}
// Usage
List<Shape> shapes = List.of(new Circle(5), new Rectangle(4, 6));
AreaVisitor visitor = new AreaVisitor();
for (Shape shape : shapes) {
shape.accept(visitor);
}
System.out.println("Total: " + visitor.getTotalArea());
JavaScript Example
// Elements
const nodes = {
number: (value) => ({ type: 'number', value, accept(v) { return v.visitNumber(this); } }),
add: (left, right) => ({ type: 'add', left, right, accept(v) { return v.visitAdd(this); } }),
variable: (name) => ({ type: 'variable', name, accept(v) { return v.visitVariable(this); } })
};
// Visitors
const evalVisitor = {
visitNumber(node) { return node.value; },
visitVariable(node, ctx) { return ctx[node.name] || 0; },
visitAdd(node, ctx) {
return node.left.accept(this, ctx) + node.right.accept(this, ctx);
}
};
const printVisitor = {
visitNumber(node) { return String(node.value); },
visitVariable(node) { return node.name; },
visitAdd(node) {
return `(${node.left.accept(this)} + ${node.right.accept(this)})`;
}
};
const expr = nodes.add(nodes.variable('x'), nodes.number(3));
const ctx = { x: 7 };
console.log(expr.accept(evalVisitor, ctx)); // 10
console.log(expr.accept(printVisitor)); // (x + 3)
Real-World Use Cases
| Framework / System | Usage |
|---|---|
Java Files.walkFileTree() |
FileVisitor operates on files/dirs without modifying the file system API. |
| Compiler AST passes | LLVM, javac — optimization, type checking, and code gen as separate visitor passes. |
| DOM traversal | TreeWalker and NodeIterator visit DOM nodes for rendering, validation, or transformation. |
| JUnit test discovery | Test engine visits class structures to find and run test methods. |
| Document export | Export visitors convert internal document model to PDF, HTML, or Markdown. |
| Linting tools (ESLint) | AST visitors analyze code patterns without modifying the parser’s node classes. |
Pros and Cons
| Pros | Cons |
|---|---|
| Add new operations easily — just create a new visitor class | Adding new element types requires updating ALL visitors |
| Groups related operations in one visitor class | Visitors need access to element internals (breaks encapsulation) |
| Can accumulate state across elements during traversal | Circular element dependencies complicate visitor design |
| Separates algorithms from object structure | Not all languages support clean double dispatch |
| Open/Closed for operations (not for element types) | Visitor interface grows with each new element type |
When to Use vs When NOT to Use
Use when:
- An object structure contains many classes with different interfaces and you need operations across all of them.
- You need to perform many distinct operations on the structure and do not want to pollute element classes.
- The element class hierarchy is stable but operations change frequently.
- You are building compiler passes, export pipelines, or analysis tools over a fixed AST.
Do NOT use when:
- The element hierarchy changes frequently (every new element breaks all visitors).
- Element classes are simple and few — just add methods directly.
- Operations need access to element private state that should remain encapsulated.
- The structure is not a stable hierarchy (flat collections — use Iterator + functions).
Common Mistakes
- Adding operations via Visitor when element types change often — the pattern optimizes for stable structures.
- Visitor accessing private fields — breaks encapsulation; elements should provide accessors or pass data to visit methods.
- Forgetting to update all visitors when adding element types — compile-time checks help (interface requires all visit methods).
- Not using double dispatch — calling
visitor.visit(element)directly loses type-specific dispatch. - God visitor — one visitor doing unrelated operations; create separate visitors per concern.
Related Patterns
- Composite — visitors commonly traverse composite tree structures.
- Iterator — iterator walks the structure; visitor performs operations at each stop.
- Interpreter — interpreter evaluates expression trees; visitor adds operations to the same trees.
- Strategy — visitor is a strategy for operating on elements, but uses double dispatch instead of single delegation.
- Command — can encapsulate visitor operations as commands for undo or queuing.