Visitor: Double Dispatch
The Visitor pattern solves a specific form of design tension:
You have a relatively stable set of object types, but you keep adding new operations that need different behavior for each type.
Consider a shape hierarchy:
Shape
|
+-- Circle
+-- Rectangle
+-- Triangle
Today we need to calculate area.
Tomorrow we need:
Area
Perimeter
Render
Export
Validate
Generate report
A straightforward design puts those operations directly into the shapes:
Circle
+-- area()
+-- perimeter()
+-- render()
+-- export()
+-- validate()
Rectangle
+-- area()
+-- perimeter()
+-- render()
+-- export()
+-- validate()
Triangle
+-- area()
+-- perimeter()
+-- render()
+-- export()
+-- validate()
The problem is not duplication alone.
The deeper problem is that the shape classes now depend on every operation performed on them.
The Visitor pattern reverses that dependency.
Shapes
|
| accept(visitor)
v
Visitor
|
+----------+----------+
| | |
Area Render Export
Visitor Visitor Visitor
The shapes represent what they are.
The visitors represent what we want to do with them.
1. The Core Idea
Visitor separates:
Object structure
from:
Operations performed on that structure
For example:
Shape Hierarchy
|
+------------+------------+
| | |
Circle Rectangle Triangle
| | |
+------------+------------+
|
accept()
|
v
Visitor
|
+------------+------------+
| | |
AreaVisitor RenderVisitor ExportVisitor
The key engineering insight is:
Keep the data/structure stable and move changing operations into separate objects.
This is especially powerful when the number of operations grows faster than the number of element types.
2. A Concrete Example: Calculating Area
Let’s make the idea concrete.
We have:
Shape
|
+-- Circle
+-- Rectangle
Their data is different.
A Circle has:
radius
A Rectangle has:
width
height
Area calculation therefore differs:
Circle:
π × radius²
Rectangle:
width × height
A normal object-oriented approach could be:
interface Shape {
double area();
}
class Circle implements Shape {
double radius;
public double area() {
return Math.PI * radius * radius;
}
}
class Rectangle implements Shape {
double width;
double height;
public double area() {
return width * height;
}
}
This is actually good design if area is a fundamental responsibility of the shape.
So why would we introduce Visitor?
Because imagine the requirements grow:
area()
perimeter()
render()
exportToJson()
exportToXml()
generateSql()
validate()
generateReport()
Now every Shape needs to know about every one of these operations.
Visitor says:
Shape
|
+-- structural/domain information
|
+-- accept(visitor)
while:
AreaVisitor
RenderVisitor
ExportVisitor
ValidationVisitor
contain the operations.
That distinction is the real reason to use Visitor.
3. UML Class Diagram
The classic structure looks like this:
<<interface>>
Visitor
+---------------------+
| + visit(Circle) |
| + visit(Rectangle) |
+----------+----------+
^
|
implements
|
+----------+----------+
| |
+------+-------+ +-------+------+
| AreaVisitor | | RenderVisitor|
+--------------+ +--------------+
| visit(...) | | visit(...) |
+--------------+ +--------------+
<<interface>>
Element
+---------------------+
| + accept(Visitor) |
+----------+----------+
^
|
implements
|
+-------------+-------------+
| |
+------+-------+ +------+-------+
| Circle | | Rectangle |
+--------------+ +--------------+
| radius | | width |
| | | height |
| accept() | | accept() |
+--------------+ +--------------+
There are two important relationships:
Element
|
| accept(visitor)
v
Visitor
|
| visit(concreteElement)
v
Operation
4. The accept() Method Is the Critical Piece
The element interface is:
interface Shape {
void accept(ShapeVisitor visitor);
}
Circle:
class Circle implements Shape {
private final double radius;
Circle(double radius) {
this.radius = radius;
}
double getRadius() {
return radius;
}
@Override
public void accept(ShapeVisitor visitor) {
visitor.visit(this);
}
}
Rectangle:
class Rectangle implements Shape {
private final double width;
private final double height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
double getWidth() {
return width;
}
double getHeight() {
return height;
}
@Override
public void accept(ShapeVisitor visitor) {
visitor.visit(this);
}
}
The important line is:
visitor.visit(this);
Circle effectively says:
"I am a Circle.
Visitor, perform your Circle-specific operation."
Rectangle says:
"I am a Rectangle.
Visitor, perform your Rectangle-specific operation."
5. The Visitor Interface
Now define the operations:
interface ShapeVisitor {
void visit(Circle circle);
void visit(Rectangle rectangle);
}
Notice something important.
The Visitor explicitly knows the concrete element types:
visit(Circle)
visit(Rectangle)
That is intentional.
The Visitor needs to be able to say:
"If this is a Circle, do this."
"If this is a Rectangle, do that."
6. Concrete Area Visitor
Now area becomes an independent operation:
class AreaVisitor implements ShapeVisitor {
@Override
public void visit(Circle circle) {
double area =
Math.PI *
circle.getRadius() *
circle.getRadius();
System.out.println(area);
}
@Override
public void visit(Rectangle rectangle) {
double area =
rectangle.getWidth() *
rectangle.getHeight();
System.out.println(area);
}
}
Now the responsibilities are clear:
Circle
owns radius
Rectangle
owns width + height
AreaVisitor
knows how to calculate area
The shape doesn’t need to contain the area algorithm.
7. Runtime Flow
The client does:
Shape shape = new Circle(5);
ShapeVisitor visitor = new AreaVisitor();
shape.accept(visitor);
The call flows like this:
Client
|
| shape.accept(visitor)
v
Circle
|
| visitor.visit(this)
v
AreaVisitor
|
| visit(Circle)
v
Calculate Circle Area
For a Rectangle:
Client
|
| rectangle.accept(visitor)
v
Rectangle
|
| visitor.visit(this)
v
AreaVisitor
|
| visit(Rectangle)
v
Calculate Rectangle Area
8. Sequence Diagram
Client Circle AreaVisitor
| | |
| accept(visitor)| |
|--------------->| |
| | |
| | visit(this) |
| |---------------->|
| | |
| | |
| | calculate area |
| |<----------------|
| | |
|<---------------| |
The complete call chain is:
shape.accept(visitor)
|
v
Circle.accept(visitor)
|
v
visitor.visit(Circle)
|
v
AreaVisitor.visit(Circle)
9. Why Is This Called Double Dispatch?
This is one of the most important technical details of Visitor.
The operation depends on two types:
1. The concrete element
2. The concrete visitor
Conceptually:
+----------------+
| Visitor |
| |
| visit(Circle) |
| visit(Rectangle)|
+-------+--------+
^
|
|
accept(visitor)
|
+-----------+-----------+
| |
Circle Rectangle
For:
circle.accept(areaVisitor);
the Circle implementation performs:
visitor.visit(this);
where this is a Circle.
Therefore:
visit(Circle)
is selected.
If the element is a Rectangle:
visit(Rectangle)
is selected.
The two-stage dispatch gives Visitor its type-specific behavior.
10. Why Not Use instanceof?
Without Visitor, you might write:
void calculateArea(Shape shape) {
if (shape instanceof Circle) {
Circle circle = (Circle) shape;
// calculate circle area
}
else if (shape instanceof Rectangle) {
Rectangle rectangle = (Rectangle) shape;
// calculate rectangle area
}
}
Then another operation creates another chain:
void render(Shape shape) {
if (shape instanceof Circle) {
...
}
else if (shape instanceof Rectangle) {
...
}
}
And another:
void export(Shape shape) {
if (shape instanceof Circle) {
...
}
else if (shape instanceof Rectangle) {
...
}
}
Now type knowledge is scattered across the application.
Visitor centralizes the type-specific behavior:
AreaVisitor
visit(Circle)
visit(Rectangle)
RenderVisitor
visit(Circle)
visit(Rectangle)
ExportVisitor
visit(Circle)
visit(Rectangle)
11. The Real Power: Adding Operations
Suppose we already have:
Circle
Rectangle
Triangle
and:
AreaVisitor
RenderVisitor
Now the business asks for:
ExportToJson
With Visitor:
JsonExportVisitor
can be added.
Shapes
|
+-------------+-------------+
| | |
Circle Rectangle Triangle
| | |
+-------------+-------------+
|
accept()
|
v
Visitors
|
+--------------+--------------+
| | |
Area Render JSON
Visitor Visitor Visitor
The shape classes don’t need to learn JSON.
This is where Visitor earns its complexity.
12. But Adding a New Element Is Expensive
Now suppose we add:
Ellipse
The Visitor interface needs:
void visit(Ellipse ellipse);
And every concrete Visitor potentially needs:
visit(Ellipse)
So:
Add Ellipse
|
+------------+------------+
| | |
Visitor AreaVisitor RenderVisitor
| | |
visit(Ellipse) visit(...) visit(...)
This is the fundamental trade-off.
Visitor makes:
NEW OPERATION
cheap.
Visitor makes:
NEW ELEMENT TYPE
expensive.
13. The Change Matrix
This is the easiest way to reason about Visitor.
+----------------------+-----------------------------+
| Change | Visitor |
+----------------------+-----------------------------+
| Add new operation | Easy |
| Add new element | Expensive |
+----------------------+-----------------------------+
Therefore:
Stable element hierarchy
+
Frequently changing operations
|
v
Visitor
But:
Frequently changing element hierarchy
+
Stable operations
|
v
Visitor is usually poor
14. The Deeper Architectural Idea
Visitor isn’t fundamentally about shapes.
Shapes are just an easy example.
The real problem looks like:
STABLE STRUCTURE
|
+------------+------------+
| | |
Type A Type B Type C
| | |
+------------+------------+
|
Many operations
|
+--------------+--------------+
| | |
Operation A Operation B Operation C
Visitor separates those two dimensions.
DIMENSION 1
Object types
DIMENSION 2
Operations
Visitor makes the operation dimension extensible.
15. Classic Example: Compiler AST
This is a much stronger real-world example than shapes.
Consider:
ASTNode
|
+------------+------------+
| | |
Literal Binary Variable
A compiler may perform:
Type checking
Optimization
Code generation
Pretty printing
Static analysis
Dependency analysis
Without Visitor:
Literal
+-- typeCheck()
+-- optimize()
+-- generateCode()
+-- print()
+-- analyze()
Binary
+-- typeCheck()
+-- optimize()
+-- generateCode()
+-- print()
+-- analyze()
Variable
+-- typeCheck()
+-- optimize()
+-- generateCode()
+-- print()
+-- analyze()
With Visitor:
AST
|
+-- Literal
+-- Binary
+-- Variable
|
+-- accept(visitor)
|
+-- TypeCheckVisitor
+-- OptimizationVisitor
+-- CodeGenVisitor
+-- PrintVisitor
+-- AnalysisVisitor
This is where the pattern becomes genuinely useful.
The AST structure is relatively stable.
The compiler passes can grow.
Visitor fits that change pattern extremely well.
16. Visitor + Composite
Visitor is frequently paired with the Composite pattern.
Imagine:
Document
|
+---------+---------+
| | |
Paragraph Image Table
|
+----+----+
| |
Row Row
Composite gives you the tree structure.
Visitor gives you operations over that structure.
Composite Tree
|
v
accept()
|
v
Visitor
|
+---------+---------+
| | |
Export Validate Analyze
This combination is particularly powerful for:
ASTs
Documents
UI trees
File systems
Expression trees
17. When Visitor Is the Right Choice
Ask these questions.
Question 1
Do I have a meaningful hierarchy of element types?
A
B
C
D
Question 2
Is that hierarchy relatively stable?
YES
Question 3
Are new operations added frequently?
YES
Question 4
Do those operations need different behavior for each element?
YES
Question 5
Would putting those operations into the elements create bloated classes?
YES
If most answers are yes:
Visitor is a strong candidate.
18. When Not to Use It
Don’t use Visitor merely because you have multiple classes.
For example:
Circle
Rectangle
with only:
area()
doesn’t justify Visitor.
This is simpler:
interface Shape {
double area();
}
Visitor would introduce unnecessary machinery:
Visitor
Element
accept()
visit()
ConcreteVisitor
The pattern is valuable when the operation dimension is substantial.
19. Visitor vs Normal Polymorphism
This is an important design decision.
Normal polymorphism
Put behavior with the object:
Circle
+-- area()
Rectangle
+-- area()
This is usually preferable when the behavior is a natural responsibility of the object.
Visitor
Move behavior outside:
Circle
Rectangle
|
v
AreaVisitor
This is useful when there are many independent operations over a stable structure.
So:
Visitor should not replace ordinary polymorphism by default.
Start with normal polymorphism.
Introduce Visitor when the direction of change justifies it.
20. Visitor vs Strategy
Strategy
|
+-- Choose an algorithm
Example:
PaymentService
|
+-- CreditCardStrategy
+-- PayPalStrategy
+-- BankTransferStrategy
The question is:
Which algorithm should this object use?
Visitor:
Object Structure
|
+-- Circle
+-- Rectangle
+-- Triangle
|
v
Visitor
The question is:
How should this operation behave for each element type?
A useful distinction:
Strategy
= algorithm substitution
Visitor
= operation separation across element types
21. Visitor vs Template Method
These patterns solve fundamentally different problems.
Template Method
Base class
|
+-- fixed algorithm
|
+-- variable steps
It controls workflow.
Visitor
Element hierarchy
|
+-- accept(visitor)
|
v
Visitor
It separates operations from structure.
Think:
Template Method
"What sequence of steps must happen?"
Visitor
"What operation should I perform on this element?"
22. Visitor vs Pattern Matching
Modern languages may provide:
Pattern matching
Sealed classes
Algebraic data types
These can solve similar problems without the classic:
accept()
visit()
machinery.
Therefore the design principle matters more than mechanically implementing the GoF structure.
The real principle is:
Separate operations over a stable set of variants when that separation improves the system’s changeability.
23. Encapsulation Trade-Off
There is one subtle issue.
Suppose:
class Circle {
private double radius;
}
The Visitor needs the radius.
You may expose:
getRadius()
This is reasonable.
But if Visitors require:
getRadius()
getCenter()
getColor()
getBorder()
getInternalStateA()
getInternalStateB()
...
the Visitor may be fighting the object’s encapsulation.
This is a design warning.
Ask:
Is this operation genuinely external to the object, or am I extracting behavior that actually belongs inside it?
Visitor should not become an excuse for turning domain objects into bags of getters.
24. Advantages
+-------------------------------------------+
| Visitor Advantages |
+-------------------------------------------+
| Easy to add new operations |
| Keeps operations together |
| Keeps element classes focused |
| Avoids scattered instanceof logic |
| Works well with trees and ASTs |
| Supports multiple independent operations |
| Can enforce type-specific behavior |
+-------------------------------------------+
25. Disadvantages
+-------------------------------------------+
| Visitor Disadvantages |
+-------------------------------------------+
| New element types are expensive |
| Visitor interface can become large |
| Creates coupling to concrete element types |
| Can complicate encapsulation |
| More classes and indirection |
| Often overkill for simple hierarchies |
+-------------------------------------------+
26. A Principal-Engineer Way to Evaluate Visitor
Don’t ask:
“Does my code look like the Visitor UML?”
Ask:
“What is the dominant axis of change in this system?”
Suppose:
Elements:
10 types
rarely change
Operations:
20 operations
frequently change
Visitor is attractive.
But:
Elements:
constantly adding new types
Operations:
only one or two
Visitor is probably the wrong abstraction.
This is the core architectural decision.
27. The Pattern in One Diagram
CLIENT
|
| choose operation
v
ConcreteVisitor
|
|
v
+----------------+
| Visitor |
+----------------+
^
|
accept(visitor)
|
+------------+------------+
| | |
v v v
Circle Rectangle Triangle
| | |
+------------+------------+
|
Object Structure
The direction of responsibility is:
Elements
|
| expose structure/data
v
Visitor
|
| performs operation
v
ResultPremium Content
Unlock Visitor and all premium lessons with a subscription.
From ₹199.99/year — See plans