Visitor Pattern — Separate Operations from Object Structure
What You'll Learn
You will learn how the Visitor pattern lets you add new operations to a class hierarchy without modifying the classes themselves, by routing calls through a double-dispatch mechanism. You will understand the trade-off between adding new operations (easy) and adding new element types (hard).
Why It Matters
An Abstract Syntax Tree may need operations for Type Checking, optimisation, Code Generation, and pretty printing. Adding each operation to every node class pollutes them with unrelated logic. Visitor separates the operation from the object structure so new operations can be added without touching existing classes. Without Visitor, adding a new operation means modifying every node class — with Visitor, you write one new class.
Real-World Use
Compilers use visitors to traverse ASTs for Semantic Analysis, optimisation passes, and Code Generation. DodaTech's configuration validator uses visitors to walk the config tree and apply different validation rules — required fields, type checks, cross-reference resolution — without coupling validation logic to config node classes. Adding a new validation rule means writing one visitor; the config node classes remain untouched.
The Pattern
Visitor declares visit methods for each element type. Element accepts a visitor. ConcreteVisitor implements operations. ObjectStructure provides iteration over elements.
from abc import ABC, abstractmethod
class Visitor(ABC):
@abstractmethod
def visit_circle(self, circle: "Circle") -> str:
pass
@abstractmethod
def visit_rectangle(self, rect: "Rectangle") -> str:
pass
class Shape(ABC):
@abstractmethod
def accept(self, visitor: Visitor) -> str:
pass
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def accept(self, visitor: Visitor) -> str:
return visitor.visit_circle(self)
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def accept(self, visitor: Visitor) -> str:
return visitor.visit_rectangle(self)
class AreaVisitor(Visitor):
def visit_circle(self, circle: Circle) -> str:
area = 3.14159 * circle.radius ** 2
return f"Circle area: {area:.2f}"
def visit_rectangle(self, rect: Rectangle) -> str:
area = rect.width * rect.height
return f"Rectangle area: {area:.2f}"
class XMLExportVisitor(Visitor):
def visit_circle(self, circle: Circle) -> str:
return f'<circle radius="{circle.radius}" />'
def visit_rectangle(self, rect: Rectangle) -> str:
return f'<rect width="{rect.width}" height="{rect.height}" />'
shapes = [Circle(5), Rectangle(4, 6)]
area_visitor = AreaVisitor()
xml_visitor = XMLExportVisitor()
for shape in shapes:
print(shape.accept(area_visitor))
for shape in shapes:
print(shape.accept(xml_visitor))
Circle area: 78.54
Rectangle area: 24.00
<circle radius="5" />
<rect width="4" height="6" />
Structure
classDiagram
class Visitor {
<>
+visitConcreteElementA(ElementA)
+visitConcreteElementB(ElementB)
}
class ConcreteVisitor1 {
+visitConcreteElementA(ElementA)
+visitConcreteElementB(ElementB)
}
class ConcreteVisitor2 {
+visitConcreteElementA(ElementA)
+visitConcreteElementB(ElementB)
}
class Element {
<>
+accept(Visitor)
}
class ConcreteElementA {
+accept(Visitor)
+operationA()
}
class ConcreteElementB {
+accept(Visitor)
+operationB()
}
ConcreteVisitor1 ..|> Visitor
ConcreteVisitor2 ..|> Visitor
ConcreteElementA ..|> Element
ConcreteElementB ..|> Element
ConcreteElementA --> Visitor : double-dispatch
ConcreteElementB --> Visitor : double-dispatch
Real-World Usage
- Java compiler
javac— uses visitor pattern for AST traversal in linting and Code Generation. - Python
ast.NodeVisitor— standard library visitor for walking and transforming Python AST nodes. - .NET Roslyn analyzers — walk syntax trees using the visitor pattern to detect code issues.
- Serialisation frameworks — Jackson, Gson, and other libraries use visitors to traverse object graphs and produce JSON/XML.
Related Patterns
- Composite defines the object structure that visitors traverse.
- Iterator walks the structure; Visitor performs operations at each node.
- Interpreter uses visitors for complex Semantic Analysis of expression trees.
- Strategy encapsulates algorithms; Visitor encapsulates operations across a structure.
Pros and Cons
| Pros | Cons |
|---|---|
| New operations added without modifying element classes | Adding new element types requires changing all visitors |
| Related operations grouped in a single class | Breaks Encapsulation — visitors may need access to private fields |
| Double dispatch simplifies complex conditional logic | Can be overkill for simple, stable hierarchies |
| Accumulates state across the traversal | Cyclic dependencies between visitor and element packages |
The code demonstrates double dispatch. When shape.accept(<a href="/design-patterns/visitor/">visitor</a>) is called, the specific Circle or Rectangle class determines which visit_* method is called on the visitor. This two-step dispatch means the visitor can perform type-specific operations without using isinstance checks. Adding a new operation like PerimeterVisitor requires no changes to Circle or Rectangle.
Practice Questions
- Implement a visitor that counts nodes of each type in a tree structure.
- How does double dispatch work, and why is it essential for the Visitor pattern?
- Compare Visitor with a simple loop over elements with type checks — what maintenance costs does each approach carry?
- Implement a visitor that transforms elements (mutates the structure) rather than just reading them.
Challenge
Add a new shape called Triangle to the hierarchy. What changes are required to existing visitors? What does this reveal about the trade-off inherent in the Visitor pattern?
Real-World Task
If your application has a stable class hierarchy but needs frequent new operations, use DodaTech's AST explorer to Prototype a Visitor-based approach and compare the maintenance cost against adding methods to each class.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro