Design Patterns, Explained Simply · Behavioral Patterns: How Objects Share Work
Visitor
Add new operations to a set of classes without changing them, by putting each operation in its own visitor object that the classes accept.
The problem
A shop's cart contains books, electronics and groceries. Over time, each item class grows methods for unrelated features: tax() for finance, shipping_weight() for logistics, to_xml() for an export, risk_score() for fraud checks. The item classes, which should be simple data, now import tax tables and XML libraries, and every new report means editing all three classes.
Real-world analogy
An insurance agent visits different buildings: a house, a bank, a coffee shop. The agent has a different policy for each kind of building, and the buildings don't change; they just "let the agent in". A different visitor, such as a fire inspector, brings their own rules for the same buildings.
Without the pattern
class Book:
def tax(self): ...
def shipping_weight(self): ...
def to_xml(self): ... # every new operation is added to every item class
class Electronics:
def tax(self): ...
def shipping_weight(self): ...
def to_xml(self): ...
The pattern
- Element classes (
Book,Electronics,Grocery) each have one method:accept(visitor), which calls the visitor method for its own type:visitor.visit_book(self). - Visitor interface: one method per element type (
visit_book,visit_electronics,visit_grocery). - Concrete visitors (
TaxVisitor,ShippingVisitor): each is one operation across all element types.
The accept → visit_x call is called double dispatch: the right code depends on both the element's type and the visitor's type.
Trade-off: adding a new operation is easy (one new visitor), but adding a new element type means updating every visitor.
The code
class Book:
def __init__(self, price):
self.price = price
def accept(self, visitor):
return visitor.visit_book(self)
class Electronics:
def __init__(self, price, kg):
self.price, self.kg = price, kg
def accept(self, visitor):
return visitor.visit_electronics(self)
class Grocery:
def __init__(self, price, kg):
self.price, self.kg = price, kg
def accept(self, visitor):
return visitor.visit_grocery(self)
class TaxVisitor: # one operation, all element types
def visit_book(self, item):
return 0 # books are tax-free here
def visit_electronics(self, item):
return item.price * 20 // 100
def visit_grocery(self, item):
return item.price * 5 // 100
class ShippingWeightVisitor: # another operation, no item class changed
def visit_book(self, item):
return 1
def visit_electronics(self, item):
return item.kg
def visit_grocery(self, item):
return item.kg + 1 # cold packaging
cart = [Book(2000), Electronics(50000, 3), Grocery(1200, 2)]
print("tax:", sum(item.accept(TaxVisitor()) for item in cart), "cents")
print("shipping weight:", sum(item.accept(ShippingWeightVisitor()) for item in cart), "kg")
Output
tax: 10060 cents shipping weight: 7 kg
interface Visitor<T> {
visitBook(item: Book): T;
visitElectronics(item: Electronics): T;
visitGrocery(item: Grocery): T;
}
interface Item { accept<T>(visitor: Visitor<T>): T; }
class Book implements Item {
constructor(readonly price: number) {}
accept<T>(visitor: Visitor<T>) { return visitor.visitBook(this); }
}
class Electronics implements Item {
constructor(readonly price: number, readonly kg: number) {}
accept<T>(visitor: Visitor<T>) { return visitor.visitElectronics(this); }
}
class Grocery implements Item {
constructor(readonly price: number, readonly kg: number) {}
accept<T>(visitor: Visitor<T>) { return visitor.visitGrocery(this); }
}
class TaxVisitor implements Visitor<number> { // one operation, all element types
visitBook() { return 0; } // books are tax-free here
visitElectronics(item: Electronics) { return Math.floor(item.price * 20 / 100); }
visitGrocery(item: Grocery) { return Math.floor(item.price * 5 / 100); }
}
class ShippingWeightVisitor implements Visitor<number> { // another operation, no item class changed
visitBook() { return 1; }
visitElectronics(item: Electronics) { return item.kg; }
visitGrocery(item: Grocery) { return item.kg + 1; } // cold packaging
}
const cart: Item[] = [new Book(2000), new Electronics(50000, 3), new Grocery(1200, 2)];
const sum = (visitor: Visitor<number>) => cart.reduce((total, item) => total + item.accept(visitor), 0);
console.log("tax:", sum(new TaxVisitor()), "cents");
console.log("shipping weight:", sum(new ShippingWeightVisitor()), "kg");
Output
tax: 10060 cents shipping weight: 7 kg
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Item> cart = List.of(new Book(2000), new Electronics(50000, 3), new Grocery(1200, 2));
System.out.println("tax: " + cart.stream().mapToInt(i -> i.accept(new TaxVisitor())).sum() + " cents");
System.out.println("shipping weight: " + cart.stream().mapToInt(i -> i.accept(new ShippingWeightVisitor())).sum() + " kg");
}
}
interface Visitor<T> {
T visitBook(Book item);
T visitElectronics(Electronics item);
T visitGrocery(Grocery item);
}
interface Item { <T> T accept(Visitor<T> visitor); }
record Book(int price) implements Item {
public <T> T accept(Visitor<T> v) { return v.visitBook(this); }
}
record Electronics(int price, int kg) implements Item {
public <T> T accept(Visitor<T> v) { return v.visitElectronics(this); }
}
record Grocery(int price, int kg) implements Item {
public <T> T accept(Visitor<T> v) { return v.visitGrocery(this); }
}
class TaxVisitor implements Visitor<Integer> { // one operation, all element types
public Integer visitBook(Book item) { return 0; } // books are tax-free here
public Integer visitElectronics(Electronics item) { return item.price() * 20 / 100; }
public Integer visitGrocery(Grocery item) { return item.price() * 5 / 100; }
}
class ShippingWeightVisitor implements Visitor<Integer> { // another operation, no item class changed
public Integer visitBook(Book item) { return 1; }
public Integer visitElectronics(Electronics item) { return item.kg(); }
public Integer visitGrocery(Grocery item) { return item.kg() + 1; } // cold packaging
}
Output
tax: 10060 cents shipping weight: 7 kg
Run this on your computer — see “Get Started” for setup.
<?php
interface Visitor {
public function visitBook(Book $item): int;
public function visitElectronics(Electronics $item): int;
public function visitGrocery(Grocery $item): int;
}
interface Item { public function accept(Visitor $visitor): int; }
final class Book implements Item {
public function __construct(public readonly int $price) {}
public function accept(Visitor $v): int { return $v->visitBook($this); }
}
final class Electronics implements Item {
public function __construct(public readonly int $price, public readonly int $kg) {}
public function accept(Visitor $v): int { return $v->visitElectronics($this); }
}
final class Grocery implements Item {
public function __construct(public readonly int $price, public readonly int $kg) {}
public function accept(Visitor $v): int { return $v->visitGrocery($this); }
}
class TaxVisitor implements Visitor { // one operation, all element types
public function visitBook(Book $item): int { return 0; } // books are tax-free here
public function visitElectronics(Electronics $item): int { return intdiv($item->price * 20, 100); }
public function visitGrocery(Grocery $item): int { return intdiv($item->price * 5, 100); }
}
class ShippingWeightVisitor implements Visitor { // another operation, no item class changed
public function visitBook(Book $item): int { return 1; }
public function visitElectronics(Electronics $item): int { return $item->kg; }
public function visitGrocery(Grocery $item): int { return $item->kg + 1; } // cold packaging
}
$cart = [new Book(2000), new Electronics(50000, 3), new Grocery(1200, 2)];
$sum = fn(Visitor $v) => array_sum(array_map(fn(Item $i) => $i->accept($v), $cart));
echo "tax: ", $sum(new TaxVisitor()), " cents\n";
echo "shipping weight: ", $sum(new ShippingWeightVisitor()), " kg\n";
Output
tax: 10060 cents shipping weight: 7 kg
The item classes stayed tiny. A fraud-risk report would be one new visitor class.
When to use it
- You have a stable set of element types but keep adding new operations over them: compilers and linters walking a syntax tree, document exporters (HTML, PDF, Markdown), reports over a fixed domain model.
- You want to keep unrelated operations (tax, export, rendering) out of the element classes.
When not to use it
- New element types are added often. Every visitor must then be updated.
- There are only one or two operations. Methods on the classes are simpler.
- Modern alternatives: pattern matching over sealed types (Java 21
switchwith sealed interfaces and records, TypeScript discriminated unions, Pythonmatch) often gives the same benefit with less ceremony.
You've already used it
- Compilers and linters: Babel plugins and ESLint rules are visitors over JavaScript syntax trees; Python's
ast.NodeVisitor; Java annotation processors. - Code formatters and transpilers: walk the same tree with different visitors (format, minify, type-check).
- Java:
FileVisitorwithFiles.walkFileTree().
Key points
- Visitor moves operations out of element classes into visitor objects.
accept(visitor)calls the visitor method for the element's own type (double dispatch).- New operations are cheap; new element types are expensive.
- Pattern matching on sealed types is a modern, lighter alternative.
- Further reading: Refactoring Guru: Visitor and Python's ast.NodeVisitor.
Practice questions
What is double dispatch in the Visitor pattern?
Answer
The code that runs depends on two types: the element's and the visitor's. The element's accept(visitor) calls visitor.visitBook(this), selecting the visitor method for its own type; which visitor object is passed selects the operation.
What is the main trade-off of the Visitor pattern?
Answer
Adding a new operation is easy: write a new visitor. Adding a new element type is hard: every existing visitor needs a new method. Use it when element types are stable and operations keep growing.
Where is the Visitor pattern commonly used?
Answer
Walking syntax trees in compilers, linters and transpilers: Babel plugins, ESLint rules, Python's ast.NodeVisitor. Also document exporters and Java's Files.walkFileTree() with a FileVisitor.
What modern language feature can replace the Visitor pattern?
Answer
Pattern matching over closed (sealed) type hierarchies: Java 21 switch with sealed interfaces and records, TypeScript discriminated unions with exhaustive switch, Python's match. The compiler can check every type is handled, without accept methods.
Open in the app to mark lessons complete, take notes and track your progress.