InterviewKata
Design patterns · 21 of 26 · Objects talking

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

A building inspector visits every flat in a block. The inspector brings the checklist; each flat just opens the door and says what kind of flat it is ("studio", "two-bed"), and the inspector uses the right part of the checklist. Next year a fire-safety inspector comes with a different checklist, and the flats don't change at all.

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

RoleIts jobIn Pedal
VisitorDeclares one visit method per element type.VehicleVisitor
Concrete visitorOne operation across all element types.CsvRow, MaintenanceTask
ElementDeclares accept(visitor).Vehicle
Concrete elementCalls the visitor method for its own type.Bike, EBike, Scooter
Object structureThe 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 row

With 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 changeWithout the patternWith it
Add insurance valuesA method in every vehicle classOne InsuranceValue visitor
Forget a vehicle type in a new operationA silent gap in an if/elif chainAn error: the visitor interface requires every method
Add a cargo bikeOne new classOne 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: accept methods 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 operationsMethods on the classes (plain polymorphism)
The language has pattern matching or single dispatchmatch / functools.singledispatch
You only need to walk a structure20 Iterator
There's one operationA method or a function

8. How it goes wrong

Using visitor when types change oftenEach new vehicle type edits every visitor. If types are the moving part, keep methods on the classes.
A default visit method that does nothingIt hides missing cases, which was the main benefit. Make every visit method required.
Visitors that change the elementsAn export visitor that also fixes battery levels mixes reading and writing. Keep visitors to one job.
Exposing everything for the visitor's sakeMaking every field public so visitors can read it weakens the classes. Expose what operations genuinely need.

9. Remember it and say it

Memory hook: "The inspector brings the checklist"Each flat opens the door and says what it is; the visiting inspector applies the right checks.
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 buildingThe code
Flats: studio, two-bed, penthouseBike, EBike, Scooter
A gas-safety inspectorMaintenanceTask
A census takerCsvRow
Opening the dooraccept(visitor)

Watch it on an example

  1. The loop hands E-205 the maintenance visitor.
  2. E-205's accept calls visit_ebike(self).
  3. 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

10. Check yourself

What does a visitor separate?

Each operation becomes its own class.

What does EBike.accept(v) do?

That's double dispatch.

Adding an insurance-value operation with visitors changes…

Operations are the cheap direction.

Adding a cargo bike with visitors changes…

Types are the expensive direction.

Which Python feature is a lighter alternative?

Dispatch on the argument's type without accept methods.

11. Go deeper

ResourceWhy
10 CompositeVisitors often walk composites.
Python functools.singledispatchType-based dispatch without accept methods.
Python ast.NodeVisitorA visitor in the standard library, walking syntax trees.
Wikipedia: Visitor patternDouble dispatch and the expression problem.