Visitor Pattern
Define a new operation on elements of an object structure without changing the classes of those elements.
Overview
Object hierarchies tend to grow in two independent directions: you add new element types, and you add new operations over those elements. If every new operation forces you to open every element class and add a method, the classes become cluttered with concerns that have nothing to do with their core purpose.
The Visitor pattern is a behavioral design pattern that separates an algorithm from the object structure it operates on. You define the algorithm once in a dedicated "visitor" object. Each element in the structure simply accepts the visitor and hands itself over for processing.
The cost is a rigid element hierarchy — adding a new element type requires updating every visitor. But when operations are the fast-moving dimension and element types are stable, Visitor is a remarkably clean solution.
Problem & Motivation
Imagine you are building a compiler that works with an Abstract Syntax Tree (AST). The tree is made up of nodes: NumberLiteral, BinaryExpression, Identifier, and so on. You need several passes over this tree:
- A pretty-printer that formats the code as a string
- A type-checker that validates operand compatibility
- A code generator that emits bytecode
The naive approach puts all three concerns inside the node classes: a print() method on every node, a typeCheck() method on every node, and a emit() method on every node. After three passes your BinaryExpression class is handling formatting, type logic, and code generation — responsibilities that belong in separate modules.
Visitor solves this by inverting the relationship. Instead of adding methods to the nodes, you write a visitor class for each operation. The nodes expose a single accept(visitor) method that delegates back to the visitor. This technique — calling a method on the visitor based on the concrete type of the element — is known as double dispatch.
Class Diagram
Loading diagram...
The key structural points are:
Visitordeclares onevisitmethod per concrete element type.- Each element class implements
accept(visitor), which calls the matchingvisitmethod on the visitor — this is the double dispatch mechanism. - New operations are added by writing a new
Visitorimplementation. Element classes are never touched.
Implementation
The implementations below model a small AST with two node types — NumberNode and BinaryNode — and two visitors: a PrintVisitor that formats the tree as a string, and an EvalVisitor that computes a numeric result. The expression (3 + 4) * 2 is built and processed by both visitors.
// --- Element interfaces and classes ---
interface ASTNode {
accept<T>(visitor: Visitor<T>): T;
}
interface Visitor<T> {
visitNumber(node: NumberNode): T;
visitBinary(node: BinaryNode): T;
}
class NumberNode implements ASTNode {
constructor(public readonly value: number) {}
accept<T>(visitor: Visitor<T>): T {
return visitor.visitNumber(this);
}
}
class BinaryNode implements ASTNode {
constructor(
public readonly op: string,
public readonly left: ASTNode,
public readonly right: ASTNode,
) {}
accept<T>(visitor: Visitor<T>): T {
return visitor.visitBinary(this);
}
}
// --- Concrete visitors ---
class PrintVisitor implements Visitor<string> {
visitNumber(node: NumberNode): string {
return String(node.value);
}
visitBinary(node: BinaryNode): string {
const left = node.left.accept(this);
const right = node.right.accept(this);
return `(${left} ${node.op} ${right})`;
}
}
class EvalVisitor implements Visitor<number> {
visitNumber(node: NumberNode): number {
return node.value;
}
visitBinary(node: BinaryNode): number {
const left = node.left.accept(this);
const right = node.right.accept(this);
switch (node.op) {
case "+": return left + right;
case "-": return left - right;
case "*": return left * right;
case "/": return left / right;
default: throw new Error(`Unknown operator: ${node.op}`);
}
}
}
// --- Usage: (3 + 4) * 2 ---
const tree = new BinaryNode(
"*",
new BinaryNode("+", new NumberNode(3), new NumberNode(4)),
new NumberNode(2),
);
const printer = new PrintVisitor();
const evaluator = new EvalVisitor();
console.log(printer.accept(tree)); // ((3 + 4) * 2)
// TS doesn't allow calling accept on the interface directly:
console.log(tree.accept(printer)); // ((3 + 4) * 2)
console.log(tree.accept(evaluator)); // 14Double Dispatch
Visitor achieves double dispatch by combining two single-dispatch calls. The first dispatch happens when tree.accept(visitor) selects the right accept implementation based on the element's concrete type. The second dispatch happens inside accept, which calls visitor.visitBinary(this) — selecting the right visitor method based on the visitor's concrete type. Most OO languages only dispatch on one type at a time; this two-step handshake simulates dispatching on two types simultaneously.
Real-World Examples
Compiler and interpreter passes — Production compilers (LLVM, Java's javac, TypeScript's own type-checker) use AST visitors extensively. Each compiler phase — name resolution, type inference, optimization, code emission — is a separate visitor. The AST node types are locked once the grammar is defined, so Visitor is a natural fit.
Document export pipelines — Rich text editors (Notion, ProseMirror, Slate.js) represent documents as trees of block and inline nodes. When you export to HTML, Markdown, or PDF, the exporter walks the tree with a visitor. Each target format is an isolated visitor implementation, with zero coupling to the document model itself.
Static analysis and linting tools — ESLint, Checkstyle, and similar tools register "rule" plugins that are visitors. The tool walks the AST once, calling each registered visitor at the appropriate node. Plugin authors write a visitor for the node types they care about; they never modify the parser or the AST.
Pros and Cons
Advantages
Open/Closed Principle for operations — adding a new operation means writing one new class, not modifying every element in the hierarchy.
Single Responsibility — each visitor encapsulates exactly one algorithm. The element classes stay focused on their own data and structure.
Accumulation across a traversal — a visitor can accumulate state as it walks the structure (e.g., summing values, collecting symbol names) without polluting the element classes with that state.
Type-safe dispatch — because each element calls the specific visitX method for its own type, the visitor always receives the most concrete type. No instanceof or type assertions are needed inside the visitor.
Disadvantages
Closed to new element types — adding a new node type requires updating every existing visitor. In a codebase with many visitors, this is a significant maintenance burden.
Breaks encapsulation — visitors often need access to the internal state of elements. Elements may have to expose fields or methods that would otherwise be private, weakening information hiding.
Traversal logic is scattered — by default, each visitor is responsible for recursing into children (as seen in visitBinary above). This duplicates traversal code across visitors unless you extract it into a shared walker.
Verbose in languages without generics — in languages without generic methods on interfaces, the visitor interface returns Object and callers must cast, reducing type safety.
When to Use / When to Avoid
Use Visitor when:
- The object structure (element types) is stable and unlikely to change, but you anticipate many new operations over it.
- You need to perform several unrelated operations on a structure and do not want to pollute the element classes with them.
- The algorithm for an operation needs to behave differently depending on the concrete type of each element — without
instanceofchains. - You are building a plugin or extension system where third parties contribute new operations but not new types.
Avoid Visitor when:
- You frequently add new element types. Every addition requires touching all visitors, which becomes painful quickly.
- The object hierarchy is shallow or has only one or two element types. A simple method or switch on a discriminated union is far less ceremony.
- Elements need to remain strictly encapsulated. Visitor tends to require elements to expose their internals.
- You are working in a language with first-class pattern matching (Rust's
match, Haskell's case expressions) — the language handles dispatch natively without the Visitor boilerplate.
Related Patterns
Composite — Visitor is almost always used together with Composite. The Composite pattern defines a tree structure of elements (exactly the kind of structure Visitor operates on). The visitBinary recursion in the examples above mirrors how Composite's leaf and composite nodes are distinguished.
Iterator — both Iterator and Visitor separate a traversal concern from the object structure. Iterator exposes a sequence of elements for the client to process; Visitor pushes the algorithm into a dedicated object that the element itself calls back. Use Iterator when you want the caller to control processing; use Visitor when the dispatch on element type is the key concern.
Strategy — Strategy also encapsulates an algorithm in a separate object, but it is injected into a single host object and replaces one method. Visitor operates across an entire hierarchy of different types. Think of Strategy as per-object behavior switching and Visitor as per-type operation dispatching across a structure.
Interpreter — Interpreter builds an AST and evaluates it. Visitor is the mechanism that makes multi-pass evaluation clean: each evaluation rule is a visitor rather than a method baked into each grammar node.
Design Tip
The classic Visitor forces you to choose between extensible types (add types freely, but operations are hard) and extensible operations (add operations freely, but types are hard). This tension is known as the Expression Problem. If you need both dimensions to be open, look at language-level solutions: discriminated unions with exhaustive pattern matching (TypeScript, Rust, Swift) let you add operations without modifying types and give you a compiler error when a new type is added but not all operations cover it.