AcademyDesign Patterns
Visitor pattern
Adding a new operation to a fixed set of classes without editing them, the every-visitor update a new node type costs, and why it fails once classes change more than the ops.
6 min readUpdated 3 exercises
Visitor adds a new operation to a set of classes without editing any of them: the operation lives in its own class instead, and the classes just route to it.
A Visitor moves an operation out of the classes it works on and into its own class, with each target class only holding a small accept(visitor) method that routes to the right visitor method. Adding a new operation means writing a new visitor, and the target classes never change again.
The problem it solves
You have a tree of syntax nodes: identifiers, function calls, literals. You want several operations over it: lint it, minify it, print it back as code, count its nodes.
The direct approach adds each operation as a method on every node class:
class Identifier { lint() {} minify() {} print() {} count() {} }
class Call { lint() {} minify() {} print() {} count() {} }- Every new operation edits every class. Ten node types and a new operation means ten edits.
- Classes fill up with unrelated code. A node class knows about linting, minifying and printing.
- You may not own the classes. If the node types come from a library, you cannot add methods to them at all.
The fix is to keep each operation together in one class, and give each node type a single accept method that hands itself to it.
Structure
Three parts:
- The element interface (
accept(visitor)): implemented by each class in the structure. - Concrete elements (
Dog,Cat): each calls the visitor method for its own type, passing itself. - The visitor (
SoundVisitor): has one method per element type, containing the operation.
class Dog {
accept(visitor) { return visitor.visitDog(this); }
}
class Cat {
accept(visitor) { return visitor.visitCat(this); }
}
class SoundVisitor {
visitDog(dog) { return 'Woof'; }
visitCat(cat) { return 'Meow'; }
}
// [new Dog(), new Cat()].map((a) => a.accept(new SoundVisitor())) → ['Woof', 'Meow']Dog.accept calls visitDog, and Cat.accept calls visitCat. This is called double dispatch: the method that runs depends on the type of the visitor and the type of the element. A new operation, such as FeedVisitor, is a new class, and Dog and Cat stay untouched.
Where it is used in real systems
| Use case | The elements | The visitors |
|---|---|---|
| Compilers and linters | Syntax tree nodes | Type checker, linter, code generator. |
| Code transformation | Syntax tree nodes | Babel plugins, codemods. |
| Document processing | Headings, paragraphs, images | Export to HTML, PDF, plain text. |
| File system walks | Files and folders | Size calculator, search, permission audit. |
| Pricing and invoices | Different item types | Tax, shipping, discount calculations. |
| Serialization | Object graph | JSON, XML, binary writers. |
| Schema validation | Schema nodes | Validate, generate types, generate docs. |
The signal: a stable set of node types, and a growing list of operations over them.
Visitors you already use
- Babel plugins and ESLint rules. A plugin is an object with methods named after node types, such as
Identifier(path) {}orCallExpression(node) {}. That is a visitor. - TypeScript compiler API.
ts.forEachChildandvisitNodewalk a syntax tree, calling code for each kind of node. - Markdown and HTML processors. unified, remark and rehype use visitors over document trees.
- GraphQL.
visit(ast, { Field(node) {} })applies a visitor to a query. - Prettier and codemod tools built on syntax trees.
The trade-off
Visitor swaps which kind of change is easy.
- Adding a new operation is easy. Write one visitor. No element changes.
- Adding a new element type is hard. Every existing visitor needs a new
visitXmethod, or the code stops compiling.
That is the reverse of ordinary methods, where a new type is easy and a new operation is not. Choose based on what changes more often: if the node types are stable and the operations keep growing, use a Visitor. If new node types keep appearing, use plain methods.
Visitor in TypeScript
TypeScript offers a lighter alternative for closed sets of types: a discriminated union and a switch.
type Animal = { kind: 'dog' } | { kind: 'cat' };
function sound(animal: Animal): string {
switch (animal.kind) {
case 'dog': return 'Woof';
case 'cat': return 'Meow';
}
}The compiler checks the switch is exhaustive, so a new animal type makes every operation fail to compile until it is handled. You get the same benefits with less code, and no accept methods. Reach for the classic Visitor when the element classes come with behavior and are a real hierarchy, or when you are following a library's visitor API.
When not to use it
- The element types change often. Each new type touches every visitor.
- The hierarchy is small and simple. Two element types and one operation do not justify the ceremony.
- Visitors need private data. A visitor is outside the class, so it can only use what the elements expose. That can force you to widen the public interface.
- A union type and a `switch` are enough. In TypeScript this is usually the simpler answer.
- Deep or cyclic structures. Recursive visiting needs care to avoid stack overflows and infinite loops.
What interviewers ask
- "What is the Visitor pattern?" Separating an operation from the classes it works on, so new operations can be added without changing them.
- "What is double dispatch?" The method called depends on both the element's type and the visitor's type, achieved through
acceptcalling the matchingvisitmethod. - "What is the main drawback?" Adding a new element type forces a change in every visitor.
- "Where is it used in JavaScript?" Babel, ESLint and TypeScript tooling that walks syntax trees.
- "How would you do it in TypeScript without classes?" A discriminated union with an exhaustive
switch.
The mistake to avoid
An accept method that calls the wrong visitor method, such as Cat.accept calling visitor.visitDog(this). It is easy to make by copy and paste, and it fails silently in JavaScript: the cat gets barked at. Each element must call the method named for its own type, and a test that visits one of each type catches the slip.
The mechanism above, an element that hands itself to the code that operates on it, is the same in JavaScript, Python or Java. Only the syntax changes.
Now practice it
1 challenge · 2 quizzesReading this page is the cheap half. These are the exercises that make you use it.
- VisitorProAdd a new operation to a fixed set of classes through a visitor instead of editing them — the accept()/visit() routing this page traces.ChallengeSolve · HardChallengeSolve · Hard
- Design Patterns · Behavioral PatternsTen quick questions across Strategy, State, Observer, Command and the rest of the behavioral family, to check the mental model before this one.QuizTake · HardQuizTake · Hard
- Design Patterns · Architectural Patterns — Series 3ProA second, tougher pass on the same behavioral family, for once the first set feels easy.QuizTake · HardQuizTake · Hard
Read next
Iterator pattern
Walking a collection without exposing how it is stored, the extra object between caller and data it costs, and the wrong answer when next() runs past the end or data mutates.
5 min
Interpreter pattern
Evaluating a small tree of grammar rules one node at a time, the class per rule that costs fast past a toy language, and the point a real parser beats one more class.
6 min