The Visitor pattern represents an operation to be performed on elements of an object structure. It lets you define a new operation without changing the classes of the elements on which it operates. The key mechanism is double dispatch: the element calls back to the visitor with its type.

Intent and Motivation

Intent: Represent an operation to be performed on elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.

Motivation: A compiler’s AST has NumberNode, AddNode, VariableNode classes. You need operations: evaluate(), print(), typeCheck(), optimize(). Adding each operation as a method on every node class leads to bloated node classes and violates Single Responsibility. Visitor extracts each operation into a separate visitor class (EvaluateVisitor, PrintVisitor). Each node implements accept(Visitor v) which calls v.visitThisNode(this) — double dispatch ensures the correct visitor method is invoked. Adding a new operation means adding a new visitor class — no node classes change.

The trade-off: adding new node types requires updating all visitors.

Structure (UML-like)

  ┌──────────────┐  accept()    ┌──────────────┐
│   Element    │ ───────────► │   Visitor    │ (interface)
├──────────────┤              ├──────────────┤
│ + accept(v)  │              │ + visitElemA │
└──────▲───────┘              │ + visitElemB │
       │                       └──────▲───────┘
       │ implements                    │ implements
┌──────┴───────┐               ┌───────┴───────┐
│ ConcreteElem │               │ ConcreteVisitor│
│   A, B, C    │               │ (Export, Eval) │
└──────────────┘               └───────────────┘

Double dispatch flow:
  element.accept(visitor)
    → visitor.visitConcreteElement(element)
  

Participants:

  • Visitor — declares a visit method for each concrete element type.
  • ConcreteVisitor — implements operations for each element type.
  • Element — declares accept(Visitor) method.
  • ConcreteElement — implements accept() by calling the matching visit method on the visitor.
  • ObjectStructure — collection of elements that visitors traverse.

Java Example

  // Visitor
interface ShapeVisitor {
    void visitCircle(Circle c);
    void visitRectangle(Rectangle r);
}

// Elements
interface Shape {
    void accept(ShapeVisitor visitor);
}

class Circle implements Shape {
    double radius;
    Circle(double radius) { this.radius = radius; }
    public void accept(ShapeVisitor v) { v.visitCircle(this); }
}

class Rectangle implements Shape {
    double width, height;
    Rectangle(double w, double h) { this.width = w; this.height = h; }
    public void accept(ShapeVisitor v) { v.visitRectangle(this); }
}

// Concrete Visitor — area calculation
class AreaVisitor implements ShapeVisitor {
    private double totalArea = 0;

    public void visitCircle(Circle c) {
        double area = Math.PI * c.radius * c.radius;
        totalArea += area;
        System.out.println("Circle area: " + area);
    }

    public void visitRectangle(Rectangle r) {
        double area = r.width * r.height;
        totalArea += area;
        System.out.println("Rectangle area: " + area);
    }

    double getTotalArea() { return totalArea; }
}

// Usage
List<Shape> shapes = List.of(new Circle(5), new Rectangle(4, 6));
AreaVisitor visitor = new AreaVisitor();
for (Shape shape : shapes) {
    shape.accept(visitor);
}
System.out.println("Total: " + visitor.getTotalArea());
  

JavaScript Example

  // Elements
const nodes = {
  number: (value) => ({ type: 'number', value, accept(v) { return v.visitNumber(this); } }),
  add: (left, right) => ({ type: 'add', left, right, accept(v) { return v.visitAdd(this); } }),
  variable: (name) => ({ type: 'variable', name, accept(v) { return v.visitVariable(this); } })
};

// Visitors
const evalVisitor = {
  visitNumber(node) { return node.value; },
  visitVariable(node, ctx) { return ctx[node.name] || 0; },
  visitAdd(node, ctx) {
    return node.left.accept(this, ctx) + node.right.accept(this, ctx);
  }
};

const printVisitor = {
  visitNumber(node) { return String(node.value); },
  visitVariable(node) { return node.name; },
  visitAdd(node) {
    return `(${node.left.accept(this)} + ${node.right.accept(this)})`;
  }
};

const expr = nodes.add(nodes.variable('x'), nodes.number(3));
const ctx = { x: 7 };
console.log(expr.accept(evalVisitor, ctx));  // 10
console.log(expr.accept(printVisitor));       // (x + 3)
  

Real-World Use Cases

Framework / System Usage
Java Files.walkFileTree() FileVisitor operates on files/dirs without modifying the file system API.
Compiler AST passes LLVM, javac — optimization, type checking, and code gen as separate visitor passes.
DOM traversal TreeWalker and NodeIterator visit DOM nodes for rendering, validation, or transformation.
JUnit test discovery Test engine visits class structures to find and run test methods.
Document export Export visitors convert internal document model to PDF, HTML, or Markdown.
Linting tools (ESLint) AST visitors analyze code patterns without modifying the parser’s node classes.

Pros and Cons

Pros Cons
Add new operations easily — just create a new visitor class Adding new element types requires updating ALL visitors
Groups related operations in one visitor class Visitors need access to element internals (breaks encapsulation)
Can accumulate state across elements during traversal Circular element dependencies complicate visitor design
Separates algorithms from object structure Not all languages support clean double dispatch
Open/Closed for operations (not for element types) Visitor interface grows with each new element type

When to Use vs When NOT to Use

Use when:

  • An object structure contains many classes with different interfaces and you need operations across all of them.
  • You need to perform many distinct operations on the structure and do not want to pollute element classes.
  • The element class hierarchy is stable but operations change frequently.
  • You are building compiler passes, export pipelines, or analysis tools over a fixed AST.

Do NOT use when:

  • The element hierarchy changes frequently (every new element breaks all visitors).
  • Element classes are simple and few — just add methods directly.
  • Operations need access to element private state that should remain encapsulated.
  • The structure is not a stable hierarchy (flat collections — use Iterator + functions).

Common Mistakes

  1. Adding operations via Visitor when element types change often — the pattern optimizes for stable structures.
  2. Visitor accessing private fields — breaks encapsulation; elements should provide accessors or pass data to visit methods.
  3. Forgetting to update all visitors when adding element types — compile-time checks help (interface requires all visit methods).
  4. Not using double dispatch — calling visitor.visit(element) directly loses type-specific dispatch.
  5. God visitor — one visitor doing unrelated operations; create separate visitors per concern.
  • Composite — visitors commonly traverse composite tree structures.
  • Iterator — iterator walks the structure; visitor performs operations at each stop.
  • Interpreter — interpreter evaluates expression trees; visitor adds operations to the same trees.
  • Strategy — visitor is a strategy for operating on elements, but uses double dispatch instead of single delegation.
  • Command — can encapsulate visitor operations as commands for undo or queuing.