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 — by separating what to do at each node from the nodes themselves.

The Problem

You're working with an AST (abstract syntax tree) for a small expression language. The node hierarchy looks like this:

Text
Expr
├── Literal      (a number)
├── BinaryOp     (left, op, right)
└── FunctionCall (name, args)

Now you need to:

  • Pretty-print the tree into source code.
  • Evaluate the tree to a number.
  • Type-check the tree.
  • Optimise constant subexpressions.
  • Compile the tree to a different target.

The naive way is to add a method per operation to every node:

PHP
abstract class Expr
{
    abstract public function prettyPrint(): string;
    abstract public function evaluate(Context $c): mixed;
    abstract public function typeCheck(TypeEnv $e): Type;
    abstract public function optimise(): Expr;
    abstract public function compile(Backend $b): IR;
}

Every new operation is a new method on every node class. Every operation touches every file. Code that knows about pretty-printing leaks into the node that should only know about being a BinaryOp. And in many real situations, the node hierarchy is framework code you can't edit — a third-party AST library, a generated parser output. You're stuck.

The shape of the smell: a stable hierarchy of types with many operations that should grow over time. You want to add operations without touching the nodes.

The Solution

Visitor flips the relationship. Each operation becomes a separate class — a visitor — with one method per node type (visitLiteral, visitBinaryOp, visitFunctionCall). Each node gets one method (accept(visitor)) that dispatches to the right visitor method. New operation = new visitor class. Nodes never change.

PHP
interface ExprVisitor
{
    public function visitLiteral(Literal $node):           mixed;
    public function visitBinaryOp(BinaryOp $node):         mixed;
    public function visitFunctionCall(FunctionCall $node): mixed;
}

abstract class Expr
{
    abstract public function accept(ExprVisitor $visitor): mixed;
}

final class Literal extends Expr
{
    public function __construct(public readonly float $value) {}
    public function accept(ExprVisitor $visitor): mixed { return $visitor->visitLiteral($this); }
}

final class BinaryOp extends Expr
{
    public function __construct(public Expr $left, public string $op, public Expr $right) {}
    public function accept(ExprVisitor $visitor): mixed { return $visitor->visitBinaryOp($this); }
}

Now adding "evaluate" is a new EvaluatingVisitor class. Adding "pretty-print" is a PrettyPrintingVisitor. The nodes don't move. The accept method on each node implements double dispatch — it knows its own type, the visitor knows what to do per type, and the right method ends up running.

Real-World Analogy

A health inspector visiting different kinds of restaurants. The inspector arrives, looks around, and adapts their inspection process to what they find. An Italian restaurant gets inspected for pasta-water temperature and cheese storage. A sushi restaurant gets inspected for raw-fish freshness and rice acidity. A taco truck gets inspected for refrigeration during transport.

The restaurants don't change to accommodate the inspector. The inspector knows how to inspect each kind, and arrives with that knowledge. New regulations next year? The inspector trains on them; the restaurants are unaffected. New kind of restaurant? That's when the inspection manual needs an update.

Structure

Visitor pattern: an Expr hierarchy with Literal, BinaryOp, and FunctionCall nodes; an ExprVisitor interface with visitX methods implemented by EvaluatingVisitor and PrettyPrintingVisitor; each node's accept() dispatches to the right visit method.
Visitor: nodes accept; visitors visit; double dispatch wires the right pair.

Five roles you'll see in every Visitor implementation:

  • Element — the abstract node interface. Declares accept(visitor). Here: Expr.
  • Concrete Element — each node type that calls the right visitor method in its accept. Here: Literal, BinaryOp, FunctionCall.
  • Visitor — the interface declaring one visitX method per concrete element type. Here: ExprVisitor.
  • Concrete Visitor — one class per operation. Implements all visitX methods. Here: EvaluatingVisitor, PrettyPrintingVisitor.
  • Object Structure — usually a Composite — the tree being walked. Iterates its elements and calls accept on each.

The defining trick is double dispatch: node.accept(visitor) first dispatches on the node's type (we end up in BinaryOp.accept), which then calls visitor.visitBinaryOp(this) — dispatching again on the visitor's type. Two dispatches; one per axis.

Code Examples

Here's a small expression-AST visitor in five languages. Watch how EvaluatingVisitor is one focused class — and how adding a PrettyPrintingVisitor doesn't touch a single node class.

interface ExprVisitor<T> {
  visitLiteral(node: Literal): T;
  visitBinaryOp(node: BinaryOp): T;
}

abstract class Expr {
  abstract accept<T>(visitor: ExprVisitor<T>): T;
}

class Literal extends Expr {
  constructor(public readonly value: number) { super(); }
  accept<T>(visitor: ExprVisitor<T>): T { return visitor.visitLiteral(this); }
}

class BinaryOp extends Expr {
  constructor(public left: Expr, public op: "+" | "-" | "*" | "/", public right: Expr) { super(); }
  accept<T>(visitor: ExprVisitor<T>): T { return visitor.visitBinaryOp(this); }
}

class EvaluatingVisitor implements ExprVisitor<number> {
  visitLiteral(n: Literal): number { return n.value; }
  visitBinaryOp(n: BinaryOp): number {
    const l = n.left.accept(this);
    const r = n.right.accept(this);
    switch (n.op) {
      case "+": return l + r;
      case "-": return l - r;
      case "*": return l * r;
      case "/": return l / r;
    }
  }
}

class PrettyPrintingVisitor implements ExprVisitor<string> {
  visitLiteral(n: Literal): string { return String(n.value); }
  visitBinaryOp(n: BinaryOp): string {
    return `(${n.left.accept(this)} ${n.op} ${n.right.accept(this)})`;
  }
}
from abc import ABC, abstractmethod

class ExprVisitor(ABC):
    @abstractmethod
    def visit_literal(self, node):    ...
    @abstractmethod
    def visit_binary_op(self, node): ...

class Expr(ABC):
    @abstractmethod
    def accept(self, visitor): ...

class Literal(Expr):
    def __init__(self, value):
        self.value = value
    def accept(self, visitor):
        return visitor.visit_literal(self)

class BinaryOp(Expr):
    def __init__(self, left, op, right):
        self.left, self.op, self.right = left, op, right
    def accept(self, visitor):
        return visitor.visit_binary_op(self)

class EvaluatingVisitor(ExprVisitor):
    def visit_literal(self, n):     return n.value
    def visit_binary_op(self, n):
        l = n.left.accept(self)
        r = n.right.accept(self)
        return {"+": l+r, "-": l-r, "*": l*r, "/": l/r}[n.op]

class PrettyPrintingVisitor(ExprVisitor):
    def visit_literal(self, n):     return str(n.value)
    def visit_binary_op(self, n):
        return f"({n.left.accept(self)} {n.op} {n.right.accept(self)})"
public interface ExprVisitor<T> {
    T visitLiteral(Literal node);
    T visitBinaryOp(BinaryOp node);
}

public abstract class Expr {
    public abstract <T> T accept(ExprVisitor<T> visitor);
}

public final class Literal extends Expr {
    public final double value;
    public Literal(double value) { this.value = value; }

    @Override
    public <T> T accept(ExprVisitor<T> visitor) {
        return visitor.visitLiteral(this);
    }
}

public final class BinaryOp extends Expr {
    public final Expr left, right;
    public final char op;

    public BinaryOp(Expr left, char op, Expr right) {
        this.left = left; this.op = op; this.right = right;
    }

    @Override
    public <T> T accept(ExprVisitor<T> visitor) {
        return visitor.visitBinaryOp(this);
    }
}

public final class EvaluatingVisitor implements ExprVisitor<Double> {
    @Override public Double visitLiteral(Literal n) { return n.value; }
    @Override public Double visitBinaryOp(BinaryOp n) {
        double l = n.left.accept(this);
        double r = n.right.accept(this);
        return switch (n.op) {
            case '+' -> l + r;
            case '-' -> l - r;
            case '*' -> l * r;
            case '/' -> l / r;
            default -> throw new IllegalStateException();
        };
    }
}
<?php

namespace App\Ast;

interface ExprVisitor
{
    public function visitLiteral(Literal $node):     mixed;
    public function visitBinaryOp(BinaryOp $node):   mixed;
}

abstract class Expr
{
    abstract public function accept(ExprVisitor $visitor): mixed;
}

final class Literal extends Expr
{
    public function __construct(public readonly float $value) {}

    public function accept(ExprVisitor $visitor): mixed
    {
        return $visitor->visitLiteral($this);
    }
}

final class BinaryOp extends Expr
{
    public function __construct(public Expr $left, public string $op, public Expr $right) {}

    public function accept(ExprVisitor $visitor): mixed
    {
        return $visitor->visitBinaryOp($this);
    }
}

final class EvaluatingVisitor implements ExprVisitor
{
    public function visitLiteral(Literal $n): float { return $n->value; }

    public function visitBinaryOp(BinaryOp $n): float
    {
        $l = $n->left->accept($this);
        $r = $n->right->accept($this);
        return match ($n->op) {
            '+' => $l + $r,
            '-' => $l - $r,
            '*' => $l * $r,
            '/' => $l / $r,
        };
    }
}
package ast

// Go doesn't support visitor through inheritance, but it does fine through
// a sum-style interface and a switch — which is what the language nudges
// you toward in idiomatic code. The pattern shape is recognisable; the
// ceremony is lighter.

type Expr interface {
    Accept(visitor ExprVisitor) any
}

type ExprVisitor interface {
    VisitLiteral(*Literal)   any
    VisitBinaryOp(*BinaryOp) any
}

type Literal struct {
    Value float64
}

func (l *Literal) Accept(v ExprVisitor) any { return v.VisitLiteral(l) }

type BinaryOp struct {
    Left, Right Expr
    Op          rune
}

func (b *BinaryOp) Accept(v ExprVisitor) any { return v.VisitBinaryOp(b) }

type EvaluatingVisitor struct{}

func (EvaluatingVisitor) VisitLiteral(n *Literal) any { return n.Value }

func (e EvaluatingVisitor) VisitBinaryOp(n *BinaryOp) any {
    l := n.Left.Accept(e).(float64)
    r := n.Right.Accept(e).(float64)
    switch n.Op {
    case '+': return l + r
    case '-': return l - r
    case '*': return l * r
    case '/': return l / r
    }
    panic("unknown op")
}

The pattern looks heavy at first — accept everywhere, two interfaces — but watch what it gives you: adding a new operation (say, OptimizingVisitor) is one new file. The node hierarchy doesn't change. The other visitors don't change. That's the win.

When to Use It

Reach for Visitor when you can answer "yes" to all of these:

  • You have a stable hierarchy of node types. ASTs, syntax trees, document models, expression evaluators, scene graphs — anywhere the kinds of nodes are fixed but the operations on them will grow.
  • Many operations need to walk the structure. Pretty-print, type-check, evaluate, optimise, compile, render, serialise.
  • The operations are unrelated to the nodes' core concerns. A Pretty-Printer doesn't belong inside the AST node classes; it belongs alongside them.
  • You can't or shouldn't modify the node classes. Library code, generated code, framework code, code shared with other teams.

If you have a stable set of operations but the node types keep growing, Visitor is the wrong fit — adding a node type forces you to update every Visitor. In that case, regular polymorphism (methods on the nodes) is cleaner.

Pros and Cons

Pros

  • New operations don't touch any node class — they're new files only.
  • Related operations stay grouped in one Visitor class — easier to read than scattering them across nodes.
  • Visitors can carry their own state (a counter, a string buffer) that nodes don't need to know about.
  • Compile-time enforcement: adding a new node type forces every existing Visitor to be updated (which is sometimes a good thing — you can't accidentally forget a case).

Cons

  • Adding a new node type is painful. Every Visitor needs a new visitX method. The expression problem flips: cheap to add operations, expensive to add types.
  • Visitor methods need access to node state. That usually means making node fields public (or using accessors). The encapsulation that nodes had to themselves leaks to all visitors.
  • Hard to learn at first. "Why does the node call back into the visitor instead of the other way around?" needs a careful explanation. Double dispatch is unintuitive until it clicks.
  • Less natural in dynamic languages that don't enforce method resolution at compile time. Some people prefer simple type-dispatch (if isinstance(n, Literal): ...) — less rigorous but easier to read.
  • Cyclic compile dependency. Nodes know about the Visitor interface; the Visitor interface mentions every node type. Splitting them across modules requires care.

Pro Tips

  • Only reach for Visitor when the node hierarchy is truly stable. If you'll add a fourth node type next month, every existing visitor needs editing — and you're paying Visitor's cost without its benefit.
  • Make the visit methods generic in the return type. ExprVisitor<T> lets a PrettyPrintingVisitor return string, an EvaluatingVisitor return number, and a TypeCheckingVisitor return Type — without casts.
  • Don't put a default visit in the visitor base class. If you add a node type, you want the compiler/type-checker to scream at every visitor that hasn't handled it. A default that does nothing silently masks bugs.
  • Combine with Iterator for traversal control. Visitor handles what to do at each node; an iterator handles which order to visit them. Pre-order vs post-order is the iterator's choice; per-node logic is the visitor's.
  • In dynamic languages, consider plain type-dispatch first. match expr: case Literal(v): ... (Python's structural pattern matching) or switch (true) { case node instanceof Literal: ... } (TypeScript) is often clearer than the full pattern. Visitor's structural rigour is worth it when types are many or stability matters.

Relations with Other Patterns

  • Composite is the natural object structure to visit. Visitor pairs with Composite the way Iterator pairs with collections.
  • Iterator is the alternative when operations are uniform across all nodes (just process every leaf). Visitor is the choice when operations differ per node type.
  • Command is conceptually adjacent — each visitX method is a command-shape (encapsulated action) — but Visitor's defining trick (double dispatch) is what distinguishes it.
  • Interpreter typically uses Visitor for walking and evaluating the grammar tree; the interpreter's grammar nodes form a Composite, the evaluation logic is a Visitor over them.

Final Tips

The Visitor I've shipped most often is for static-analysis tooling — walking parsed source code looking for patterns. Each lint rule was a Visitor; the AST was generated code we couldn't change. Adding a new lint rule meant adding a new visitor file; the AST node classes never opened.

Reach for Visitor when the operations grow but the structure doesn't. Skip it when the opposite is true. The pattern is one of the harder GoF members — but the moment you have a stable AST and a growing list of analyses to run on it, you'll either invent it or be glad you knew it.