Coding · Design patterns
Visitor pattern explained with examples
Short answer
Visitor is a behavioral design pattern that lets you add new operations to a group of related classes without changing those classes. Each operation is written as a visitor object with one method per element type, and each element has an accept(visitor) method that calls the matching visitor method. This technique, called double dispatch, picks the code to run based on both the element type and the visitor type. Compilers, linters and code transformers use visitors to walk syntax trees.
Intent and the problem it solves
Intent: 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. It is one of the behavioral patterns in the Gang of Four (GoF) book.
Suppose a drawing program has three shape classes: Circle, Rectangle and Group. Over time you need many operations on them: compute the total area, export to SVG, export to JSON, count the shapes, check for overlaps. If every operation becomes a method on every shape class, the shape classes grow with code that has nothing to do with being a shape, and every new operation touches all of them.
With Visitor, each operation becomes one class (an area visitor, an SVG visitor) that contains the code for every shape type. The shapes only need one generic method, accept. Adding an operation means writing one new visitor; the shape classes stay unchanged.
Structure and participants
| Role | Responsibility |
|---|---|
| Visitor | Interface with one visit method per concrete element type, such as visitCircle and visitRectangle |
| Concrete visitor | Implements one operation for all element types and may collect a result |
| Element | Interface with an accept(visitor) method |
| Concrete element | Implements accept by calling the visit method for its own type, passing itself |
| Object structure | A collection or tree of elements, such as a document or a syntax tree, that visitors walk over |
Why accept is needed: in TypeScript, Java and C#, a method call chooses its implementation from the type of the object it is called on (single dispatch). The accept call dispatches on the element type, and the call inside it, such as visitor.visitCircle(this), dispatches on the visitor type. Together they form double dispatch.
TypeScript implementation
The visitor interface is generic in its return type, so one visitor can compute numbers and another can produce strings.
interface ShapeVisitor<R> {
visitCircle(c: Circle): R;
visitRectangle(r: Rectangle): R;
visitGroup(g: Group): R;
}
interface Shape {
accept<R>(visitor: ShapeVisitor<R>): R;
}
class Circle implements Shape {
constructor(readonly radius: number) {}
accept<R>(v: ShapeVisitor<R>) { return v.visitCircle(this); }
}
class Rectangle implements Shape {
constructor(readonly width: number, readonly height: number) {}
accept<R>(v: ShapeVisitor<R>) { return v.visitRectangle(this); }
}
class Group implements Shape {
constructor(readonly children: Shape[]) {}
accept<R>(v: ShapeVisitor<R>) { return v.visitGroup(this); }
}
class AreaVisitor implements ShapeVisitor<number> {
visitCircle(c: Circle) { return Math.PI * c.radius ** 2; }
visitRectangle(r: Rectangle) { return r.width * r.height; }
visitGroup(g: Group): number {
return g.children.reduce((sum, child) => sum + child.accept(this), 0);
}
}
class SvgVisitor implements ShapeVisitor<string> {
visitCircle(c: Circle) { return '<circle r="' + c.radius + '"/>'; }
visitRectangle(r: Rectangle) { return '<rect width="' + r.width + '" height="' + r.height + '"/>'; }
visitGroup(g: Group): string { return "<g>" + g.children.map((c) => c.accept(this)).join("") + "</g>"; }
}
const drawing = new Group([new Circle(1), new Rectangle(2, 3), new Group([new Rectangle(1, 1)])]);
console.log(drawing.accept(new AreaVisitor()).toFixed(2)); // 10.14
console.log(drawing.accept(new SvgVisitor()));
// <g><circle r="1"/><rect width="2" height="3"/><g><rect width="1" height="1"/></g></g>A third operation, such as counting shapes, would be a new class implementing ShapeVisitor<number>. None of the shape classes would change.
The TypeScript alternative: union types and switch
In TypeScript, a discriminated union with an exhaustive switch gives many of the same benefits with less code. The compiler reports an error if a case is missing, which replaces the safety that the visitor interface provides:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rect"; width: number; height: number }
| { kind: "group"; children: Shape[] };
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.radius ** 2;
case "rect": return s.width * s.height;
case "group": return s.children.reduce((sum, c) => sum + area(c), 0);
default: {
const unreachable: never = s; // compile error if a kind is not handled
return unreachable;
}
}
}
console.log(area({ kind: "group", children: [{ kind: "rect", width: 2, height: 3 }] })); // 6Each new operation is a new function. This style fits plain data, such as JSON or syntax trees, and is what many TypeScript code bases use instead of class-based visitors.
When to use it and when not to
- Use it when you have a stable set of element classes and need many unrelated operations on them, such as evaluation, printing, type checking and optimization on a syntax tree.
- Use it to keep operation code together: all of the SVG export lives in one visitor, not spread over many classes.
- Use it when a visitor needs to accumulate state while walking a structure, such as a symbol table or a running total.
- Do not use it when element types change often; every change ripples through all visitors.
- Do not use it when the elements are few and the operations are simple. Methods on the classes, or a function with a
switch, are easier to read. - Be aware that visitors usually need access to element data, which can push you to make fields public.
Real-world uses
- Babel plugins are visitors: an object with methods named after node types, such as
IdentifierorCallExpression, which@babel/traversecalls while walking the syntax tree. - ESLint rules return an object of visitor methods keyed by node type, and ESLint calls them during traversal.
- Python's
ast.NodeVisitorandast.NodeTransformerclasses callvisit_methods named after the node class, such asvisit_Name. - Java's
java.nio.file.FileVisitor, used withFiles.walkFileTree, receives callbacks for each directory and file in a file tree.
Related patterns
| Pattern | How it relates to Visitor |
|---|---|
| Composite | Visitors are often applied to Composite structures; a group's visit method visits its children, as in the example. |
| Interpreter | An interpreter evaluates a syntax tree; a visitor can implement that evaluation, plus other operations on the same tree. |
| Iterator | An iterator walks the elements; a visitor defines the operation on each element. They can be combined. |
| Strategy | A strategy varies one algorithm for one context; a visitor bundles one operation for many element types. |
FAQ
What is double dispatch?
Double dispatch means the code that runs depends on the types of two objects. In Visitor, element.accept(visitor) selects the element type, and visitor.visitX(this) inside it selects the visitor type.
When should I not use the Visitor pattern?
Avoid it when new element types are added often, because each new type requires changes in every visitor. It also adds ceremony that is not worth it for a few simple operations.
Is Visitor useful in TypeScript?
It is useful for class-based object structures. For plain data, a discriminated union with an exhaustive switch usually achieves the same result with less code and full type checking.
Why do Babel and ESLint use visitors?
Both work on syntax trees with many node types and support many independent plugins or rules. Visitors let each plugin or rule handle only the node types it cares about, while the tool handles the traversal.