3.12 Visitor
The Visitor pattern lets you add new operations to the elements of an object structure without modifying their classes. It pulls "operations" out of the elements and into separate visitor objects. When you have a stable object structure but keep adding new functionality to it, Visitor shines.
In the lab below, a set of shapes (circle, square, rectangle) stays fixed; switch between "area visitor / perimeter visitor" to apply a brand-new algorithm to all shapes.
Double dispatch: accept and visit
Visitor relies on a trick called double dispatch:
interface Visitor {
void visit(Circle c);
void visit(Square s);
}
interface Shape { void accept(Visitor v); }
class Circle implements Shape {
public void accept(Visitor v) { v.visit(this); } // callback carrying its real type
}When you call shape.accept(visitor), the real type of shape first decides which accept runs, and it calls visitor.visit(this) — where this's static type is now fixed, selecting the right visit overload. The two dispatches together match "element type × operation type" precisely.
In the export lab below, the same document structure (heading, paragraph, image) outputs HTML or Markdown just by swapping the visitor.
Its sweet spot and its sore spot
Visitor has a very explicit asymmetric tradeoff:
- Great at adding operations: need a new algorithm over the structure? Write a new visitor; element classes don't change a line.
- Bad at adding element types: need a new element (say, a
Triangle)? You must edit every visitor interface and implementation to addvisit(Triangle).
So the rule is clear: element types stable, operations change often → use Visitor; the reverse (elements churn, operations stable) → Visitor will be painful, and ordinary polymorphic methods are better.
Real-world appearances
A compiler's multi-pass AST processing (type checking, optimization, code generation each a visitor), multi-format export of document/UI trees, and object-graph serialization are all classic Visitor battlegrounds.