Design PatternsBehavioral

Visitor

Separate an algorithm from the object structure it operates on, so new operations can be added without modifying the structure.

4 min readDesigns · LLD & Patterns

Intent

Add new operations over a class hierarchy without modifying the classes themselves. The trick: each element accepts a visitor and dispatches based on its concrete type (double dispatch).

Use when:

  • The object hierarchy is stable but you keep adding operations.
  • Operations span the whole hierarchy (e.g., serialize, print, validate, compute size).
  • Putting these operations in the classes themselves would bloat them with unrelated concerns.

The Problem It Solves

You have an expression tree (Number, Add, Mul). You want to:

  • Evaluate
  • Pretty-print
  • Compute depth
  • Compile to bytecode
  • Type-check

Adding all five methods to every class clutters them. Visitor pulls each algorithm into its own class.


Structure

StructureClass diagram

Example: Expression Tree

Element hierarchy

public interface Expr {
    <R> R accept(ExprVisitor<R> visitor);
}

public class Num implements Expr {
    public final int value;
    public Num(int v) { this.value = v; }
    public <R> R accept(ExprVisitor<R> v) { return v.visitNum(this); }
}

public class Add implements Expr {
    public final Expr left, right;
    public Add(Expr l, Expr r) { this.left = l; this.right = r; }
    public <R> R accept(ExprVisitor<R> v) { return v.visitAdd(this); }
}

public class Mul implements Expr {
    public final Expr left, right;
    public Mul(Expr l, Expr r) { this.left = l; this.right = r; }
    public <R> R accept(ExprVisitor<R> v) { return v.visitMul(this); }
}

Visitor interface

public interface ExprVisitor<R> {
    R visitNum(Num n);
    R visitAdd(Add a);
    R visitMul(Mul m);
}

Concrete visitors

public class Evaluator implements ExprVisitor<Integer> {
    public Integer visitNum(Num n) { return n.value; }
    public Integer visitAdd(Add a) { return a.left.accept(this) + a.right.accept(this); }
    public Integer visitMul(Mul m) { return m.left.accept(this) * m.right.accept(this); }
}

public class Printer implements ExprVisitor<String> {
    public String visitNum(Num n) { return Integer.toString(n.value); }
    public String visitAdd(Add a) { return "(" + a.left.accept(this) + " + " + a.right.accept(this) + ")"; }
    public String visitMul(Mul m) { return "(" + a.left.accept(this) + " * " + a.right.accept(this) + ")"; }
}

public class DepthCounter implements ExprVisitor<Integer> {
    public Integer visitNum(Num n) { return 1; }
    public Integer visitAdd(Add a) { return 1 + Math.max(a.left.accept(this), a.right.accept(this)); }
    public Integer visitMul(Mul m) { return 1 + Math.max(m.left.accept(this), m.right.accept(this)); }
}

Usage

Expr e = new Mul(new Add(new Num(2), new Num(3)), new Num(4));

System.out.println(e.accept(new Evaluator()));       // 20
System.out.println(e.accept(new Printer()));         // ((2 + 3) * 4)
System.out.println(e.accept(new DepthCounter()));    // 3

To add a new operation (Compiler, TypeChecker), write a new visitor — no changes to Expr/Num/Add/Mul.


Double Dispatch

Java methods dispatch on the receiver only — single dispatch. To dispatch on both the visitor and the element, you need two virtual calls:

e.accept(visitor)            // 1st dispatch: which Expr subtype?
   -> visitor.visitNum(n)    // 2nd dispatch: which Visitor subtype?

That's why accept() exists — it's the bridge from "which element type?" to "which visitor method?"

Double DispatchSequence diagram

The Trade-off

Visitor makes adding operations easy but adding element types hard:

Add a new element typeAdd a new operation
Without visitor (methods on elements)Easy — just one new classHard — edit every class
With visitorHard — every visitor needs a new methodEasy — one new visitor class

Pick visitor when the hierarchy is stable but operations grow. Don't pick visitor for hierarchies that change a lot.


Real-world Examples

Use caseVisitors
Compiler ASTsType checker, optimizer, code generator
File systemDisk usage, search, permission audit
XML / JSON DOMValidators, transformers, pretty-printers
javax.lang.modelJava annotation processors
Roslyn (.NET compiler)Syntax tree visitors

Modern Alternatives

In modern Java, sealed interfaces + pattern matching can replace visitor:

public sealed interface Expr permits Num, Add, Mul {}

int evaluate(Expr e) {
    return switch (e) {
        case Num n -> n.value;
        case Add a -> evaluate(a.left) + evaluate(a.right);
        case Mul m -> evaluate(m.left) * evaluate(m.right);
    };
}

The compiler enforces exhaustiveness without the visitor boilerplate. Use this when targeting Java 21+.


Trade-offs

✅ Pros:

  • Add new operations without modifying element classes
  • Each operation lives in one focused class
  • Visitor can carry state across visits (accumulators)

❌ Cons:

  • Adding element types is painful (touches every visitor)
  • Boilerplate: accept + N visit methods per visitor
  • Visitor needs access to element internals (often forces public fields)
  • Less natural than methods-on-classes for newcomers

Interview Tips

  • Use visitor when the interviewer says "the data is stable but we keep adding analyses / outputs / reports."
  • Mention double dispatch by name — interviewers like the term.
  • For Java 21+, mention sealed types + switch patterns as the modern alternative.

Finished reading?

Mark it done to track your progress in Designs.