2026-05-05
You have a stable hierarchy of types — AST nodes, file system entries, shape primitives — and you keep needing to add new operations across all of them. Every new operation forces you to crack open every class and add another method. The Visitor pattern flips this: instead of putting operations on the types, you put the type-dispatch on the operation.
The mechanic is double dispatch. Each node exposes a single accept(visitor) method that calls back visitor.visitConcreteType(this). The runtime type of the node selects the accept implementation; the visitor's method overload selects the operation. Two dispatches, one for each axis.
Concrete example — a small expression evaluator:
NumberLit, BinaryOp, VariableRef, FunctionCallPrettyPrinter, TypeChecker, Compiler) with four visit methods. The node classes stay frozen.This is the core trade-off, often called the Expression Problem:
Rule of thumb: if your type hierarchy changes more than ~2× per year but operations rarely change, skip Visitor and use polymorphic methods. If the hierarchy is stable (think: language grammars, document formats, geometric primitives) but you keep inventing new things to do with it, Visitor pays for itself after roughly 3 operations × 4 types — about 12 dispatch sites.
Real-world sightings: Roslyn's C# compiler uses CSharpSyntaxVisitor<T> for analyzers and refactorings. Babel's AST traversal is essentially Visitor. ESLint rules are Visitors over the JS AST.
Practical pitfalls:
match or a dict-of-handlers is simpler and gives you the same separation without ceremony.