Behavioral

Visitor

Represent an operation to be performed on elements of an object structure — define a new operation without changing the classes of the elements it operates on.

Advanced Pattern #23 of 23

What is the Visitor Pattern?

Visitor lets you add new operations to an existing object structure without modifying the classes of those objects. You separate the algorithm from the object structure — the algorithm lives in a Visitor class, while the object's accept(visitor) method dispatches the right visitor method via double dispatch.

Every time you need a new operation on the structure, you add a new Visitor class rather than touching the existing element classes.

Real-world analogy: A tax inspector (visitor) visiting different types of businesses (elements — restaurant, hotel, factory). Each business type has a different tax calculation. The inspector visits each and applies the right calculation for each type — the businesses don't change, just the inspector logic.

The Problem It Solves

You have a document object model: Paragraph, Image, Table elements. You need operations: export to HTML, export to PDF, spell-check. Adding each operation as a method to every element class bloats them all. Adding a fourth operation requires modifying all three element classes again.

Visitor puts each operation in its own class: HTMLExportVisitor, PDFExportVisitor, SpellCheckVisitor. Adding a new operation = one new class, zero changes to elements.

Participants

Visitor (interface)
Declares a visit(ConcreteElement) overload for each concrete element type in the structure. One method per element class.
ConcreteVisitor
Implements all visit methods — one per element type. Implements one full operation across all element types.
Element (interface)
Declares accept(Visitor). This is the key — it lets the visitor know which concrete element it's dealing with via double dispatch.
ConcreteElement
Implements accept(v) as v.visit(this) — passing itself to the visitor. This is the double dispatch mechanism.
ObjectStructure
Holds the collection of elements. Provides a method to iterate elements and call accept(visitor) on each.

Visual Flow Diagram

«interface» Visitor visit(Paragraph) visit(Image) visit(Table) HTMLExportVisitor visit(Paragraph) … SpellCheckVisitor visit(Paragraph) … «interface» Element accept(Visitor v) Paragraph accept(v) { v.visit(this) Image accept(v) { v.visit(this) v.visit(this) double dispatch Double dispatch: element.accept(visitor) → visitor.visit(element) — visitor always knows the exact concrete type

Java Code Example

Java — Document Export Visitor
// Visitor interface — one visit() overload per element type
public interface DocumentVisitor {
    void visit(Paragraph paragraph);
    void visit(Image     image);
    void visit(Table     table);
}

// Element interface — accept dispatches to correct visit() via double dispatch
public interface DocumentElement {
    void accept(DocumentVisitor visitor);
}

// Concrete Elements — each calls v.visit(this)
public class Paragraph implements DocumentElement {
    private final String text;
    public Paragraph(String text) { this.text = text; }
    public String getText() { return text; }
    public void accept(DocumentVisitor v) { v.visit(this); } // double dispatch
}

public class Image implements DocumentElement {
    private final String src;
    private final int    width, height;
    public Image(String src, int w, int h) { this.src=src; width=w; height=h; }
    public String getSrc() { return src; }
    public int getWidth() { return width; }
    public int getHeight() { return height; }
    public void accept(DocumentVisitor v) { v.visit(this); }
}

public class Table implements DocumentElement {
    private final List<List<String>> rows;
    public Table(List<List<String>> rows) { this.rows = rows; }
    public List<List<String>> getRows() { return rows; }
    public void accept(DocumentVisitor v) { v.visit(this); }
}

// ConcreteVisitor 1 — HTML export operation
public class HTMLExportVisitor implements DocumentVisitor {
    private final StringBuilder html = new StringBuilder();

    public void visit(Paragraph p) {
        html.append("<p>").append(p.getText()).append("</p>\n");
    }
    public void visit(Image img) {
        html.append("<img src='").append(img.getSrc())
            .append("' width='").append(img.getWidth()).append("'/>\n");
    }
    public void visit(Table t) {
        html.append("<table>\n");
        t.getRows().forEach(row -> {
            html.append("  <tr>");
            row.forEach(cell -> html.append("<td>").append(cell).append("</td>"));
            html.append("</tr>\n");
        });
        html.append("</table>\n");
    }
    public String getHTML() { return html.toString(); }
}

// ConcreteVisitor 2 — Spell check (separate operation, zero changes to elements)
public class SpellCheckVisitor implements DocumentVisitor {
    private final List<String> errors = new ArrayList<>();

    public void visit(Paragraph p) {
        if (p.getText().contains("teh")) errors.add("Typo in paragraph: teh");
    }
    public void visit(Image     img) { /* images have no text to check */ }
    public void visit(Table     t)   {
        t.getRows().stream().flatMap(List::stream)
            .filter(cell -> cell.contains("teh"))
            .forEach(c -> errors.add("Typo in table cell: " + c));
    }
    public List<String> getErrors() { return errors; }
}

// Object Structure — holds elements, provides traversal
public class Document {
    private final List<DocumentElement> elements = new ArrayList<>();

    public void add(DocumentElement e) { elements.add(e); }

    public void accept(DocumentVisitor visitor) {
        elements.forEach(e -> e.accept(visitor)); // double dispatch here
    }
}

// Client
public class Main {
    public static void main(String[] args) {
        Document doc = new Document();
        doc.add(new Paragraph("Hello teh world"));
        doc.add(new Image("logo.png", 200, 100));
        doc.add(new Table(List.of(List.of("Name", "Age"), List.of("Alice", "30"))));

        HTMLExportVisitor htmlVisitor = new HTMLExportVisitor();
        doc.accept(htmlVisitor);
        System.out.println(htmlVisitor.getHTML());

        SpellCheckVisitor spellVisitor = new SpellCheckVisitor();
        doc.accept(spellVisitor);
        System.out.println("Errors: " + spellVisitor.getErrors());
    }
}

When to Use / Avoid

✓ Use When

  • Object structure is stable but you frequently need to add new operations to it
  • Many distinct and unrelated operations need to be performed on elements
  • You don't want to "pollute" element classes with unrelated operations
  • Working with composite trees (AST, DOM, file system)

✕ Avoid When

  • New element types are added frequently — every new element requires updating all visitors
  • Element classes need to remain encapsulated — visitors may need access to private state
  • Only one or two operations are needed — a simple method on the element is cleaner

Real-World Examples

Pros & Cons

Pros

  • Open/Closed — add operations without changing element classes
  • Single Responsibility — each operation in its own Visitor class
  • Visitor can accumulate state across elements during traversal
  • Makes adding new operations trivial when element structure is stable

Cons

  • Adding a new element type requires updating ALL Visitor classes
  • Visitors may break encapsulation — they often need access to element internals
  • Double dispatch is non-obvious and hard to understand for beginners

How Visitor Can Be Broken

⚠ Attack Vectors

  • New element added without updating visitors: A new Video element is added to the structure — existing Visitors have no visit(Video) method. Java compile error if interface has it, but default no-op implementations silently ignore the new element
  • accept() not implemented with double dispatch: An element implements accept(v) as v.visit(this) but mistakenly calls the wrong overload — visitor executes logic for the wrong element type
  • Visitor accumulating shared mutable state across threads: A stateful Visitor (e.g., HTMLExportVisitor with a StringBuilder) is shared across multiple threads traversing the same document concurrently — output is corrupted
  • Element bypasses accept() and calls visitor directly: Element calls visitor.visit(anotherElement) instead of v.visit(this) — double dispatch breaks and the wrong element's logic is invoked

✓ Prevention

  • Sealed classes (Java 17+): Declare elements as sealed — every permitted subtype must be listed. Adding a new element type forces a compile error in all visitors that use exhaustive pattern matching, making gaps impossible to miss
  • Never use default no-op visit() implementations: All visit methods should be abstract in the interface — the compiler forces every visitor to explicitly handle every element
  • One visitor instance per traversal: Create a fresh visitor for each traversal invocation — never share mutable visitor state across threads or across multiple document.accept() calls
  • Strict accept() contract: The rule is absolute — accept(v) always calls v.visit(this) and nothing else. Enforce via code review or a lint rule. Any deviation breaks double dispatch silently.
Java — Break & Fix
// ❌ BREAKING — new element not handled (default no-op)
public interface DocumentVisitor {
    void visit(Paragraph p);
    void visit(Image img);
    default void visit(Video v) {} // ❌ default no-op — Video silently ignored
}

// ✅ FIX — all visit methods abstract, compiler forces implementation
public interface DocumentVisitor {
    void visit(Paragraph p);
    void visit(Image img);
    void visit(Video v);  // ✅ must implement — won't compile otherwise
}

// ✅ Java 17 sealed classes — exhaustive coverage enforced
public sealed interface DocumentElement
    permits Paragraph, Image, Table, Video {
    void accept(DocumentVisitor v);
}
// Pattern match in visitor — compiler error if new permit type not handled
public void process(DocumentElement e) {
    switch (e) {
        case Paragraph p -> visit(p);
        case Image     i -> visit(i);
        case Table     t -> visit(t);
        case Video     v -> visit(v); // ✅ sealed — compiler ensures complete coverage
    }
}

// ❌ BREAKING — wrong double dispatch
public class Image implements DocumentElement {
    public void accept(DocumentVisitor v) {
        v.visit(new Paragraph("")); // ❌ wrong type! visits Paragraph logic for an Image
    }
}

// ✅ FIX — accept() always and only calls v.visit(this)
public class Image implements DocumentElement {
    public void accept(DocumentVisitor v) {
        v.visit(this); // ✅ always this — double dispatch is sacred
    }
}

How Other Patterns Relate

Interview Cheat Sheet

  1. What: Separates operations from the object structure. Add new operations (Visitors) without changing element classes. Works via double dispatch: element.accept(v)v.visit(this) — visitor always knows the exact concrete type.
  2. Double dispatch explained: First dispatch = polymorphic accept() call selects the right element class. Second dispatch = overloaded visit(ConcreteElement) selects the right visitor method. Together they give type-safe dispatch without instanceof.
  3. Trade-off rule: Visitor is ideal when element types are stable but operations grow frequently. If element types grow frequently instead, Visitor becomes a maintenance burden — every new element requires updating all visitors.