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
The Visitor Pattern is a behavioral design pattern that lets you define a new operation on an object structure without changing the classes of the elements on which it operates. It separates algorithms from the objects they operate on, making it easy to add new operations to a complex class hierarchy.
When to Use
Use the Visitor Pattern when:
- You need to perform operations on all elements of a complex object structure. See Composite Pattern for tree structures.
- The object structure is stable but operations on it change frequently. See Strategy Pattern for interchangeable algorithms.
- You want to avoid polluting element classes with unrelated operations. See Single Responsibility Principle for focused classes.
- The operation logic depends on the concrete class of the element, not just the interface
- Examples: AST traversal (compilers), document export (PDF, HTML), report generation on entity trees
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 appropriatevisit()method
This is known as double dispatch: the first dispatch is shape.accept(visitor), the second is visitor.visit(circle) inside the element’s accept method. This lets the visitor execute different code based on the element’s concrete type without instanceof checks.
Variants
| Variant | Description | Use Case |
|---|---|---|
| Classic Visitor | Separate visitor class per operation | Compilers, AST traversal |
| Acyclic Visitor | Visitor uses abstract interface, not concrete types | When element hierarchy is unstable |
| Reflective Visitor | Uses reflection to avoid accept() methods | Prototypes, scripting |
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
- Consider
instanceof+ sealed classes (Java 17+) as a modern alternative - Keep visitors stateless when possible, or clearly document mutable state
- Use alongside Composite for traversing tree structures
Common Mistakes
- Applying Visitor when the element hierarchy changes frequently (high maintenance cost)
- Breaking encapsulation by exposing too many internals to visitors
- Forgetting to add
accept()methods to new element types - Using Visitor when a simple polymorphic method override would suffice
- Creating a separate visitor class for every tiny operation, creating class explosion
Troubleshooting
- Pattern does not fit the problem: re-evaluate the forces (performance, scalability, team size, coupling). A pattern is only appropriate when its trade-offs match your constraints.
- Too many abstractions: if adding a pattern increases complexity without a clear benefit, simplify. Not every module needs a factory, decorator, or strategy.
- Tight coupling after refactoring: check that interfaces are stable and dependencies point inward.
- Tests break when the design changes: favor stable contracts over internal structure.
- Performance regression from indirection: measure before and after. Layers, decorators, and adapters can add latency; cache or inline hot paths if needed.
Quick Reference
- Main command: run the base solution from the article and verify the expected result.
- Validation: confirm tests pass and key metrics did not degrade.
- Rollback: if something fails, revert the change and consult the Troubleshooting section.
Further Reading
- Official documentation: check the current reference for the framework or tool used.
- Related guides: explore the visitor and pattern guides for deeper coverage.
- Complementary patterns: review design patterns applicable to your technology stack.
- Public postmortems: study real incidents from teams that faced similar production issues.
Production Notes
- Deploy gradually using canary or blue-green to catch regressions early.
- Configure alerts for error rate, p99 latency, and failure rate before enabling in production.
- Document the rollback in the runbook; test the procedure in staging at least once per quarter.
- Review structured logs with correlation IDs to trace requests end-to-end during incidents.
Key Takeaways
- Apply visitor pattern when you need a practical solution for your use case.
- Monitor performance after implementation; measure latency, errors, and resource usage before and after.
- Check the Troubleshooting section for common failures; most have documented root causes with fixes.
- Keep dependencies updated and run tests in CI to prevent production regressions.
Common Production Pitfalls
- Applying the pattern where no abstraction is needed, adding accidental complexity.
- Letting the pattern leak into unrelated modules and blur ownership boundaries.
- Over-engineering the first implementation instead of starting simple and measuring pain.
- Skipping contract tests, so refactors silently break consumers.
- Ignoring failure modes that the pattern does not cover.
- Using the pattern as a default instead of choosing the right tool for the current scale.
- Forgetting to document when to stop using the pattern and what replaces it.
- Missing observability around the pattern’s performance and error propagation.
Frequently Asked Questions
Why not just add methods to the element classes directly?
If the operation is specific to a client use case (e.g., PDF export) and not intrinsic to the element, adding it directly violates the Single Responsibility Principle. Visitor keeps element classes focused.
Is there a modern alternative to Visitor?
In languages with sealed classes and pattern matching (Java 17+, TypeScript 5.3+), you can use switch expressions with exhaustive type checking instead of the classic Visitor double-dispatch.
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.