Visitor
A shape library starts with just calculateArea(). Then someone needs
exportToSVG(). Then calculatePerimeter(). Every time, you're back
editing Circle, Square, and Triangle all over again to add
one more method. The Visitor pattern flips this around: new
operations become new classes instead of new methods, and the shape classes themselves
never need to change again.
⚠️ The Problem
You have a small hierarchy of shapes — Circle, Square,
Triangle — and you keep needing new operations across all of them:
calculateArea() first, then exportToSVG(), then
calculatePerimeter(). If each operation is a method added directly to every shape
class, adding the Nth operation means opening and editing N files every single time, even
though each individual shape class has nothing to do with the others. Worse, some of these
operations (like SVG export) are arguably a rendering concern that has no business living
inside a geometry class in the first place — but there's nowhere else natural to put a method
that needs to behave differently per shape.
✅ The Solution
Define a Visitor interface with one visit method per concrete
shape — visitCircle(circle), visitSquare(square),
visitTriangle(triangle). Give every shape class exactly one new method,
accept(visitor), whose entire body is just calling back the matching visit method:
circle.accept(visitor) calls visitor.visitCircle(this). This callback
handoff is called double dispatch. Now a brand-new operation — say,
AreaVisitor or SVGExportVisitor — is a whole new class implementing
Visitor, with zero changes to Circle, Square, or
Triangle. The tradeoff: adding a brand-new shape class still means updating every
existing Visitor implementation to handle it, since each one needs a
visit method per shape.
Example 1 — a shape hierarchy with area and SVG export
interface Visitor {
visitCircle(c: Circle): any;
visitSquare(s: Square): any;
visitTriangle(t: Triangle): any;
}
interface Shape {
accept(visitor: Visitor): any;
}
class Circle implements Shape {
constructor(public radius: number) {}
accept(visitor: Visitor) { return visitor.visitCircle(this); }
}
class Square implements Shape {
constructor(public side: number) {}
accept(visitor: Visitor) { return visitor.visitSquare(this); }
}
class Triangle implements Shape {
constructor(public base: number, public height: number) {}
accept(visitor: Visitor) { return visitor.visitTriangle(this); }
}
// Operation 1: compute area — no changes to any Shape class needed.
class AreaVisitor implements Visitor {
visitCircle(c: Circle) { return Math.PI * c.radius ** 2; }
visitSquare(s: Square) { return s.side ** 2; }
visitTriangle(t: Triangle) { return 0.5 * t.base * t.height; }
}
// Operation 2: export to SVG — again, zero changes to Shape classes.
class SVGExportVisitor implements Visitor {
visitCircle(c: Circle) { return `<circle r="${c.radius}" />`; }
visitSquare(s: Square) { return `<rect width="${s.side}" height="${s.side}" />`; }
visitTriangle(t: Triangle) {
return `<polygon points="0,0 ${t.base},0 ${t.base / 2},${t.height}" />`;
}
}
const shapes: Shape[] = [new Circle(5), new Square(4), new Triangle(6, 3)];
const areaVisitor = new AreaVisitor();
const totalArea = shapes.reduce((sum, shape) => sum + shape.accept(areaVisitor), 0);
console.log(totalArea.toFixed(2)); // 94.54
const svgVisitor = new SVGExportVisitor();
shapes.forEach((shape) => console.log(shape.accept(svgVisitor)));
Every shape's accept() method is one line and never changes again, no matter how
many new visitors get written. AreaVisitor and SVGExportVisitor each
carry all the logic for their own operation, spread across shape-specific
visit methods, entirely separate from each other.
Example 2 — a secondary context: an AST pretty-printer
from abc import ABC, abstractmethod
class Visitor(ABC):
@abstractmethod
def visit_number(self, node): ...
@abstractmethod
def visit_add(self, node): ...
@abstractmethod
def visit_multiply(self, node): ...
class Node(ABC):
@abstractmethod
def accept(self, visitor): ...
class Number(Node):
def __init__(self, value):
self.value = value
def accept(self, visitor):
return visitor.visit_number(self)
class Add(Node):
def __init__(self, left, right):
self.left = left
self.right = right
def accept(self, visitor):
return visitor.visit_add(self)
class Multiply(Node):
def __init__(self, left, right):
self.left = left
self.right = right
def accept(self, visitor):
return visitor.visit_multiply(self)
# New operation: pretty-printing — no changes to Number/Add/Multiply needed.
class PrettyPrintVisitor(Visitor):
def visit_number(self, node):
return str(node.value)
def visit_add(self, node):
return f"({node.left.accept(self)} + {node.right.accept(self)})"
def visit_multiply(self, node):
return f"({node.left.accept(self)} * {node.right.accept(self)})"
# Represents: (3 + 4) * 2
tree = Multiply(Add(Number(3), Number(4)), Number(2))
printer = PrettyPrintVisitor()
print(tree.accept(printer)) # (3 + 4) * 2
Same double-dispatch handoff as the shapes example: each AST node's accept()
calls back into the matching visit_* method. A future
EvaluateVisitor that actually computes the numeric result would be a new class,
with Number, Add, and Multiply untouched.
👍 When To Use It
- You have a relatively stable set of classes but keep adding new operations across all of them — Visitor moves the "growth axis" from the classes to the operations.
- An operation's logic doesn't really belong inside the element classes themselves (like rendering or serialization logic sitting inside a geometry class).
- You want each operation's logic for every type gathered in one place, rather than spread thinly across many classes' methods.
👎 When NOT To Use It
- When the set of element classes changes often — every new class means updating every existing Visitor, which can get expensive fast if the hierarchy isn't stable.
- When you only ever need one or two operations total — plain virtual methods on each class are simpler until operations start multiplying.
- In languages without real method overloading/dispatch support, implementing double dispatch cleanly can be awkward enough that a simpler type-check-based approach reads more clearly.
❓ FAQ
What exactly is "double dispatch," and why does it need a whole extra method?
Normal method calls in most object-oriented languages are "single dispatch" — which
method actually runs depends on just one thing, the runtime type of the object the method is
called on. If you write generic code like visitor.visit(shape) where
shape is typed as the general Shape interface, single dispatch can
only pick a method based on visitor's type — it can't also pick based on
shape's concrete type, because that decision already happened once, at the
single call site, using the compile-time/declared type of the argument. Double
dispatch fixes this by routing the call through two single-dispatch steps instead of
one: first shape.accept(visitor) dispatches on shape's real
concrete type (a normal virtual method call — this is why every concrete shape needs its own
tiny accept override), and *inside* that method, visitor.visitCircle(this)
dispatches a second time on which concrete Visitor it is. The combination of the
two picks the exactly correct method for the actual pair of (concrete element type, concrete
visitor type) — something a single method call could never do on its own.
Why not just use instanceof / type-checking instead of accept()?
You could write if (shape instanceof Circle) { ... } else if (shape instanceof
Square) { ... } inside each visitor, and it would work. But then every visitor has to
repeat that type-checking chain, the compiler can't help you catch a missing case (an
unhandled shape subtype silently falls through instead of failing to compile), and it's
exactly the kind of scattered conditional logic most patterns in this track exist to avoid.
accept()/double dispatch gets you the equivalent dispatch table for free, checked
by the type system in languages that support method overloading properly.
Doesn't Visitor break encapsulation, since visitors often need shape internals?
It can lean that way — a visitCircle(circle) method typically needs to read
circle.radius, which means either that field has to be public/exposed via a
getter, or the element class has to expose exactly the data its visitors need. This is a
known, accepted tradeoff of the pattern: it optimizes for adding operations without touching
element classes, at the cost of those element classes exposing a bit more of their internals
than they might otherwise.
🙋 There Are No Dumb Questions
Q: If adding a new shape means updating every Visitor, isn't that just moving the
same maintenance problem somewhere else?
A: In a sense, yes — Visitor doesn't eliminate the coupling between "the set of types" and "the
set of operations," it just chooses which side absorbs a change. If new element types are rare
but new operations are frequent (the shape/AST scenarios above), Visitor is a clear win. If it's
the other way around — new element types show up constantly but the set of operations is fixed
— plain virtual methods on each class are usually the better fit, since that's the axis that
changes.
✅ Quick Check
What does "double dispatch" mean in the Visitor pattern?
That's all 23 patterns — time for the recap.