Visitor
Separates an algorithm from the objects it operates on, allowing new operations to be added to a class hierarchy without modifying it — via double dispatch, each element "accepts" the visitor and invokes the correct method for its own type.
Intent
Represent an operation to be performed on the elements of
an object hierarchy. Visitor lets you define a new
operation without changing the classes of the elements it operates
on. Each concrete class in the hierarchy implements an
accept(visitor) method that calls the visitor's method
specific to that type — the so-called double dispatch.
Cataloged by the GoF (1994) as a behavioral pattern, Visitor is the right pattern when an object hierarchy is stable but you need to add new operations frequently. It's widely used in compilers (an AST with type-checking visitors, code generation, and optimization), static analysis tools, tree serialization, and structured document processing.
Problem
Imagine an abstract syntax tree (AST) with three node types:
NumberNode, Sum, and Multiplication.
You need to add two independent operations: printing the expression
as a string and computing its numeric result. The direct approach
puts the operations inside the classes:
// Naive approach — DON'T do this:
class Sum {
constructor(readonly left: IExpression, readonly right: IExpression) {}
print(): string {
// Printing logic coupled to the AST node.
return `(${this.left.print()} + ${this.right.print()})`;
}
evaluate(): number {
// Evaluation logic also coupled.
return this.left.evaluate() + this.right.evaluate();
}
// Adding a third operation (e.g.: serialize, optimize, compile)
// requires opening and modifying EVERY class in the hierarchy.
}
Every new operation requires opening and modifying every class in
the hierarchy — NumberNode, Sum,
Multiplication — all at once. This violates the
Open/Closed Principle: domain classes should be closed for
modification. In real compilers, the AST can have dozens of node
types and dozens of passes (optimizations, type checks, bytecode
generation) — mixing everything into the node classes makes the
code unintelligible.
Visitor solves this by extracting each operation into its own class (the ConcreteVisitor). The hierarchy's classes stay stable; new behaviors are added without touching them.
Solution
Visitor organizes the code into four participants:
-
Element (interface): every class in the
hierarchy declares
accept(visitor: IVisitor): void. The implementation is always the same:visitor.visitConcreteType(this). This is the double dispatch — the correct visitor method is chosen at runtime based on the element's concrete type. -
ConcreteElement: the concrete classes of the
hierarchy (
NumberNode,Sum,Multiplication). Each one implementsaccept()by calling the correspondingvisitmethod on the visitor. They expose their internal data (via getters orreadonlyfields) so visitors can access it. -
Visitor (interface): declares a
visitmethod for each ConcreteElement in the hierarchy. E.g.:visitNumber(node: NumberNode): void,visitSum(node: Sum): void. When a new node type is added to the hierarchy, every visitor needs to be updated — the inverse cost compared to the naive approach. - ConcreteVisitor: implements the specific operation for each element type. Each visitor is a cohesive class with a single responsibility: printing, evaluating, serializing, optimizing. It can accumulate internal state during the visit (e.g.: a stack for evaluation, a list of parts for printing).
The central mechanism is double dispatch: the language
resolves the correct method in two steps — first it dispatches to
the correct element's accept() (chosen via the
hierarchy's polymorphism); inside accept(), the
element calls visitor.visitType(this), resolving the
visitor's method by the concrete type of this.
Without this second dispatch, the visitor would need
instanceof/type-switch to detect the type — breaking polymorphism.
Structure
«interface»
IExpression
┌──────────────────────────────────┐
│ + accept(v: IVisitor): void │
└──────────────────────────────────┘
▲
┌───────────┼──────────────────┐
│ │ │
NumberNode Sum Multiplication
(Terminal) (NonTerminal) (NonTerminal)
│ │ │
│accept: │accept: │accept:
│v.visitNum.. │v.visitSum(this) │v.visitMult..(this)
▼ ▼ ▼
«interface»
IVisitor
┌──────────────────────────────────────────────┐
│ + visitNumber(node: NumberNode): void │
│ + visitSum(node: Sum): void │
│ + visitMultiplication(node: Multiplication): │
│ void │
└──────────────────────────────────────────────┘
▲
┌───────────┴──────────────────┐
│ │
PrintVisitor EvaluateVisitor
│ parts: string[] stack: number[]
│ visitNumber → push visitNumber → push
│ visitSum → parentheses visitSum → pop+pop+sum
│ result(): string result(): number
└──────────────────────────────┘
Double dispatch — step by step:
tree.accept(evaluator)
│
├─ tree is Multiplication → Multiplication.accept(evaluator)
│ └─ evaluator.visitMultiplication(this)
│ ├─ this.left.accept(evaluator) // Sum
│ │ └─ evaluator.visitSum(this)
│ │ ├─ this.left.accept(evaluator) // NumberNode(2)
│ │ │ └─ evaluator.visitNumber(this) → stack:[2]
│ │ └─ this.right.accept(evaluator) // NumberNode(3)
│ │ └─ evaluator.visitNumber(this) → stack:[2,3]
│ │ → pop 3, pop 2, push 5 stack:[5]
│ └─ this.right.accept(evaluator) // NumberNode(4)
│ └─ evaluator.visitNumber(this) → stack:[5,4]
│ → pop 4, pop 5, push 20 stack:[20]
result(): 20
Code examples
Example 1 — Expression AST with PrintVisitor and EvaluateVisitor
A complete implementation with the expression hierarchy
(NumberNode, Sum,
Multiplication), the IVisitor interface,
and two concrete visitors. Note how every class in the hierarchy
implements accept() with exactly one line — all the
operation's logic lives in the visitor, without touching the AST
nodes.
// ── Visitor interface ─────────────────────────────────────────
// Declared BEFORE the elements because they reference IVisitor.
// In TS, forward references work fine via interfaces — no ordering issue.
interface IVisitor {
visitNumber(node: NumberNode): void;
visitSum(node: Sum): void;
visitMultiplication(node: Multiplication): void;
}
// ── Element interface ─────────────────────────────────────────
interface IExpression {
accept(visitor: IVisitor): void;
}
// ── ConcreteElements (AST nodes) ──────────────────────────────
// Named NumberNode (instead of just "Number") to avoid colliding with the built-in Number.
class NumberNode implements IExpression {
constructor(readonly value: number) {}
// Double dispatch: calls the visitor's correct method for this type.
accept(visitor: IVisitor): void {
visitor.visitNumber(this);
}
}
class Sum implements IExpression {
constructor(
readonly left: IExpression,
readonly right: IExpression
) {}
accept(visitor: IVisitor): void {
visitor.visitSum(this);
}
}
class Multiplication implements IExpression {
constructor(
readonly left: IExpression,
readonly right: IExpression
) {}
accept(visitor: IVisitor): void {
visitor.visitMultiplication(this);
}
}
// ── ConcreteVisitor 1: Print ──────────────────────────────────
// Builds the textual representation of the expression with explicit parentheses.
class PrintVisitor implements IVisitor {
private parts: string[] = [];
visitNumber(node: NumberNode): void {
this.parts.push(String(node.value));
}
visitSum(node: Sum): void {
this.parts.push('(');
node.left.accept(this); // visits the left child
this.parts.push(' + ');
node.right.accept(this); // visits the right child
this.parts.push(')');
}
visitMultiplication(node: Multiplication): void {
this.parts.push('(');
node.left.accept(this);
this.parts.push(' * ');
node.right.accept(this);
this.parts.push(')');
}
result(): string {
return this.parts.join('');
}
}
// ── ConcreteVisitor 2: Evaluate ───────────────────────────────
// Evaluates the expression using a stack — with no changes to the nodes.
class EvaluateVisitor implements IVisitor {
private stack: number[] = [];
visitNumber(node: NumberNode): void {
this.stack.push(node.value);
}
visitSum(node: Sum): void {
node.left.accept(this);
node.right.accept(this);
const b = this.stack.pop()!;
const a = this.stack.pop()!;
this.stack.push(a + b);
}
visitMultiplication(node: Multiplication): void {
node.left.accept(this);
node.right.accept(this);
const b = this.stack.pop()!;
const a = this.stack.pop()!;
this.stack.push(a * b);
}
result(): number {
return this.stack[0] ?? 0;
}
}
// ── Usage ────────────────────────────────────────────────────
// AST representing: (2 + 3) * 4
const tree: IExpression = new Multiplication(
new Sum(new NumberNode(2), new NumberNode(3)),
new NumberNode(4)
);
// Operation 1: print (without touching the AST nodes)
const printer = new PrintVisitor();
tree.accept(printer);
console.log(printer.result()); // ((2 + 3) * 4)
// Operation 2: evaluate (without touching the AST nodes)
const evaluator = new EvaluateVisitor();
tree.accept(evaluator);
console.log(evaluator.result()); // 20
// The AST can be visited multiple times by different visitors.
// No node class was modified to support these operations.
<?php
// ── Interfaces (forward declarations aren't needed in PHP) ────
interface IVisitor
{
public function visitNumber(NumberNode $node): void;
public function visitSum(Sum $node): void;
public function visitMultiplication(Multiplication $node): void;
}
interface IExpression
{
public function accept(IVisitor $visitor): void;
}
// ── ConcreteElements ──────────────────────────────────────────
// Named NumberNode for consistency with the TypeScript example.
class NumberNode implements IExpression
{
public function __construct(public readonly float $value) {}
public function accept(IVisitor $visitor): void
{
$visitor->visitNumber($this);
}
}
class Sum implements IExpression
{
public function __construct(
public readonly IExpression $left,
public readonly IExpression $right
) {}
public function accept(IVisitor $visitor): void
{
$visitor->visitSum($this);
}
}
class Multiplication implements IExpression
{
public function __construct(
public readonly IExpression $left,
public readonly IExpression $right
) {}
public function accept(IVisitor $visitor): void
{
$visitor->visitMultiplication($this);
}
}
// ── ConcreteVisitor 1: Print ──────────────────────────────────
class PrintVisitor implements IVisitor
{
/** @var string[] */
private array $parts = [];
public function visitNumber(NumberNode $node): void
{
$this->parts[] = (string) $node->value;
}
public function visitSum(Sum $node): void
{
$this->parts[] = '(';
$node->left->accept($this);
$this->parts[] = ' + ';
$node->right->accept($this);
$this->parts[] = ')';
}
public function visitMultiplication(Multiplication $node): void
{
$this->parts[] = '(';
$node->left->accept($this);
$this->parts[] = ' * ';
$node->right->accept($this);
$this->parts[] = ')';
}
public function result(): string
{
return implode('', $this->parts);
}
}
// ── ConcreteVisitor 2: Evaluate ───────────────────────────────
class EvaluateVisitor implements IVisitor
{
/** @var float[] */
private array $stack = [];
public function visitNumber(NumberNode $node): void
{
$this->stack[] = $node->value;
}
public function visitSum(Sum $node): void
{
$node->left->accept($this);
$node->right->accept($this);
$b = array_pop($this->stack);
$a = array_pop($this->stack);
$this->stack[] = $a + $b;
}
public function visitMultiplication(Multiplication $node): void
{
$node->left->accept($this);
$node->right->accept($this);
$b = array_pop($this->stack);
$a = array_pop($this->stack);
$this->stack[] = $a * $b;
}
public function result(): float
{
return $this->stack[0] ?? 0.0;
}
}
// ── Usage ────────────────────────────────────────────────────
// AST: (2 + 3) * 4
$tree = new Multiplication(
new Sum(new NumberNode(2), new NumberNode(3)),
new NumberNode(4)
);
$printer = new PrintVisitor();
$tree->accept($printer);
echo $printer->result() . "\n"; // ((2 + 3) * 4)
$evaluator = new EvaluateVisitor();
$tree->accept($evaluator);
echo $evaluator->result() . "\n"; // 20
Example 2 — Adding new operations without touching the hierarchy
Visitor's main benefit: adding operations is open for extension.
Reusing the same AST from Example 1, we implement two additional
visitors — CountNodesVisitor (counts how many nodes
the AST has) and SerializeJsonVisitor (serializes the
tree to JSON) — without changing a single line of
NumberNode, Sum, or
Multiplication.
// Reuses IVisitor, IExpression, NumberNode, Sum and Multiplication from Example 1.
// ── ConcreteVisitor 3: Count nodes ───────────────────────────
// Counts the total number of AST nodes — leaves (NumberNode) and internal (Sum, Multiplication).
class CountNodesVisitor implements IVisitor {
private count = 0;
visitNumber(_node: NumberNode): void {
this.count++;
}
visitSum(node: Sum): void {
this.count++;
node.left.accept(this);
node.right.accept(this);
}
visitMultiplication(node: Multiplication): void {
this.count++;
node.left.accept(this);
node.right.accept(this);
}
result(): number {
return this.count;
}
}
// ── ConcreteVisitor 4: Serialize to JSON ──────────────────────
// Serializes the AST to a JSON string without using JSON.stringify at the root.
// Uses recursion's implicit stack: each visit builds and stores
// the sub-node's JSON fragment before composing the parent node.
class SerializeJsonVisitor implements IVisitor {
private json = '';
visitNumber(node: NumberNode): void {
this.json = `{"type":"Number","value":${node.value}}`;
}
visitSum(node: Sum): void {
node.left.accept(this);
const left = this.json;
node.right.accept(this);
const right = this.json;
this.json = `{"type":"Sum","left":${left},"right":${right}}`;
}
visitMultiplication(node: Multiplication): void {
node.left.accept(this);
const left = this.json;
node.right.accept(this);
const right = this.json;
this.json = `{"type":"Mult","left":${left},"right":${right}}`;
}
result(): string {
return this.json;
}
}
// ── Usage ────────────────────────────────────────────────────
// Same AST: (2 + 3) * 4
const tree: IExpression = new Multiplication(
new Sum(new NumberNode(2), new NumberNode(3)),
new NumberNode(4)
);
// Visitor 3: count
const counter = new CountNodesVisitor();
tree.accept(counter);
console.log(counter.result());
// 5 (Multiplication + Sum + NumberNode(2) + NumberNode(3) + NumberNode(4))
// Visitor 4: serialize
const serializer = new SerializeJsonVisitor();
tree.accept(serializer);
console.log(serializer.result());
// {"type":"Mult",
// "left":{"type":"Sum",
// "left":{"type":"Number","value":2},
// "right":{"type":"Number","value":3}},
// "right":{"type":"Number","value":4}}
// Zero lines changed in NumberNode, Sum and Multiplication.
<?php
// Reuses IVisitor, IExpression, NumberNode, Sum and Multiplication from Example 1.
// ── ConcreteVisitor 3: Count nodes ───────────────────────────
class CountNodesVisitor implements IVisitor
{
private int $count = 0;
public function visitNumber(NumberNode $node): void
{
$this->count++;
}
public function visitSum(Sum $node): void
{
$this->count++;
$node->left->accept($this);
$node->right->accept($this);
}
public function visitMultiplication(Multiplication $node): void
{
$this->count++;
$node->left->accept($this);
$node->right->accept($this);
}
public function result(): int
{
return $this->count;
}
}
// ── ConcreteVisitor 4: Serialize to JSON ──────────────────────
class SerializeJsonVisitor implements IVisitor
{
private string $json = '';
public function visitNumber(NumberNode $node): void
{
$this->json = sprintf('{"type":"Number","value":%s}', $node->value);
}
public function visitSum(Sum $node): void
{
$node->left->accept($this);
$left = $this->json;
$node->right->accept($this);
$right = $this->json;
$this->json = sprintf('{"type":"Sum","left":%s,"right":%s}', $left, $right);
}
public function visitMultiplication(Multiplication $node): void
{
$node->left->accept($this);
$left = $this->json;
$node->right->accept($this);
$right = $this->json;
$this->json = sprintf('{"type":"Mult","left":%s,"right":%s}', $left, $right);
}
public function result(): string
{
return $this->json;
}
}
// ── Usage ────────────────────────────────────────────────────
$tree = new Multiplication(
new Sum(new NumberNode(2), new NumberNode(3)),
new NumberNode(4)
);
// Visitor 3: count
$counter = new CountNodesVisitor();
$tree->accept($counter);
echo $counter->result() . "\n";
// 5 (Multiplication + Sum + NumberNode(2) + NumberNode(3) + NumberNode(4))
// Visitor 4: serialize
$serializer = new SerializeJsonVisitor();
$tree->accept($serializer);
echo $serializer->result() . "\n";
// {"type":"Mult",
// "left":{"type":"Sum",
// "left":{"type":"Number","value":2},
// "right":{"type":"Number","value":3}},
// "right":{"type":"Number","value":4}}
When to use
- When you need to add frequent operations to a stable hierarchy: if the hierarchy (the node types) rarely changes but new operations (visitors) are added often, Visitor keeps the operations' code cohesive and the domain classes untouched.
- In compilers and analysis tools: the AST has stable node types (expressions, statements, literals), but the passes (type-checking, inlining, code generation, flow analysis) are added incrementally. Each pass is a Visitor.
- For operations that access data from multiple unrelated types: a serialization or auditing visitor can access internal fields of completely different types and produce a unified result — without those types needing to share behavior.
- To separate concerns in complex hierarchies: when mixing multiple operations into the classes makes the code hard to understand, Visitor concentrates each operation into a single cohesive class.
When to avoid
-
When the hierarchy changes frequently: adding a
new element type (e.g.: a new AST node) requires updating the
IVisitorinterface and every existing visitor. If the hierarchy is volatile, Visitor generates more work than it saves — the Open/Closed axis is inverted for elements. -
When the classes need to protect their internals:
Visitor needs to access the elements' internal data
(
readonlyfields or getters). If encapsulation is critical and the data shouldn't be exposed, the pattern is unsuitable — it essentially forces every element to be a DTO for the visitors. -
For small hierarchies and few operations: with
two element types and one operation, creating a visitor
interface, two
accept()methods, and a concrete class is overengineering. A direct method on the class solves it with less code.
Pros and cons
Pros
- Open for new operations: adding a visitor doesn't touch the hierarchy's classes.
- Concentrates each operation's logic in a single cohesive class — easy to locate, test, and maintain.
- Allows accumulating state during the visit (counting, a stack, an output buffer) without polluting the elements.
- Can traverse composite hierarchies (Composite) in an ordered, recursive way, applying complex transformations.
- Multiple visitation: the same structure can be visited by different visitors independently.
Cons
- Closed for new elements: adding a type to the hierarchy requires updating every visitor — the inverse cost compared to the naive approach.
- Partially breaks encapsulation: elements need to expose their internal data so visitors can access it.
- Double dispatch is non-intuitive to those unfamiliar with the pattern — the execution flow is less obvious than a direct call.
- Increases the number of classes: each operation becomes a class; large hierarchies with many operations generate many visitors.
Common pitfalls
1. Obscured double dispatch — using instanceof instead of accept()
The most common temptation when learning Visitor: implementing the
visitor with an if/instanceof block instead of using
accept(). This defeats the pattern's purpose and
creates explicit coupling to the type:
// WRONG — visitor with instanceof (not a real Visitor):
class WrongEvaluator {
evaluate(node: IExpression): number {
if (node instanceof NumberNode) return node.value;
if (node instanceof Sum) return this.evaluate(node.left) + this.evaluate(node.right);
if (node instanceof Multiplication) return this.evaluate(node.left) * this.evaluate(node.right);
throw new Error('Unknown type');
// Problem: adding a new node type doesn't cause a compile error —
// the code simply throws at runtime, with no static warning.
}
}
// CORRECT — the compiler guarantees every type is covered:
// If Division is added to IVisitor, every ConcreteVisitor that
// doesn't implement visitDivision() will fail to compile.
With real double dispatch, the compiler forces every visitor to
implement the method for each new type — the IVisitor
interface's contract acts as a static coverage checklist.
2. Unstable hierarchy — Open/Closed inverted for elements
Visitor flips the Open/Closed axis: it's open for new operations (visitors) but closed for new types in the hierarchy. When a project is still in the modeling phase and the element hierarchy is still changing, every new type requires updating every existing visitor — the opposite of what the pattern promises. Apply Visitor only when the element hierarchy is reasonably stable.
3. A visitor with mutable state shared across visits
When a visitor accumulates internal state (stack, parts, count), it can only be used once per visit. Reusing the same instance on two different trees produces incorrect results because the first visit's state contaminates the second.
Rule of thumb: create a new visitor instance for
each visit. If a visitor needs to be reusable, implement a
reset() method that zeroes the internal state — and
document that requirement explicitly.
4. Visitor vs Strategy — the intent distinction
The most frequent confusion between these two patterns.
Strategy encapsulates an interchangeable
algorithm per object: the context has one active strategy
that can be swapped, but each object has its own. Visitor
encapsulates an operation that traverses an entire
hierarchy: a single visitor walks every element, calling the
correct method on each type — the algorithm is distributed across
the visit* methods, not across isolated objects. Use
Strategy when a single object needs interchangeable behavior; use
Visitor when an operation needs to traverse and process multiple
types in a hierarchy.
Related patterns
Visitor works alongside patterns that organize hierarchies and traversals:
Composite is Visitor's natural partner: Composite
organizes objects into tree structures; Visitor traverses that
tree applying an operation to each node. The accept()
method on a composite node typically delegates to the children,
which in turn delegate to their own children — Composite's
recursion and Visitor's double dispatch combine naturally.
Iterator is a simpler alternative when the
operation is just traversing and handing elements to the caller,
without processing per type; when each element type needs to be
processed differently, Visitor is more appropriate.
Command encapsulates a discrete action on a
specific receiver; Visitor encapsulates an operation that's
distributed across an entire hierarchy — they're complementary
patterns when a system has both discrete actions (Command) and
analysis or transformation passes over data structures (Visitor).