Visitor Pattern
Represent an operation to be performed on elements of an object structure without changing the classes of the elements. A behavioral design pattern.
Overview
A growing object structure forces an ugly choice: add a new method to every element class, or litter the code with instanceof chains. The Visitor Pattern is a behavioral design pattern that takes a third path — it lets you define a new operation on an object structure without changing the classes of the elements on which it operates.
The pattern separates algorithms from the objects they operate on. Adding a new operation means writing a new visitor, not touching the element hierarchy — which is why it fits compilers, exporters, and linters where the node types are stable but operations keep multiplying.
When to Use
Use the Visitor Pattern when:
- You need to perform operations on all elements of a complex object structure — for tree structures, see Composite Pattern.
- The object structure is stable but operations on it change frequently — for interchangeable algorithms, see Strategy Pattern.
- You want to avoid polluting element classes with unrelated operations — see the Single Responsibility Principle for why that matters.
- The operation logic depends on the concrete class of the element, not just the interface.
- Examples: AST traversal in compilers, document export (PDF, HTML), report generation over entity trees, lint rules over a syntax tree.
Skip it when the element hierarchy changes often — every new element type forces you to update every visitor. For a concrete TypeScript application of the pattern over an AST, see Visitor Pattern for Extensible Operations.
Solution
Python
from abc import ABC, abstractmethod
from typing import List
class ShapeVisitor(ABC):
@abstractmethod
def visit_circle(self, circle):
pass
@abstractmethod
def visit_rectangle(self, rectangle):
pass
class Shape(ABC):
@abstractmethod
def accept(self, visitor: ShapeVisitor):
pass
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def accept(self, visitor: ShapeVisitor):
visitor.visit_circle(self)
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def accept(self, visitor: ShapeVisitor):
visitor.visit_rectangle(self)
class AreaVisitor(ShapeVisitor):
def __init__(self):
self.total = 0
def visit_circle(self, circle: Circle):
self.total += 3.14159 * circle.radius ** 2
def visit_rectangle(self, rectangle: Rectangle):
self.total += rectangle.width * rectangle.height
class DrawVisitor(ShapeVisitor):
def visit_circle(self, circle: Circle):
print(f"Drawing circle with radius {circle.radius}")
def visit_rectangle(self, rectangle: Rectangle):
print(f"Drawing rectangle {rectangle.width}x{rectangle.height}")
# Usage
shapes: List[Shape] = [Circle(5), Rectangle(4, 6)]
area_visitor = AreaVisitor()
for shape in shapes:
shape.accept(area_visitor)
print(f"Total area: {area_visitor.total}")
draw_visitor = DrawVisitor()
for shape in shapes:
shape.accept(draw_visitor)
JavaScript
class AreaVisitor {
constructor() {
this.total = 0;
}
visitCircle(circle) {
this.total += Math.PI * circle.radius ** 2;
}
visitRectangle(rectangle) {
this.total += rectangle.width * rectangle.height;
}
}
class DrawVisitor {
visitCircle(circle) {
console.log(`Drawing circle with radius ${circle.radius}`);
}
visitRectangle(rectangle) {
console.log(`Drawing rectangle ${rectangle.width}x${rectangle.height}`);
}
}
class Circle {
constructor(radius) {
this.radius = radius;
}
accept(visitor) {
visitor.visitCircle(this);
}
}
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
accept(visitor) {
visitor.visitRectangle(this);
}
}
// Usage
const shapes = [new Circle(5), new Rectangle(4, 6)];
const areaVisitor = new AreaVisitor();
shapes.forEach(s => s.accept(areaVisitor));
console.log(`Total area: ${areaVisitor.total}`);
const drawVisitor = new DrawVisitor();
shapes.forEach(s => s.accept(drawVisitor));
Java
public interface ShapeVisitor {
void visit(Circle circle);
void visit(Rectangle rectangle);
}
public interface Shape {
void accept(ShapeVisitor visitor);
}
public class Circle implements Shape {
public final double radius;
public Circle(double radius) {
this.radius = radius;
}
public void accept(ShapeVisitor visitor) {
visitor.visit(this);
}
}
public class Rectangle implements Shape {
public final double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public void accept(ShapeVisitor visitor) {
visitor.visit(this);
}
}
public class AreaVisitor implements ShapeVisitor {
public double total = 0;
public void visit(Circle circle) {
total += Math.PI * circle.radius * circle.radius;
}
public void visit(Rectangle rectangle) {
total += rectangle.width * rectangle.height;
}
}
public class DrawVisitor implements ShapeVisitor {
public void visit(Circle circle) {
System.out.println("Drawing circle with radius " + circle.radius);
}
public void visit(Rectangle rectangle) {
System.out.println("Drawing rectangle " + rectangle.width + "x" + rectangle.height);
}
}
// Usage
List<Shape> shapes = List.of(new Circle(5), new Rectangle(4, 6));
AreaVisitor area = new AreaVisitor();
shapes.forEach(s -> s.accept(area));
System.out.println("Total area: " + area.total);
DrawVisitor draw = new DrawVisitor();
shapes.forEach(s -> s.accept(draw));
Explanation
The Visitor Pattern has two roles:
- Visitor (
ShapeVisitor): declares avisit()method for each concrete element type. - Element (
Shape): declares anaccept()method that takes a visitor and calls the matchingvisit()method.
That pairing is called double dispatch — the mechanism the entire pattern is built on. The first dispatch is shape.accept(visitor) — the runtime picks the concrete element. The second is visitor.visit_circle(self) inside accept — the visitor picks the right overload for that concrete type. Two virtual calls resolve both directions of the type pair, so the visitor executes different code per element type without a single instanceof check.
Adding a new operation stays cheap — one more visitor class — while adding a new element type is the expensive direction: every existing visitor needs a visitX method, and in typed languages the compiler forces you through each one. Visitors also tend to need access to element internals, which weakens encapsulation: Circle had to expose radius publicly for AreaVisitor to work. Accept that trade-off explicitly or the pattern leaks abstractions.
Variants
| Variant | Description | Use Case |
|---|---|---|
| Classic Visitor | Separate visitor class per operation | Compilers, AST traversal |
| Acyclic Visitor | Visitor uses an abstract interface, not concrete types | When the element hierarchy is less stable |
| Reflective Visitor | Uses reflection to avoid accept() methods | Prototypes, scripting languages |
| Applied Visitor | Visitor over a TypeScript AST with serialization | Extensible operations on real structures |
What Works
- Use only when the element hierarchy is stable — adding a new element type requires changing all visitors.
- Group related operations into a single visitor instead of many small ones —
ExportVisitorhandling PDF and HTML is better than two micro-visitors. - Consider
instanceof+ sealed classes (Java 17+, or union types in TypeScript) as a modern alternative when the hierarchy is small. - Keep visitors stateless when possible; if they accumulate results like
total, document the state and reset it between traversals. - Combine with Composite for traversing tree structures — Composite owns the structure, Visitor owns the operation.
Common Mistakes
- Applying Visitor when the element hierarchy changes frequently — the maintenance cost lands on every visitor at once.
- Breaking encapsulation by exposing more internals than the visitor actually needs.
- Forgetting to add
accept()methods to new element types — the element silently falls back to a defaultvisitor fails at runtime. - Using Visitor when a simple polymorphic method override would do the job.
- Creating a separate visitor class for every tiny operation — class explosion with no benefit.
Troubleshooting
- A new element type broke every visitor: that’s the pattern’s known cost, not a bug. If the hierarchy isn’t actually stable, switch to pattern matching (sealed classes + exhaustive
switchin Java 17+, discriminated unions in TypeScript) instead of fighting the pattern. instanceofchecks creeping back in: double dispatch is being bypassed — an element is missing itsaccept()or the visitor is missing thevisitXoverload for that type.- Visitor can’t reach the element state it needs: expose only the minimum the operation requires, and treat the visitor↔element coupling as a deliberate design decision, not an accident.
- Stale results from a reused visitor: accumulated state (like
total) persists between traversals. Reset it at the start of each run or create a fresh visitor instance. - Dozens of tiny visitor classes: merge related operations into one cohesive visitor — one visitor per concern (export, draw, validate), not per method.
Key Takeaways
- Visitor moves operations out of element classes and into visitor objects, resolved by double dispatch:
accept()picks the element,visitX()picks the operation. - It pays off when the hierarchy is stable and operations change often — the classic case is compilers and exporters.
- Adding element types is the expensive direction: every existing visitor must be updated.
- The modern alternative is sealed types plus exhaustive pattern matching — same exhaustiveness guarantee, less boilerplate.
Common Production Pitfalls
- Applying Visitor to a hierarchy that gains a node type every sprint — the update cost lands on every visitor simultaneously.
- Letting visitors reach deep into element internals, so every element refactor breaks all visitors.
- Reusing a stateful visitor across traversals without resetting its accumulated results.
- Visitors that throw mid-traversal and leave partially computed results — decide error semantics up front.
- Hiding recursion inside
accept()on deep trees and hitting stack limits on large structures. - Reaching for Visitor where a plain
switch/matchover a sealed type would be shorter and easier to read.
Frequently Asked Questions
Why not just add methods to the element classes directly?
If the operation is specific to a client use case — PDF export, say — and not intrinsic to the element, adding it directly violates the Single Responsibility Principle. Visitor keeps element classes focused while letting you pile on operations.
What is double dispatch?
Single dispatch picks a method based on one object's type — ordinary polymorphism. Double dispatch picks based on two types: the element via accept(), then the visitor via visitX(this). That pairing is what lets the visitor run type-specific code without instanceof.
Is there a modern alternative to Visitor?
In languages with sealed classes and pattern matching — Java 17+, TypeScript with discriminated unions — a switch/match with exhaustive type checking gives you the same safety without the accept() boilerplate. Visitor still wins when you need the operation logic in a separate, reusable object.
When should I avoid the Visitor pattern?
When element types are added frequently, when there's only one or two operations that could live on the elements themselves, or when your language already has good pattern matching. Visitor earns its complexity only in the stable-hierarchy + many-operations quadrant.
How does Visitor relate to Iterator and Composite?
What happens when I add a new element type?
Every concrete visitor must implement a new visitX method. In typed languages the compiler enforces this — which is both the feature (nothing is silently unhandled) and the cost (a new node type touches all visitors at once).
Related Resources
Composite Pattern
Compose objects into tree structures to represent part-whole hierarchies. A structural design pattern for treating individual objects and compositions uniformly.
PatternIterator Pattern
Provide a way to access elements of a collection sequentially without exposing its underlying representation. A behavioral design pattern for traversal.
PatternStrategy Pattern
Define a family of algorithms, encapsulate each one, and make them interchangeable. A behavioral design pattern for flexible behavior selection.
PatternVisitor Pattern: Extensible Operations in TypeScript
Separate algorithms from the objects they operate on, allowing new operations to be added without modifying existing element classes
GuidePractical Design Patterns Guide
Pick the right design pattern for each problem: Singleton, Factory, Observer, Strategy and more, with runnable code and honest trade-offs.