CMD Guide
HomeOO & Low-Level DesignBehavioral

Visitor Pattern

The Visitor pattern lets you add new operations to a fixed family of element types without touching those types, and it works through double dispatch: the call element.accept(visitor) dispatches once on the element's runtime type to land in Food.accept, which then calls visitor.visitFood(this) — a second dispatch on the visitor's runtime type. Two virtual calls in a row resolve both the element type and the operation type, which is the one thing a single method call in most OO languages cannot do on its own.

Why the naive single-dispatch version fails

The instinct is to skip the dance and just write one method that switches on type:

// BROKEN: tries to pick the operation from one dispatch
void applyHoliday(Product p) {
    if (p instanceof Food)        { /* food logic */ }
    else if (p instanceof Clothing) { /* clothing logic */ }
    else if (p instanceof Electronics) { /* electronics logic */ }
    // forgot a type? silent fall-through, no compile error
}

This compiles, but it pushes the type decision into a runtime instanceof ladder. Add a fourth product and every such ladder, scattered across every operation, must be found and edited — the compiler will not remind you. Double dispatch replaces the ladder with the language's own method resolution: accept already knows it is a Food (no instanceof), and the visitor interface forces every visitor to implement visitFood (a missing one is a compile error). The naive version trades that compile-time safety for a runtime guess.

diagram
diagram

Traced example with real values

Three products in a cart, two discount visitors. Each product carries a base price; each visitor applies a percentage based on the concrete product type. Trace the call food.accept(holidayVisitor) and its siblings:

CallDispatch 1 (element)Dispatch 2 (visitor)Rule firedResult
food.accept(holiday)Food.acceptHoliday.visitFoodfood: 5% off$20.00 → $19.00
clothing.accept(clearance)Clothing.acceptClearance.visitClothingclothing: 40% off$80.00 → $48.00
electronics.accept(holiday)Electronics.acceptHoliday.visitElectronicselectronics: 10% off$500.00 → $450.00

Notice no instanceof and no type tag anywhere: the pair (product type, discount strategy) is selected purely by which method the two virtual calls resolve to. Swap holiday for clearance on the same product and a totally different cell of the matrix fires — that is double dispatch picking one cell from the (elements × visitors) grid.

Working code (Java)

The accept/visit dance, completed so the trace above actually runs and prints the numbers:

interface DiscountVisitor {
    double visitFood(Food f);
    double visitClothing(Clothing c);
    double visitElectronics(Electronics e);
}

interface Product {              // Element interface
    double accept(DiscountVisitor v);
    double price();
}

class Food implements Product {
    private final double price;
    Food(double p) { this.price = p; }
    public double price() { return price; }
    public double accept(DiscountVisitor v) { return v.visitFood(this); }   // dispatch 1
}
class Clothing implements Product {
    private final double price;
    Clothing(double p) { this.price = p; }
    public double price() { return price; }
    public double accept(DiscountVisitor v) { return v.visitClothing(this); }
}
class Electronics implements Product {
    private final double price;
    Electronics(double p) { this.price = p; }
    public double price() { return price; }
    public double accept(DiscountVisitor v) { return v.visitElectronics(this); }
}

class HolidayDiscountVisitor implements DiscountVisitor {   // dispatch 2 lands here
    public double visitFood(Food f)              { return f.price() * 0.95; } //  5% off
    public double visitClothing(Clothing c)      { return c.price() * 0.90; } // 10% off
    public double visitElectronics(Electronics e){ return e.price() * 0.90; } // 10% off
}
class ClearanceDiscountVisitor implements DiscountVisitor {
    public double visitFood(Food f)              { return f.price() * 0.80; }
    public double visitClothing(Clothing c)      { return c.price() * 0.60; } // 40% off
    public double visitElectronics(Electronics e){ return e.price() * 0.85; }
}

public class Solution {
    public static void main(String[] args) {
        Product food = new Food(20.0);
        Product clothing = new Clothing(80.0);
        Product electronics = new Electronics(500.0);
        var holiday = new HolidayDiscountVisitor();
        var clearance = new ClearanceDiscountVisitor();

        System.out.printf("%.2f%n", food.accept(holiday));         // 19.00
        System.out.printf("%.2f%n", clothing.accept(clearance));   // 48.00
        System.out.printf("%.2f%n", electronics.accept(holiday));  // 450.00
    }
}

The Go equivalent uses a method on each concrete type calling back into the visitor interface — the same two dispatches, since Go has no overloading: name the methods VisitFood, VisitClothing, VisitElectronics on the visitor interface and have each type's Accept(v Visitor) call the right one.

Pitfalls

When to use it / when NOT to

Signals that point here: a fixed set of element types (an AST, a DOM, a shape hierarchy, a fixed product catalog); many distinct, unrelated operations over them (type-check, pretty-print, optimize, serialize); and you want each operation's logic in one cohesive place rather than smeared as a method on every element class.

Trade-offs vs alternatives

Decision rule: choose Visitor when element types are stable but operations grow and each must branch per element type; prefer methods on the elements when the operation set is stable but you keep adding element types; prefer Strategy when the behavior varies but not by element type.

Takeaways


Re-authored and deepened for this guide. Sources: Gamma, Helm, Johnson & Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (1994), the canonical Visitor / double-dispatch treatment; Robert Nystrom, Crafting Interpreters, ch. "Representing Code" (Visitor over an AST); the Refactoring.Guru entry on Visitor; and the original GeeksforGeeks retail-discount example that this page expands. Worked numbers, the double-dispatch diagram, the broken single-dispatch counter-example, and the selection/trade-off analysis were added for this guide.

The Expression Problem — and a modern alternative

Visitor's "cheap operations, expensive elements" trade-off is one face of the Expression Problem: no single technique lets you add both new element types and new operations without editing existing code. Putting methods on the elements makes new elements cheap and new operations expensive; Visitor makes exactly the opposite choice. If you genuinely need both axes open, Visitor is the wrong tool — reach for an operation registry keyed by type, or a language with multi-methods (Clojure, Julia) where dispatch is symmetric on all arguments.

In modern Java (17+), a sealed interface plus a switch with pattern matching is often the better replacement for Visitor when the element set is stable but you dislike the accept/visit boilerplate: sealed interface Product permits Food, Clothing, Electronics makes the compiler enforce that a switch (product) covers every permitted subtype — the same exhaustiveness Visitor buys, but without the double-dispatch ceremony and the cyclic element↔visitor coupling. The tradeoff flips back if elements change often: with a sealed switch you edit every switch when you add a type (like Visitor edits every visitor), whereas plain polymorphic methods on the elements absorb a new type for free. Choose the sealed-switch when operations grow and you want exhaustiveness without accept(); choose Visitor when you cannot use a modern language feature or must dispatch across two independent hierarchies.

A trap the compiler will NOT catch

If you write the visitor with method overloads (visit(Shape), visit(Circle)) instead of distinctly-named methods, Java resolves the overload on the static type at the accept call site — so visitor.visit(this) where this is statically Shape silently calls visit(Shape), never visit(Circle). Compilation stays green; behavior is wrong. This is why the canonical Visitor uses distinct names (visitFood, visitClothing) — it forces the second dispatch to land on the concrete type rather than collapsing to an overload chosen at compile time.

🤖 Don't fully get this? Learn it with Claude

Stuck on Visitor Pattern? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Visitor Pattern** (OO & Low-Level Design) and want to truly understand it. Explain Visitor Pattern from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Visitor Pattern** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Visitor Pattern** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Visitor Pattern** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes