Visitor Pattern
Visitor Pattern
Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.
Intent
The Visitor decouples operations from the element classes they operate on. Instead of adding new methods to each element class every time a new operation is needed, you define a Visitor class that implements the operation across all element types. The element classes only need a single accept(visitor) method. New operations are new Visitor classes — no element classes are touched. This isolates unrelated operations from each other and from the element hierarchy.
When NOT to Use
- The element hierarchy changes frequently — adding a new element type requires changing every existing Visitor interface and every ConcreteVisitor implementation. This violates Open/Closed for elements. If you add new element types more often than new operations, Visitor is the wrong choice — use polymorphism (virtual dispatch) instead
- Breaking element encapsulation is required to expose enough internal state to the Visitor — if Visitors need access to private internals, the pattern forces poor encapsulation
- The element hierarchy is small and operations are unlikely to grow — Visitor adds significant structural overhead for minimal gain
When to Use
- The element hierarchy is stable (rarely gains new types) and operations change or grow frequently
- Many distinct and unrelated operations must be performed on an object structure without polluting element classes with unrelated methods
- Accumulating state across multiple elements during traversal is needed (e.g., computing totals, generating reports, collecting statistics)
How It Works
Participants: Visitor (interface declaring visitConcreteElementA(), visitConcreteElementB() — one method per concrete element type), ConcreteVisitor (implements one operation across all element types), Element (interface declaring accept(visitor: Visitor)), ConcreteElement (implements accept() by calling visitor.visit(this)), ObjectStructure (iterates elements and calls accept() on each).
The double-dispatch mechanism: element.accept(visitor) dispatches on the runtime type of element (first dispatch), which then calls visitor.visit(element) dispatching on the runtime type of the visitor (second dispatch). Both the element type and the visitor type are resolved at runtime, ensuring the correct visit method executes for the correct element type.
Class Diagram
classDiagram
class Visitor {
<<interface>>
+visitElementA(e : ElementA)*
+visitElementB(e : ElementB)*
}
class ConcreteVisitor1 {
+visitElementA(e : ElementA)
+visitElementB(e : ElementB)
}
class ConcreteVisitor2 {
+visitElementA(e : ElementA)
+visitElementB(e : ElementB)
}
class Element {
<<interface>>
+accept(v : Visitor)*
}
class ElementA {
+accept(v : Visitor)
+operationA()
}
class ElementB {
+accept(v : Visitor)
+operationB()
}
Visitor <|.. ConcreteVisitor1
Visitor <|.. ConcreteVisitor2
Element <|.. ElementA
Element <|.. ElementB
ElementA --> Visitor : accept calls v.visitElementA(this)
ElementB --> Visitor : accept calls v.visitElementB(this)
note for Element "Double dispatch:\nelement.accept(visitor) calls\nvisitor.visitElement(this)"
TypeScript Example
// Visitor — TypeScript
// Source: GoF Design Patterns, p.331
interface ShapeVisitor {
visitCircle(c: Circle): void;
visitRectangle(r: Rectangle): void;
}
interface Shape {
accept(v: ShapeVisitor): void;
}
class Circle implements Shape {
constructor(public radius: number) {}
accept(v: ShapeVisitor) { v.visitCircle(this); } // double dispatch
}
class Rectangle implements Shape {
constructor(public width: number, public height: number) {}
accept(v: ShapeVisitor) { v.visitRectangle(this); } // double dispatch
}
class AreaVisitor implements ShapeVisitor {
visitCircle(c: Circle) { console.log(Math.PI * c.radius ** 2); }
visitRectangle(r: Rectangle) { console.log(r.width * r.height); }
}
const shapes: Shape[] = [new Circle(5), new Rectangle(3, 4)];
const areaVisitor = new AreaVisitor();
shapes.forEach(s => s.accept(areaVisitor));Java Example
// Visitor — Java
// Source: GoF Design Patterns, p.331
interface ShapeVisitor {
void visitCircle(Circle c);
void visitRectangle(Rectangle r);
}
interface Shape {
void accept(ShapeVisitor v);
}
class Circle implements Shape {
double radius;
Circle(double r) { this.radius = r; }
public void accept(ShapeVisitor v) { v.visitCircle(this); }
}
class Rectangle implements Shape {
double width, height;
Rectangle(double w, double h) { this.width = w; this.height = h; }
public void accept(ShapeVisitor v) { v.visitRectangle(this); }
}
class AreaVisitor implements ShapeVisitor {
public void visitCircle(Circle c) { System.out.println(Math.PI * c.radius * c.radius); }
public void visitRectangle(Rectangle r) { System.out.println(r.width * r.height); }
}Open/Closed Asymmetry: Visitor makes it easy to add new operations (new ConcreteVisitor class, no element changes). But adding a new element type requires updating every existing Visitor interface and implementation. Use Visitor when the element hierarchy is stable and operations are volatile. Use polymorphism when element types are volatile and operations are stable.
Related Concepts
| Pattern | Relationship |
|---|---|
| Composite-Pattern | Visitor is typically applied to Composite trees — the ObjectStructure traverses a Composite and calls accept() on each node |
| Iterator-Pattern | Iterator traverses a structure; Visitor performs an operation during traversal — the two are frequently combined (Iterator provides the traversal, Visitor provides the operation) |
| Command-Pattern | Command encapsulates one request as an object; Visitor encapsulates one operation type across many element types — different scopes of encapsulation |
Sources
- Gamma, Helm, Johnson, Vlissides — Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, 1994, p.331
Backlinks
Notes that link here: GoF-Patterns-MOC