Intermediate13 min readDesign Patternslive prototype

Visitor

Add new operations to a fixed family of classes without editing them, by packaging each operation as its own *visitor* object. Like an insurance agent walking a street of buildings: the house, the bank and the coffee shop never learn anything new — each just opens the door (`accept(visitor)`) and the agent does the right thing for that building. Need a safety audit next month? Send a *different* visitor down the same street — zero changes to the buildings.

The idea

What it is

Visitor answers one question: how do I add a brand-new operation to a whole family of classes without editing any of them? The intent in one sentence: put each operation in its own visitor object, and let every element 'accept' the visitor instead of implementing the operation itself. Imagine an insurance agent working one street with three very different buildings — a house, a bank, a coffee shop. The agent carries all the expertise: at the house they sell home insurance, at the bank theft insurance, at the coffee shop fire insurance. The buildings don't know anything about insurance. Each one just opens the door — accept(visitor) — and the visitor does whatever is right for that building type.

Now the payoff. Next month a safety auditor needs to inspect the same street, and next year a tax assessor. Without Visitor you'd crack open House, Bank and CoffeeShop and bolt a new method onto each — three edits per operation, forever, and the building classes slowly fill up with insurance math, audit checklists and tax tables that have nothing to do with being a building. With Visitor, every new operation is simply a new visitor class walking the same street. The buildings are written once and never touched again — which is [[open-closed]] in its purest form: closed for modification, open for extension by adding visitors.

The one sentence to remember

Visitor moves operations out of a family of classes and into standalone visitor objects — one visitor per operation, with one visitX method per element type. Elements only ever do one tiny thing: accept(v), which hands themselves to the visitor's matching method.

Fair warning: this is the trickiest GoF pattern

Visitor feels backwards the first time — the object hands itself to the operation instead of the operation being a method on the object. That flip is powered by a technique called double dispatch, which we unpack slowly below. Hold onto the street: a building opening its door (first dispatch), then the agent doing the right thing for that building type (second dispatch). Every confusing line of Visitor code is just one of those two moments.

Mechanics

How it works

The four moving parts

Visitor splits the world into a stable side (the elements — our buildings) and a growing side (the visitors — one per operation):

  • Element interface — declares exactly one method: accept(visitor). That's the building's front door. (Building with accept(v).)
  • Concrete elementsHouse, Bank, CoffeeShop. Each implements accept with one line that names its own type: House.accept(v) calls v.visitHouse(this), Bank.accept(v) calls v.visitBank(this), and so on. This one line is the whole trick.
  • Visitor interface — declares one visitX method per element type: visitHouse(h), visitBank(b), visitShop(s). It's the shape every operation must fill in: 'tell me what you do at each kind of building'.
  • Concrete visitors — one class per operation: InsuranceAgent, SafetyAuditor, TaxAssessor. Each fills in all three visitX methods with that operation's logic. Adding an operation = adding one of these. Nothing else changes.
typescript
interface Visitor {                    // one visitX per element type
  visitHouse(h: House): void;
  visitBank(b: Bank): void;
  visitShop(s: CoffeeShop): void;
}

interface Building {
  accept(v: Visitor): void;            // the front door
}

class House implements Building {
  accept(v: Visitor) { v.visitHouse(this); }  // "I'm a House — use your House move"
}
class Bank implements Building {
  accept(v: Visitor) { v.visitBank(this); }   // "I'm a Bank — use your Bank move"
}
class CoffeeShop implements Building {
  accept(v: Visitor) { v.visitShop(this); }
}

// A new operation = a new class. The buildings above never change.
class InsuranceAgent implements Visitor {
  visitHouse(h: House)      { /* sell a home policy   */ }
  visitBank(b: Bank)        { /* sell theft cover     */ }
  visitShop(s: CoffeeShop)  { /* sell fire cover      */ }
}

Double dispatch, slowly

Here's the puzzle Visitor solves. You're holding a mixed street — a list typed as Building[] — and an agent. For each building you need to run the right combination of which building it is and which visitor is calling. That's a decision based on two runtime types at once, and ordinary method calls only ever pick a method based on one (the object you call it on). Visitor gets both by chaining two ordinary calls:

  1. First dispatch — the building's door. You call building.accept(agent). The language's normal polymorphism ([[polymorphism]]) looks at the building's real runtime type and picks the right acceptHouse.accept, Bank.accept, whichever. At this moment the code knows it's standing inside a House.
  2. Second dispatch — the visitor's move. That accept body is one line: v.visitHouse(this). Now polymorphism runs again, this time on the visitor's runtime type — if v is an InsuranceAgent you get the agent's visitHouse, if it's a SafetyAuditor you get the auditor's visitHouse. Two hops, and you've landed on exactly the (building type × visitor type) cell you needed.

Why not just overload visit(element)?

A tempting shortcut: give the visitor one overloaded method — visit(House), visit(Bank), visit(CoffeeShop) — and call agent.visit(building) directly. It doesn't work. In Java, C++ and TypeScript, overloads are chosen at compile time from the variable's declared type — and your variable is declared Building, so the compiler either refuses (no visit(Building) overload) or always picks the same one. Only virtual dispatch on the receiver looks at runtime types — so Visitor uses it twice: once on the element (accept), once on the visitor (visitHouse). That's all 'double dispatch' means.

The accept line is the building signing its name

Notice that v.visitHouse(this) hard-codes the word House — written inside the House class, where that fact is always true. The building isn't doing any insurance work; it's just announcing its own type in a way the compiler can check, then handing itself over (this). One line per element, written once, reused by every visitor forever.

The trade: cheap operations, expensive new element types

Visitor doesn't remove work — it moves it. Look at the grid of (element types × operations). Plain OO puts each operation as a method inside each element class: adding an operation means editing every element. Visitor flips the grid: adding an operation is one new visitor class and zero edits — but adding an element type (a fourth building, say Museum) now means editing every visitor: the Visitor interface gains visitMuseum, and InsuranceAgent, SafetyAuditor and TaxAssessor must each implement it. It's the exact mirror image. So the pattern pays off precisely when the element family is stable and the operations keep coming — and punishes you when it's the other way round.

Where you've already met it

  • Compilers and ASTs — the classic home. A syntax tree has a fixed set of node types (IfNode, CallNode, LiteralNode…), but the operations never stop coming: a type-check visitor, a code-gen visitor, a lint visitor, a pretty-print visitor — each a separate class walking the same tree.
  • Document exporters — one document tree of headings, paragraphs, tables and images; an HtmlExporter, PdfExporter and MarkdownExporter visitor each render the same nodes their own way, often walking a [[composite]] structure.
  • File-system reports — walk one tree of files and folders with a total-size visitor, a virus-scan visitor, or a find-large-files visitor, without teaching File and Folder any of those jobs.

Interactive prototype

See it. Build it. Break it.

A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.

About this simulation

A street of three buildings — 🏠 House, 🏦 Bank, ☕ Coffee Shop. Pick a visitor chip (🧾 InsuranceAgent or 🔍 SafetyAuditor) and press ▶ Visit street: the agent walks building to building, and at each one you see both dispatches happen — first the building's accept(v) tag flashes (the building says 'I'm a House'), then the visitor's matching method chip lights up (visitHouse(), visitBank(), visitShop()) and the result slot fills with that visitor's product for that building. Switch chips and re-visit: same three buildings, completely different results — and the counter building classes edited: 0 never moves. Press + New visitor to add a 💰 TaxAssessor: a whole new operation appears as just a new class, and the buildings still haven't changed a line.

Hands-on

Try these yourself

Open the prototype above, predict what happens, then verify.

try 01

Watch both dispatches at one building

With the 🧾 InsuranceAgent chip selected, press ▶ Visit street and keep your eyes on the House card. Two beats: first its accept(v) tag flashes — that's the first dispatch, the House's own accept method being chosen. Then the visitHouse() chip pops in and the result slot fills with 'home policy $120' — the second dispatch, the InsuranceAgent's House-specific method. The agent then repeats the same two beats at the Bank (visitBank() → 'theft cover $900') and the Coffee Shop (visitShop() → 'fire cover $310').

try 02

Same street, different operation

Click the 🔍 SafetyAuditor chip — all three result slots clear. Press ▶ Visit street again: the same three buildings now produce 'smoke alarms ✓', 'vault check ✓' and 'gas lines ✓'. Nothing about the buildings changed — check the counter: building classes edited: 0. The entire difference between the two runs lives inside which visitor object walked the street.

try 03

Add an operation for free

Press + New visitor. A 💰 TaxAssessor chip appears with a green 'just a new class' flash — that flash is the whole pattern: a brand-new operation entered the system as one new class. Select it and ▶ Visit street to see assessments fill in; the edit counter still reads 0. Now imagine the reverse — adding a fourth building — and notice every one of the three visitor chips would need a new visitMuseum() method. That asymmetry is the trade you're buying.

In practice

When to use it — and what trips people up

Reach for Visitor when operations outnumber element types

Visitor is a specialist's tool: spectacular in its niche, clumsy outside it. The niche has a precise shape:

  • The element hierarchy is stable, the operations keep coming — a compiler's AST node types are fixed by the language grammar, but every release wants another pass (lint, optimize, format). New pass = new visitor, zero risk to the nodes.
  • Unrelated operations are polluting the element classes — if Document nodes are accumulating toHtml(), toPdf(), wordCount(), spellCheck()… each with its own imports and helpers, visitors pull every operation into its own cohesive class and give the elements back a single responsibility ([[single-responsibility]]).
  • One operation needs to act differently per concrete type across a mixed collection — you're iterating Building[] and instanceof/isinstance chains are spreading. Double dispatch replaces the whole chain with compiler-checked methods: forget one element type and the code doesn't compile, whereas a forgotten else if fails silently.

When NOT to use it

Skip Visitor when the element types change often — every new element forces a new method into every existing visitor, which is death by a thousand cuts. Skip it when the hierarchy is tiny and settled with only a couple of operations — plain methods on the classes are shorter, clearer, and don't make readers chase acceptvisitX indirection. And if the operation only needs one element type, you don't need dispatch at all — just a function.

What it gives you

  • New operations are one new class — a lint pass, an exporter, a report joins the system without touching a single element, the open-closed principle at its cleanest.
  • Related behaviour lives together — all of the insurance logic sits in InsuranceAgent instead of being smeared one method at a time across every building class.
  • The compiler audits completeness — the Visitor interface forces every concrete visitor to handle every element type; a forgotten case is a compile error, not a silent runtime bug.
  • Visitors can carry state across the walk — a size-totalling or code-generating visitor accumulates results as it moves element to element, something scattered per-class methods can't do cleanly.

Common mistakes

  • Adding an element type is expensive — a fourth building means a new visitX on the interface and an implementation in every visitor ever written; the pattern's superpower exactly mirrored as its weakness.
  • Elements may need to expose internals — visitors live outside the element classes, so fields the operation needs (rooms, vaults) must become public or gain getters, weakening encapsulation.
  • The indirection is genuinely confusing at first — accept calls visit which was picked by accept; readers new to double dispatch will trace the two hops several times before it clicks.
  • It hard-couples visitors to the concrete element list — every visitor names House, Bank and CoffeeShop explicitly, so the element family and all visitors must always evolve in lockstep.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

// Visitor interface: one visitX method per element type.
interface Visitor {
  visitHouse(h: House): void;
  visitBank(b: Bank): void;
  visitShop(s: CoffeeShop): void;
}

// Element interface: just a front door.
interface Building {
  accept(v: Visitor): void;               // 1st dispatch happens here
}

class House implements Building {
  rooms = 4;
  accept(v: Visitor) { v.visitHouse(this); }   // 2nd dispatch: House-shaped
}

class Bank implements Building {
  vaults = 2;
  accept(v: Visitor) { v.visitBank(this); }
}

class CoffeeShop implements Building {
  burners = 6;
  accept(v: Visitor) { v.visitShop(this); }
}

// Operation #1 — one visitor class per operation.
class InsuranceAgent implements Visitor {
  visitHouse(h: House)     { console.log(`home policy $${30 * h.rooms}`); }
  visitBank(b: Bank)       { console.log(`theft cover $${450 * b.vaults}`); }
  visitShop(s: CoffeeShop) { console.log(`fire cover $${50 + 40 * s.burners}`); }
}

// Operation #2 — added later. The buildings above were NOT edited.
class SafetyAuditor implements Visitor {
  visitHouse(h: House)     { console.log(`check smoke alarms in ${h.rooms} rooms`); }
  visitBank(b: Bank)       { console.log(`inspect ${b.vaults} vaults`); }
  visitShop(s: CoffeeShop) { console.log(`test ${s.burners} gas burners`); }
}

const street: Building[] = [new House(), new Bank(), new CoffeeShop()];
for (const b of street) b.accept(new InsuranceAgent()); // policies for each
for (const b of street) b.accept(new SafetyAuditor());  // audits — same street

References & further reading

5 sources

Knowledge check

Did it land?

Quick questions, answers revealed on submit. Sign in to save your best score.

question 01 / 05

What is the core intent of the Visitor pattern?

question 02 / 05

In double dispatch, what do the two dispatches actually select?

question 03 / 05

Why can't a plain overloaded method — visit(House), visit(Bank) — replace the accept/visit pair when iterating a Building[] list?

question 04 / 05

Your street gains a fourth element type, Museum. What does the Visitor pattern force you to do?

question 05 / 05

Which situation is the strongest fit for the Visitor pattern?

0/5 answered