Visitor Pattern explained simply
You have a set of classes that are stable. They work. They are tested. Now someone says “we need to add a new operation to all of them.” Your instinct might be to open each class and add a new method. But that violates the Open/Closed Principle – classes should be open for extension but closed for modification.
The Visitor Pattern solves exactly this. It lets you define new operations on a set of objects without changing the objects themselves. You pull the operation out into a separate “visitor” class, and each object simply accepts the visitor and lets it do its thing.
What is the Visitor Pattern?
At its core:
- Separate an algorithm from the objects it operates on.
- Define new operations without modifying the classes of the elements.
- Use double dispatch to call the right method based on both the visitor type and the element type.
The pattern has two sides. The elements (the things being visited) each have an accept() method. The visitor has a visit() method overloaded for each element type. When an element accepts a visitor, it calls the visitor’s visit() method and passes itself. That is the whole trick.
New operations, zero modifications to existing classes.
Real-Life Analogy
Picture a tax inspector visiting different businesses in a neighborhood. There is a restaurant, a retail shop, and an office.
The tax inspector walks into each business and performs an audit. But the audit looks different for each one. At the restaurant, she checks food safety licenses and tip reporting. At the retail shop, she reviews sales tax records and inventory. At the office, she examines payroll and contractor payments.
The businesses themselves don’t change. They don’t need to learn how to audit themselves. They just open the door (accept the visitor) and let the inspector do her work. If next month a fire safety inspector shows up instead, the businesses do the same thing – open the door and let the new visitor do their inspection.
That is the Visitor Pattern. The businesses are the elements. The inspector is the visitor. You can send any kind of inspector (visitor) to any business (element) without the businesses changing their structure.
Structure
Here is a class diagram showing the pieces. The ASTNode elements each accept a visitor, and the visitor has overloaded visit() methods for each node type.
<mxfile>
<diagram name="Visitor Pattern">
<mxGraphModel dx="1200" dy="880" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1200" pageHeight="880">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<!-- ASTVisitor interface -->
<mxCell id="2" value="<<interface>>
ASTVisitor
─────────────────
+ visit(node: NumberNode): void
+ visit(node: AddNode): void
+ visit(node: MultiplyNode): void" style="shape=rectangle;whiteSpace=wrap;html=1;align=center;fontSize=12;fontFamily=monospace;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="30" y="30" width="320" height="100" as="geometry"/>
</mxCell>
<!-- ASTNode interface -->
<mxCell id="3" value="<<interface>>
ASTNode
─────────────────
+ accept(visitor: ASTVisitor): void" style="shape=rectangle;whiteSpace=wrap;html=1;align=center;fontSize=12;fontFamily=monospace;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="680" y="30" width="300" height="80" as="geometry"/>
</mxCell>
<!-- Evaluator -->
<mxCell id="4" value="Evaluator
─────────────────
- result: double
+ visit(NumberNode): void
+ visit(AddNode): void
+ visit(MultiplyNode): void
+ getResult(): double" style="shape=rectangle;whiteSpace=wrap;html=1;align=center;fontSize=12;fontFamily=monospace;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="30" y="230" width="280" height="120" as="geometry"/>
</mxCell>
<!-- PrettyPrinter -->
<mxCell id="5" value="PrettyPrinter
─────────────────
- output: StringBuilder
+ visit(NumberNode): void
+ visit(AddNode): void
+ visit(MultiplyNode): void
+ getOutput(): String" style="shape=rectangle;whiteSpace=wrap;html=1;align=center;fontSize=12;fontFamily=monospace;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="330" y="230" width="280" height="120" as="geometry"/>
</mxCell>
<!-- NumberNode -->
<mxCell id="6" value="NumberNode
─────────────────
- value: double
+ accept(visitor): void
+ getValue(): double" style="shape=rectangle;whiteSpace=wrap;html=1;align=center;fontSize=12;fontFamily=monospace;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
<mxGeometry x="630" y="220" width="220" height="90" as="geometry"/>
</mxCell>
<!-- AddNode -->
<mxCell id="7" value="AddNode
─────────────────
- left: ASTNode
- right: ASTNode
+ accept(visitor): void" style="shape=rectangle;whiteSpace=wrap;html=1;align=center;fontSize=12;fontFamily=monospace;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
<mxGeometry x="630" y="330" width="220" height="90" as="geometry"/>
</mxCell>
<!-- MultiplyNode -->
<mxCell id="8" value="MultiplyNode
─────────────────
- left: ASTNode
- right: ASTNode
+ accept(visitor): void" style="shape=rectangle;whiteSpace=wrap;html=1;align=center;fontSize=12;fontFamily=monospace;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
<mxGeometry x="630" y="440" width="220" height="90" as="geometry"/>
</mxCell>
<!-- Evaluator implements ASTVisitor -->
<mxCell id="9" style="endArrow=block;endFill=0;dashed=1;strokeColor=#82b366;" edge="1" source="4" target="2" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<!-- PrettyPrinter implements ASTVisitor -->
<mxCell id="10" style="endArrow=block;endFill=0;dashed=1;strokeColor=#82b366;" edge="1" source="5" target="2" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<!-- NumberNode implements ASTNode -->
<mxCell id="11" style="endArrow=block;endFill=0;dashed=1;strokeColor=#b85450;" edge="1" source="6" target="3" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<!-- AddNode implements ASTNode -->
<mxCell id="12" style="endArrow=block;endFill=0;dashed=1;strokeColor=#b85450;" edge="1" source="7" target="3" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<!-- MultiplyNode implements ASTNode -->
<mxCell id="13" style="endArrow=block;endFill=0;dashed=1;strokeColor=#b85450;" edge="1" source="8" target="3" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<!-- ASTNode uses ASTVisitor -->
<mxCell id="14" value="uses" style="endArrow=open;endSize=12;dashed=1;strokeColor=#6c8ebf;" edge="1" source="3" target="2" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Java Example: AST Expression Evaluator
This is the canonical real-world use of the Visitor Pattern. Compilers, linters, and code analysis tools all use it to process Abstract Syntax Trees. Let’s build a simple expression evaluator.
We will model the expression (3 + 5) * 2 as a tree, then run two different visitors on it – one that evaluates the result, and one that pretty-prints the expression.
First, the ASTNode interface. Every node in our tree must accept a visitor:
public interface ASTNode {
void accept(ASTVisitor visitor);
}
Now the ASTVisitor interface. It has an overloaded visit() method for each concrete node type:
public interface ASTVisitor {
void visit(NumberNode node);
void visit(AddNode node);
void visit(MultiplyNode node);
}
Next, the concrete node types. NumberNode holds a numeric value:
public class NumberNode implements ASTNode {
private final double value;
public NumberNode(double value) {
this.value = value;
}
public double getValue() {
return value;
}
@Override
public void accept(ASTVisitor visitor) {
visitor.visit(this);
}
}
AddNode holds a left and right child:
public class AddNode implements ASTNode {
private final ASTNode left;
private final ASTNode right;
public AddNode(ASTNode left, ASTNode right) {
this.left = left;
this.right = right;
}
public ASTNode getLeft() { return left; }
public ASTNode getRight() { return right; }
@Override
public void accept(ASTVisitor visitor) {
visitor.visit(this);
}
}
MultiplyNode follows the same shape:
public class MultiplyNode implements ASTNode {
private final ASTNode left;
private final ASTNode right;
public MultiplyNode(ASTNode left, ASTNode right) {
this.left = left;
this.right = right;
}
public ASTNode getLeft() { return left; }
public ASTNode getRight() { return right; }
@Override
public void accept(ASTVisitor visitor) {
visitor.visit(this);
}
}
Notice something important. Every node’s accept() method does the same thing: it calls visitor.visit(this). That’s it. The node doesn’t know what the visitor will do. It just hands itself over. This is the key to the pattern – the node classes never need to change when you add new operations.
Now let’s build the visitors. The Evaluator walks the tree and computes the result:
import java.util.ArrayDeque;
import java.util.Deque;
public class Evaluator implements ASTVisitor {
private final Deque<Double> stack = new ArrayDeque<>();
@Override
public void visit(NumberNode node) {
stack.push(node.getValue());
}
@Override
public void visit(AddNode node) {
node.getLeft().accept(this);
node.getRight().accept(this);
double right = stack.pop();
double left = stack.pop();
stack.push(left + right);
}
@Override
public void visit(MultiplyNode node) {
node.getLeft().accept(this);
node.getRight().accept(this);
double right = stack.pop();
double left = stack.pop();
stack.push(left * right);
}
public double getResult() {
return stack.peek();
}
}
The evaluator uses a stack to accumulate results. When it visits a NumberNode, it pushes the value. When it visits an AddNode or MultiplyNode, it evaluates both children first (by accepting itself recursively), pops their results, computes the operation, and pushes the result back.
The PrettyPrinter walks the same tree but produces a human-readable string:
public class PrettyPrinter implements ASTVisitor {
private final StringBuilder output = new StringBuilder();
@Override
public void visit(NumberNode node) {
output.append(node.getValue());
}
@Override
public void visit(AddNode node) {
output.append("(");
node.getLeft().accept(this);
output.append(" + ");
node.getRight().accept(this);
output.append(")");
}
@Override
public void visit(MultiplyNode node) {
output.append("(");
node.getLeft().accept(this);
output.append(" * ");
node.getRight().accept(this);
output.append(")");
}
public String getOutput() {
return output.toString();
}
}
Two completely different operations. Same tree. No modifications to any node class.
Putting It All Together
Let’s build the expression (3 + 5) * 2 and run both visitors:
public class MainProgram {
public static void main(String[] args) {
// Build the AST for (3 + 5) * 2
ASTNode expression = new MultiplyNode(
new AddNode(
new NumberNode(3),
new NumberNode(5)
),
new NumberNode(2)
);
// Run the Evaluator visitor
Evaluator evaluator = new Evaluator();
expression.accept(evaluator);
System.out.println("Result: " + evaluator.getResult());
// Run the PrettyPrinter visitor
PrettyPrinter printer = new PrettyPrinter();
expression.accept(printer);
System.out.println("Expression: " + printer.getOutput());
}
}
Output:
Result: 16.0
Expression: ((3.0 + 5.0) * 2.0)
The tree was built once. Two visitors walked it and produced completely different results. If tomorrow you need a third operation – say, a visitor that counts the number of nodes, or one that checks for division by zero – you write a new visitor class. You never touch NumberNode, AddNode, or MultiplyNode.
Key Components
- Visitor (
ASTVisitor): Declares avisit()method for each concrete element type. This is the interface that all operations implement. - ConcreteVisitor (
Evaluator,PrettyPrinter): Implements the visitor interface. Each one defines a different operation to perform on the elements. - Element (
ASTNode): Declares anaccept()method that takes a visitor. - ConcreteElement (
NumberNode,AddNode,MultiplyNode): Implementsaccept()by calling the appropriatevisit()method on the visitor, passingthis.
When to Use the Visitor Pattern
- When you have a stable set of element classes but need to frequently add new operations on them.
- When many unrelated operations need to be performed on an object structure, and you don’t want to pollute the element classes with all that logic.
- When the object structure rarely changes but the things you do with it change often. If the elements change frequently, Visitor is a poor fit because every new element type forces changes to every visitor.
- When you want to accumulate state across a traversal without passing context objects everywhere.
Advantages and Disadvantages
Advantages:
- Open/Closed Principle: Add new operations without modifying existing element classes. Write a new visitor and you are done.
- Single Responsibility Principle: Related behavior is grouped together in one visitor class, rather than spread across multiple element classes.
- Accumulating state: A visitor can maintain state as it traverses the structure. The
Evaluatoruses a stack. ThePrettyPrinterbuilds a string. Each visitor manages its own state cleanly. - Clean separation: The algorithm lives in the visitor, not in the elements. Elements stay focused on their data.
Disadvantages:
- Adding new element types is painful: Every time you add a new concrete element, you must update every existing visitor to handle it. If your element hierarchy changes often, this gets expensive fast.
- Breaking encapsulation: Visitors often need access to the internal state of elements. You may end up exposing fields or adding getters that you would otherwise keep private.
- Complexity overhead: For simple cases with one or two operations, the Visitor Pattern introduces more interfaces and classes than you need. Use it when the number of operations justifies the setup cost.
The Double Dispatch Trick
Java (like most OO languages) uses single dispatch – the method that gets called depends only on the runtime type of the object you call it on. If you call node.accept(visitor), Java dispatches based on the concrete type of node. But it doesn’t also dispatch based on the concrete type of visitor in a single call.
The Visitor Pattern achieves double dispatch through two method calls:
expression.accept(visitor)– Java dispatches based on the runtime type ofexpression. If it is aMultiplyNode,MultiplyNode.accept()runs.- Inside
accept(), the callvisitor.visit(this)dispatches based on the runtime type ofvisitor. And becausethishas a known compile-time type inside the concrete class (it isMultiplyNode, not justASTNode), Java resolves the correct overloadedvisit()method.
The result: the correct method is selected based on both the element type and the visitor type. Two dispatches, hence “double dispatch.” This is the mechanical heart of the Visitor Pattern, and it is the reason the accept() method exists at all – without it, you would need ugly instanceof chains.
Real-World Use Cases
The Visitor Pattern shows up wherever you have a tree or graph of objects and multiple operations to run over them:
- Java Compiler AST Processing: The
javaccompiler uses visitors extensively. Thejavax.lang.model.element.ElementVisitorandcom.sun.source.tree.TreeVisitorinterfaces let annotation processors and analysis tools walk the AST without modifying compiler internals. - DOM Traversal: XML and HTML processors walk a document tree applying different operations – validation, transformation, extraction – each as a separate visitor.
- Code Analysis Tools: Static analyzers like Checkstyle, PMD, and SpotBugs define rules as visitors. Each rule is a visitor that walks the AST looking for specific patterns. Adding a new rule means writing a new visitor, not changing the parser.
- Report Generators: When you need to export the same data structure in multiple formats (PDF, CSV, HTML), each format can be a visitor that traverses the data and produces output.
- File System Operations: Calculating disk usage, searching for files, generating directory listings – each operation can be a visitor walking the file tree. Java’s
Files.walkFileTree()withFileVisitoris literally this pattern in the standard library.
Wrapping Up
The Visitor Pattern lets you add new operations to a set of classes without modifying them. You separate the “what to do” (the visitor) from the “what to do it on” (the elements). The elements just accept visitors and let them work.
It is the go-to pattern when your object structure is stable but your operations keep growing. Compilers, static analysis tools, document processors, and report generators all lean on it heavily. The double dispatch trick through accept() and visit() is elegant once you see it – it lets Java do something it normally cannot, which is selecting a method based on two runtime types.
The tradeoff is clear: adding new operations is easy, but adding new element types is painful. If your elements change more often than your operations, consider a different pattern. But when the element hierarchy is locked down and new operations keep coming, the Visitor Pattern is exactly the right tool.