Visitor
Visitor moves an operation out of the classes it works on into its own object, so new operations don't touch those classes. This page exports Pedal's fleet to CSV and plans its maintenance with two visitors, and shows the trade-off: new operations get cheap, new vehicle types get expensive.
1. The pain
Pedal's fleet has three kinds of vehicle: bikes, e-bikes and scooters. Other teams keep wanting new things done with the whole fleet: a CSV export for finance, a maintenance plan for the workshop, insurance values for the insurer. The first approach added a method for each to every vehicle class: to_csv(), maintenance(), insurance_value().
The vehicle classes, which describe what a bike is, filled up with export formats and insurance rules owned by other teams. Each new operation edited all three classes. The alternative, a function with if isinstance(v, EBike): … elif …, broke silently whenever someone forgot a branch.
2. The idea in one picture
Visitor moves an operation out of the classes it works on and into a separate object with one method per class: visit_bike(), visit_ebike(), visit_scooter(). Each vehicle has a single accept(visitor) method that calls the visitor method for its own type. That second call is the trick (called double dispatch): the right code runs for this vehicle type and this operation, with no type checks.
Visitor makes one kind of change cheap and the other expensive. Adding an operation is one new visitor; adding a vehicle type means a new method in every visitor. Choose it when the types are stable and the operations keep coming. In Python, functools.singledispatch or a match statement are lighter alternatives.
3. The roles
| Role | Its job | In Pedal |
|---|---|---|
| Visitor | Declares one visit method per element type. | VehicleVisitor |
| Concrete visitor | One operation across all element types. | CsvRow, MaintenanceTask |
| Element | Declares accept(visitor). | Vehicle |
| Concrete element | Calls the visitor method for its own type. | Bike, EBike, Scooter |
| Object structure | The collection the client walks. | The fleet list |
4. Before and after
One operation both ways (Python sketch; the full program is in section 5).
Without the pattern
class EBike(Vehicle):
def to_csv(self): ... # finance
def maintenance(self): ... # workshop
def insurance_value(self): ... # insurer
# or, outside the classes:
def to_csv(v):
if isinstance(v, Bike): ...
elif isinstance(v, EBike): ...
# Scooter forgotten: no error, no rowWith it
class EBike(Vehicle):
def accept(self, v):
return v.visit_ebike(self)
class CsvRow(VehicleVisitor):
def visit_bike(self, b): ...
def visit_ebike(self, e): ...
def visit_scooter(self, s): ...
for vehicle in fleet:
vehicle.accept(CsvRow())| The next change | Without the pattern | With it |
|---|---|---|
| Add insurance values | A method in every vehicle class | One InsuranceValue visitor |
| Forget a vehicle type in a new operation | A silent gap in an if/elif chain | An error: the visitor interface requires every method |
| Add a cargo bike | One new class | One new class, plus a method in every visitor (the cost) |
Visitor trades cheap new types for cheap new operations. Pick it when operations change more often than types.
5. The code
Three vehicle classes, each with one accept() method, and two visitors. Each visitor has one method per vehicle type; the scooter on old firmware gets an update task.
from __future__ import annotations
from abc import ABC, abstractmethod
# ---- The element classes: stable, and each knows only how to accept a visitor. ----
class Vehicle(ABC):
def __init__(self, vid: str) -> None:
self.vid = vid
@abstractmethod
def accept(self, v: VehicleVisitor) -> str: ...
class Bike(Vehicle):
def accept(self, v: VehicleVisitor) -> str:
return v.visit_bike(self)
class EBike(Vehicle):
def __init__(self, vid: str, battery: int) -> None:
super().__init__(vid)
self.battery = battery
def accept(self, v: VehicleVisitor) -> str:
return v.visit_ebike(self)
class Scooter(Vehicle):
def __init__(self, vid: str, firmware: str) -> None:
super().__init__(vid)
self.firmware = firmware
def accept(self, v: VehicleVisitor) -> str:
return v.visit_scooter(self)
# ---- Visitors: one class per operation, with one method per vehicle type. ----
class VehicleVisitor(ABC):
@abstractmethod
def visit_bike(self, b: Bike) -> str: ...
@abstractmethod
def visit_ebike(self, e: EBike) -> str: ...
@abstractmethod
def visit_scooter(self, s: Scooter) -> str: ...
class CsvRow(VehicleVisitor):
def visit_bike(self, b: Bike) -> str: return f"{b.vid},bike,"
def visit_ebike(self, e: EBike) -> str: return f"{e.vid},e-bike,battery {e.battery}%"
def visit_scooter(self, s: Scooter) -> str: return f"{s.vid},scooter,firmware {s.firmware}"
class MaintenanceTask(VehicleVisitor):
LATEST_FIRMWARE = "2.2"
def visit_bike(self, b: Bike) -> str:
return f"{b.vid}: brakes and chain"
def visit_ebike(self, e: EBike) -> str:
return f"{e.vid}: brakes, chain, battery health ({e.battery}%)"
def visit_scooter(self, s: Scooter) -> str:
update = f", firmware update to {self.LATEST_FIRMWARE}" if s.firmware != self.LATEST_FIRMWARE else ""
return f"{s.vid}: tyres{update}"
if __name__ == "__main__":
fleet: list[Vehicle] = [Bike("B-17"), EBike("E-205", 84), Scooter("K-310", "2.1")]
print("csv:\nid,kind,detail")
for vehicle in fleet:
print(vehicle.accept(CsvRow()))
print("maintenance:")
for vehicle in fleet:
print(vehicle.accept(MaintenanceTask()))
// ---- The element classes: stable, and each knows only how to accept a visitor. ----
abstract class Vehicle {
constructor(readonly vid: string) {}
abstract accept(v: VehicleVisitor): string;
}
class Bike extends Vehicle {
accept(v: VehicleVisitor): string { return v.visitBike(this); }
}
class EBike extends Vehicle {
constructor(vid: string, readonly battery: number) { super(vid); }
accept(v: VehicleVisitor): string { return v.visitEBike(this); }
}
class Scooter extends Vehicle {
constructor(vid: string, readonly firmware: string) { super(vid); }
accept(v: VehicleVisitor): string { return v.visitScooter(this); }
}
// ---- Visitors: one class per operation, with one method per vehicle type. ----
interface VehicleVisitor {
visitBike(b: Bike): string;
visitEBike(e: EBike): string;
visitScooter(s: Scooter): string;
}
class CsvRow implements VehicleVisitor {
visitBike(b: Bike) { return `${b.vid},bike,`; }
visitEBike(e: EBike) { return `${e.vid},e-bike,battery ${e.battery}%`; }
visitScooter(s: Scooter) { return `${s.vid},scooter,firmware ${s.firmware}`; }
}
class MaintenanceTask implements VehicleVisitor {
static readonly LATEST_FIRMWARE = "2.2";
visitBike(b: Bike) { return `${b.vid}: brakes and chain`; }
visitEBike(e: EBike) { return `${e.vid}: brakes, chain, battery health (${e.battery}%)`; }
visitScooter(s: Scooter) {
const update = s.firmware !== MaintenanceTask.LATEST_FIRMWARE ? `, firmware update to ${MaintenanceTask.LATEST_FIRMWARE}` : "";
return `${s.vid}: tyres${update}`;
}
}
const fleet: Vehicle[] = [new Bike("B-17"), new EBike("E-205", 84), new Scooter("K-310", "2.1")];
console.log("csv:\nid,kind,detail");
for (const vehicle of fleet) console.log(vehicle.accept(new CsvRow()));
console.log("maintenance:");
for (const vehicle of fleet) console.log(vehicle.accept(new MaintenanceTask()));import java.util.List;
public class Main {
// ---- The element classes: stable, and each knows only how to accept a visitor. ----
abstract static class Vehicle {
final String vid;
Vehicle(String vid) { this.vid = vid; }
abstract String accept(VehicleVisitor v);
}
static class Bike extends Vehicle {
Bike(String vid) { super(vid); }
String accept(VehicleVisitor v) { return v.visitBike(this); }
}
static class EBike extends Vehicle {
final int battery;
EBike(String vid, int battery) { super(vid); this.battery = battery; }
String accept(VehicleVisitor v) { return v.visitEBike(this); }
}
static class Scooter extends Vehicle {
final String firmware;
Scooter(String vid, String firmware) { super(vid); this.firmware = firmware; }
String accept(VehicleVisitor v) { return v.visitScooter(this); }
}
// ---- Visitors: one class per operation, with one method per vehicle type. ----
interface VehicleVisitor {
String visitBike(Bike b);
String visitEBike(EBike e);
String visitScooter(Scooter s);
}
static class CsvRow implements VehicleVisitor {
public String visitBike(Bike b) { return b.vid + ",bike,"; }
public String visitEBike(EBike e) { return e.vid + ",e-bike,battery " + e.battery + "%"; }
public String visitScooter(Scooter s) { return s.vid + ",scooter,firmware " + s.firmware; }
}
static class MaintenanceTask implements VehicleVisitor {
static final String LATEST_FIRMWARE = "2.2";
public String visitBike(Bike b) { return b.vid + ": brakes and chain"; }
public String visitEBike(EBike e) { return e.vid + ": brakes, chain, battery health (" + e.battery + "%)"; }
public String visitScooter(Scooter s) {
String update = s.firmware.equals(LATEST_FIRMWARE) ? "" : ", firmware update to " + LATEST_FIRMWARE;
return s.vid + ": tyres" + update;
}
}
public static void main(String[] args) {
List<Vehicle> fleet = List.of(new Bike("B-17"), new EBike("E-205", 84), new Scooter("K-310", "2.1"));
System.out.println("csv:\nid,kind,detail");
for (Vehicle vehicle : fleet) System.out.println(vehicle.accept(new CsvRow()));
System.out.println("maintenance:");
for (Vehicle vehicle : fleet) System.out.println(vehicle.accept(new MaintenanceTask()));
}
}Output (the same in all three languages)
csv: id,kind,detail B-17,bike, E-205,e-bike,battery 84% K-310,scooter,firmware 2.1 maintenance: B-17: brakes and chain E-205: brakes, chain, battery health (84%) K-310: tyres, firmware update to 2.2
6. Try it
Which classes change?
Rows are vehicle types, columns are operations. Pick a design, then make a change and see which classes you'd edit.
Green: new code. Yellow: existing classes you must edit.
7. When not to use it
What you get
- New operations are additive: one visitor class, no edits to vehicles.
- Element classes stay focused: no export or insurance code in
EBike. - Completeness is checked: a visitor must handle every type.
- Related code together: all of the CSV logic lives in one class.
What you pay
- New types are expensive: every visitor gains a method.
- Encapsulation strain: visitors often need access to element internals.
- Ceremony:
acceptmethods and a visitor interface for what could be a function. - Hard to follow: double dispatch surprises readers.
| If… | Use instead |
|---|---|
| New vehicle types arrive more often than new operations | Methods on the classes (plain polymorphism) |
| The language has pattern matching or single dispatch | match / functools.singledispatch |
| You only need to walk a structure | 20 Iterator |
| There's one operation | A method or a function |
8. How it goes wrong
9. Remember it and say it
How this hook works: what “The inspector brings the checklist” means
What the slogan means, phrase by phrase
- “The inspector”
- A visitor: one operation.
- “brings the checklist”
- One visit method per type.
- “says what it is”
accept()calls the method for its own type.
Why this picture fits: Buildings don't change when a new kind of inspection is introduced, and inspectors handle every kind of flat.
The story → the concept
| The building | The code |
|---|---|
| Flats: studio, two-bed, penthouse | Bike, EBike, Scooter |
| A gas-safety inspector | MaintenanceTask |
| A census taker | CsvRow |
| Opening the door | accept(visitor) |
Watch it on an example
- The loop hands E-205 the maintenance visitor.
- E-205's
acceptcallsvisit_ebike(self). - The visitor returns "brakes, chain, battery health (84%)": e-bike rules, no type checks.
The trap it saves you from
Stuffing every team's operation into the domain classes, or writing if/elif chains that forget a type.
It still works when…
Yes: compilers walking syntax trees, document exporters (to PDF, to HTML), and static analysers.
In an interview or code review
- Say which axis is stable: "Vehicle types rarely change; operations keep arriving, so each operation is a visitor."
- Explain double dispatch in one line:
vehicle.accept(v)callsv.visit_ebike(self), so both the type and the operation choose the code. - Name the cost: a new vehicle type adds a method to every visitor.
- Offer the lighter option: in Python,
singledispatchormatch; in Java 21, sealed types with pattern-matchingswitch.
10. Check yourself
What does a visitor separate?
What does EBike.accept(v) do?
Adding an insurance-value operation with visitors changes…
Adding a cargo bike with visitors changes…
Which Python feature is a lighter alternative?
11. Go deeper
| Resource | Why |
|---|---|
| 10 Composite | Visitors often walk composites. |
Python functools.singledispatch | Type-based dispatch without accept methods. |
Python ast.NodeVisitor | A visitor in the standard library, walking syntax trees. |
| Wikipedia: Visitor pattern | Double dispatch and the expression problem. |