How to add a new operation to a family of classes without opening any of them, and the price you pay when a new kind of class arrives.
A shopping cart holds different kinds of thing: groceries, gadgets, books, and a gift set that holds a tea and a diary. Different teams want different things from the same cart. Finance wants the tax, and tax depends on the kind. Marketing wants the discount, which also depends on the kind. Operations wants a CSV of the lines, and then, next quarter, the shipping weight. The list of kinds is short and rarely changes. The list of questions you ask the cart never stops growing.
That is the problem the Visitor pattern is built for: new operations over a stable set of classes. Each operation becomes one class, a visitor, that has a method for every kind. The item classes learn a single trick, accept(visitor), and never change again. It is the pattern behind the "find", "disk usage" and "backup" follow-ups of the in-memory file system, and a pattern whose honest answer to "when would you use it?" is narrow.
This lesson starts with the code most people write first, a method per operation on every class, flips its costs with a visitor in Python, Java, C++ and JavaScript, and then sets the visitor against pattern matching.
Inspectors visiting a restaurant are Visitor: rooms just let them in, a new inspector needs no new room, and a new kind of room means updating every inspector.
The first version: every operation is a method on every item#
The first version puts each operation where the data is. A grocery knows its tax, its discount and its subtotal; so does a gadget; so does a book; so does a bundle, by asking its children:
# cart_v1.py: every operation is a method on every item class
class Grocery:
def __init__(self, name, price, qty):
self.name, self.price, self.qty = name, price, qty
def subtotal(self):
return self.price * self.qty
def tax(self): # 5 percent, rounded down
return self.subtotal() * 5 // 100
def discount(self): # 5 rupees off each unit when two or more are bought
return 5 * self.qty if self.qty >= 2 else 0
class Gadget:
def __init__(self, name, price, qty):
self.name, self.price, self.qty = name, price, qty
def subtotal(self):
return self.price * self.qty
def tax(self): # 18 percent, rounded down
return self.subtotal() * 18 // 100
def discount(self): # 10 percent off, rounded down
return self.subtotal() * 10 // 100
class Book:
def __init__(self, name, price, qty):
self.name, self.price, self.qty = name, price, qty
def subtotal(self):
return self.price * self.qty
def tax(self): # books carry no tax in this example
return 0
def discount(self):
return 0
class Bundle:
def __init__(self, name, items):
self.name, self.items = name, items
def subtotal(self):
return sum(i.subtotal() for i in self.items)
def tax(self):
return sum(i.tax() for i in self.items)
def discount(self):
return sum(i.discount() for i in self.items)
cart = [
Grocery("rice", 120, 2),
Gadget("earbuds", 1499, 1),
Book("atlas", 350, 1),
Bundle("gift set", [Grocery("tea", 150, 1), Book("diary", 200, 1)]),
]
print("subtotal", sum(i.subtotal() for i in cart))
print("tax", sum(i.tax() for i in cart))
print("discount", sum(i.discount() for i in cart))// CartV1.java: every operation is a method on every item class
import java.util.List;
class CartV1 {
interface Item {
int subtotal();
int tax();
int discount();
}
static final class Grocery implements Item {
final String name; final int price, qty;
Grocery(String name, int price, int qty) { this.name = name; this.price = price; this.qty = qty; }
public int subtotal() { return price * qty; }
public int tax() { return subtotal() * 5 / 100; } // 5 percent, rounded down
public int discount() { return qty >= 2 ? 5 * qty : 0; } // 5 rupees off each unit when two or more are bought
}
static final class Gadget implements Item {
final String name; final int price, qty;
Gadget(String name, int price, int qty) { this.name = name; this.price = price; this.qty = qty; }
public int subtotal() { return price * qty; }
public int tax() { return subtotal() * 18 / 100; } // 18 percent, rounded down
public int discount() { return subtotal() * 10 / 100; } // 10 percent off, rounded down
}
static final class Book implements Item {
final String name; final int price, qty;
Book(String name, int price, int qty) { this.name = name; this.price = price; this.qty = qty; }
public int subtotal() { return price * qty; }
public int tax() { return 0; } // books carry no tax in this example
public int discount() { return 0; }
}
static final class Bundle implements Item {
final String name; final List<Item> items;
Bundle(String name, List<Item> items) { this.name = name; this.items = items; }
public int subtotal() { return items.stream().mapToInt(Item::subtotal).sum(); }
public int tax() { return items.stream().mapToInt(Item::tax).sum(); }
public int discount() { return items.stream().mapToInt(Item::discount).sum(); }
}
public static void main(String[] args) {
List<Item> cart = List.of(
new Grocery("rice", 120, 2),
new Gadget("earbuds", 1499, 1),
new Book("atlas", 350, 1),
new Bundle("gift set", List.of(new Grocery("tea", 150, 1), new Book("diary", 200, 1))));
System.out.println("subtotal " + cart.stream().mapToInt(Item::subtotal).sum());
System.out.println("tax " + cart.stream().mapToInt(Item::tax).sum());
System.out.println("discount " + cart.stream().mapToInt(Item::discount).sum());
}
}// cart_v1.cpp: every operation is a method on every item class
#include <iostream>
#include <memory>
#include <numeric>
#include <string>
#include <utility>
#include <vector>
class Item {
public:
virtual ~Item() = default;
virtual int subtotal() const = 0;
virtual int tax() const = 0;
virtual int discount() const = 0;
};
class Grocery : public Item {
std::string name_;
int price_, qty_;
public:
Grocery(std::string name, int price, int qty) : name_(std::move(name)), price_(price), qty_(qty) {}
int subtotal() const override { return price_ * qty_; }
int tax() const override { return subtotal() * 5 / 100; } // 5 percent, rounded down
int discount() const override { return qty_ >= 2 ? 5 * qty_ : 0; } // 5 rupees off each unit when two or more are bought
};
class Gadget : public Item {
std::string name_;
int price_, qty_;
public:
Gadget(std::string name, int price, int qty) : name_(std::move(name)), price_(price), qty_(qty) {}
int subtotal() const override { return price_ * qty_; }
int tax() const override { return subtotal() * 18 / 100; } // 18 percent, rounded down
int discount() const override { return subtotal() * 10 / 100; } // 10 percent off, rounded down
};
class Book : public Item {
std::string name_;
int price_, qty_;
public:
Book(std::string name, int price, int qty) : name_(std::move(name)), price_(price), qty_(qty) {}
int subtotal() const override { return price_ * qty_; }
int tax() const override { return 0; } // books carry no tax in this example
int discount() const override { return 0; }
};
class Bundle : public Item {
std::string name_;
std::vector<std::unique_ptr<Item>> items_;
public:
Bundle(std::string name, std::vector<std::unique_ptr<Item>> items) : name_(std::move(name)), items_(std::move(items)) {}
int subtotal() const override { return sum([](const Item& i) { return i.subtotal(); }); }
int tax() const override { return sum([](const Item& i) { return i.tax(); }); }
int discount() const override { return sum([](const Item& i) { return i.discount(); }); }
private:
template <class F>
int sum(F f) const {
return std::accumulate(items_.begin(), items_.end(), 0, [&](int acc, const std::unique_ptr<Item>& i) { return acc + f(*i); });
}
};
int main() {
std::vector<std::unique_ptr<Item>> tail;
tail.push_back(std::make_unique<Grocery>("tea", 150, 1));
tail.push_back(std::make_unique<Book>("diary", 200, 1));
std::vector<std::unique_ptr<Item>> cart;
cart.push_back(std::make_unique<Grocery>("rice", 120, 2));
cart.push_back(std::make_unique<Gadget>("earbuds", 1499, 1));
cart.push_back(std::make_unique<Book>("atlas", 350, 1));
cart.push_back(std::make_unique<Bundle>("gift set", std::move(tail)));
int subtotal = 0, tax = 0, discount = 0;
for (const auto& i : cart) {
subtotal += i->subtotal();
tax += i->tax();
discount += i->discount();
}
std::cout << "subtotal " << subtotal << "\n";
std::cout << "tax " << tax << "\n";
std::cout << "discount " << discount << "\n";
}// cart_v1.js: every operation is a method on every item class
class Grocery {
constructor(name, price, qty) {
Object.assign(this, { name, price, qty });
}
subtotal() {
return this.price * this.qty;
}
tax() {
return Math.floor((this.subtotal() * 5) / 100); // 5 percent, rounded down
}
discount() {
return this.qty >= 2 ? 5 * this.qty : 0; // 5 rupees off each unit when two or more are bought
}
}
class Gadget {
constructor(name, price, qty) {
Object.assign(this, { name, price, qty });
}
subtotal() {
return this.price * this.qty;
}
tax() {
return Math.floor((this.subtotal() * 18) / 100); // 18 percent, rounded down
}
discount() {
return Math.floor((this.subtotal() * 10) / 100); // 10 percent off, rounded down
}
}
class Book {
constructor(name, price, qty) {
Object.assign(this, { name, price, qty });
}
subtotal() {
return this.price * this.qty;
}
tax() {
return 0; // books carry no tax in this example
}
discount() {
return 0;
}
}
class Bundle {
constructor(name, items) {
Object.assign(this, { name, items });
}
subtotal() {
return this.items.reduce((sum, i) => sum + i.subtotal(), 0);
}
tax() {
return this.items.reduce((sum, i) => sum + i.tax(), 0);
}
discount() {
return this.items.reduce((sum, i) => sum + i.discount(), 0);
}
}
const cart = [
new Grocery("rice", 120, 2),
new Gadget("earbuds", 1499, 1),
new Book("atlas", 350, 1),
new Bundle("gift set", [new Grocery("tea", 150, 1), new Book("diary", 200, 1)]),
];
const total = (method) => cart.reduce((sum, i) => sum + i[method](), 0);
console.log("subtotal", total("subtotal"));
console.log("tax", total("tax"));
console.log("discount", total("discount"));subtotal 2439
tax 288
discount 159The rules, so the numbers make sense. Tax is a percentage of the line total, rounded down: 5 for groceries, 18 for gadgets, none for books. The discount is 5 rupees off each unit when two or more groceries are bought, and 10 percent off a gadget, rounded down. The rice is 120 × 2 = 240, so its tax is 12 and its discount is 10; the earbuds are 1,499, so the tax is 269 and the discount 149. Add the atlas (350, no tax), and the gift set, a tea at 150 and a diary at 200, whose only tax is 7 on the tea. The tax is 12 + 269 + 0 + 7 = 288, the discount is 10 + 149 = 159, and the subtotal is 240 + 1,499 + 350 + 350 = 2,439. Money is a whole number, never a float.
Nothing is wrong yet, and this is a good design for what it does well: adding a fifth kind of item is one new class, and everything about that kind sits in one place. The trouble is the other direction:
- Every new operation edits every class. A shipping-weight method goes into the grocery, the gadget, the book and the bundle. In Java and C++ the interface changes too, so every class that implements it stops compiling until it has the method.
- Item classes learn things that are not theirs. A grocery now knows tax law, discount campaigns and the layout of a CSV file. Finance, marketing and operations all edit the same four files.
- The operations are scattered. To read how tax works, you open every class and read one method in each.
- A new operation cannot be added from outside. If the item classes live in another team's library, you cannot add a method to them at all.
Which change is cheap?
In the first version, which is cheaper: adding a new operation (the shipping weight) or adding a new kind of item (a gift card)? How many classes does each one touch?
One strong answer
A new kind is cheap; a new operation is expensive. A gift card is one new class with its own tax, discount and subtotal, so nothing else changes. The shipping weight is a new method in all four existing classes. The Visitor pattern flips this: a new operation becomes one new class, and a new kind becomes the expensive change. Choose by asking which list grows.
The pattern#
The book that named the pattern (Design Patterns, 1994, by Gamma, Helm, Johnson and Vlissides) states its intent in two sentences: "Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates."
In the cart that gives five roles:
| Role | In the cart | Its one job |
|---|---|---|
| Visitor (the interface) | Visitor with visit_grocery, visit_gadget, visit_book, visit_bundle |
Have one method for every kind of element |
| Concrete visitors | Subtotal, Tax, Discount, Export, Weight |
Each is one operation, written once, covering every kind |
| Element (the interface) | Item with accept(visitor) |
Let a visitor in |
| Concrete elements | Grocery, Gadget, Book, Bundle |
Hand themselves to the matching visit_ method, and nothing else |
| Object structure | the cart (a list of items, with bundles nested inside) | Be what the visitors walk over |
%% title: Visitor: operations over a cart
classDiagram
class Item {
<<interface>>
+accept(visitor)
}
class Grocery {
+accept(visitor)
}
class Gadget {
+accept(visitor)
}
class Book {
+accept(visitor)
}
class Bundle {
+items list
+accept(visitor)
}
class Visitor {
<<interface>>
+visit_grocery(g)
+visit_gadget(g)
+visit_book(b)
+visit_bundle(b)
}
class Tax {
+total int
}
class Discount {
+total int
}
class Export {
+lines list
}
Item <|.. Grocery
Item <|.. Gadget
Item <|.. Book
Item <|.. Bundle
Bundle o-- Item : holds
Visitor <|.. Tax
Visitor <|.. Discount
Visitor <|.. Export
Item ..> Visitor : calls visit_x
Visitor ..> Item : readsRead the arrows. The two dotted arrows between Item and Visitor form a loop, and the loop is the pattern. The elements know the visitor interface, and call exactly one of its methods, the one named for their own kind. The visitor interface knows every kind of element. That is why a new operation is cheap (a new class on the right-hand side, with no arrow from an element to it) and a new kind is expensive (a new method in Visitor, and so in every visitor under it).
Double dispatch#
Say the cart holds a gadget and you call item.accept(tax). Two choices happen, one after the other:
- The first choice is the element's.
item.acceptruns theacceptof whateveritemreally is: the gadget's. That is ordinary polymorphism. - The second choice is the visitor's. The gadget's
acceptcallstax.visit_gadget(self). The method name says which kind it is, andtaxsupplies its own version of that method.
After those two steps the code is inside Tax.visit_gadget, and it knows the exact type of its argument without an isinstance anywhere. That is double dispatch: the code that runs depends on two objects, the element and the visitor.
Why not skip accept and write visitor.visit(item) with one overloaded method per kind? In Java and C++ the compiler picks an overload from the declared type of the argument. A variable declared as Item matches no visit(Item), so the call does not compile, and a cast would be the instanceof ladder again. accept is how the object's real class gets back into the choice. (Python has no overloading at all; its functools.singledispatchmethod picks a method by the argument's real class, so in Python it can stand in for accept.)
Calling item.accept(tax) makes two choices in a row: the item's own accept runs, and it calls the tax visitor's method for its own kind.
The code#
Each item class is now three lines: it stores its data and hands itself to one visit_ method. Each operation is a visitor with the four methods. run hands every item in a list to a visitor and returns the visitor, which holds the answer.
# cart_visitor.py: the item classes only say "visit me"; each operation is a visitor
from abc import ABC, abstractmethod
class Item(ABC):
@abstractmethod
def accept(self, visitor): ...
class Grocery(Item):
def __init__(self, name, price, qty):
self.name, self.price, self.qty = name, price, qty
def accept(self, visitor):
return visitor.visit_grocery(self)
class Gadget(Item):
def __init__(self, name, price, qty):
self.name, self.price, self.qty = name, price, qty
def accept(self, visitor):
return visitor.visit_gadget(self)
class Book(Item):
def __init__(self, name, price, qty):
self.name, self.price, self.qty = name, price, qty
def accept(self, visitor):
return visitor.visit_book(self)
class Bundle(Item):
def __init__(self, name, items):
self.name, self.items = name, items
def accept(self, visitor):
return visitor.visit_bundle(self)
class Visitor(ABC):
"""One method per kind of item: this list is what makes a new kind expensive."""
@abstractmethod
def visit_grocery(self, g): ...
@abstractmethod
def visit_gadget(self, g): ...
@abstractmethod
def visit_book(self, b): ...
@abstractmethod
def visit_bundle(self, b): ...
class Subtotal(Visitor):
def __init__(self):
self.total = 0
def _line(self, item):
self.total += item.price * item.qty
visit_grocery = visit_gadget = visit_book = _line
def visit_bundle(self, b):
for child in b.items: # the visitor decides to walk into a bundle
child.accept(self)
class Tax(Visitor):
RATES = {"grocery": 5, "gadget": 18, "book": 0} # percent, rounded down per line
def __init__(self):
self.total = 0
def visit_grocery(self, g):
self.total += g.price * g.qty * self.RATES["grocery"] // 100
def visit_gadget(self, g):
self.total += g.price * g.qty * self.RATES["gadget"] // 100
def visit_book(self, b):
self.total += b.price * b.qty * self.RATES["book"] // 100
def visit_bundle(self, b):
for child in b.items:
child.accept(self)
class Discount(Visitor):
def __init__(self):
self.total = 0
def visit_grocery(self, g): # 5 rupees off each unit when two or more are bought
self.total += 5 * g.qty if g.qty >= 2 else 0
def visit_gadget(self, g): # 10 percent off, rounded down
self.total += g.price * g.qty * 10 // 100
def visit_book(self, b):
pass
def visit_bundle(self, b):
for child in b.items:
child.accept(self)
class Export(Visitor):
def __init__(self):
self.lines, self.depth = [], 0
def _row(self, kind, name, qty, total):
self.lines.append(" " * self.depth + f"{kind},{name},{qty},{total}")
def visit_grocery(self, g):
self._row("grocery", g.name, g.qty, g.price * g.qty)
def visit_gadget(self, g):
self._row("gadget", g.name, g.qty, g.price * g.qty)
def visit_book(self, b):
self._row("book", b.name, b.qty, b.price * b.qty)
def visit_bundle(self, b):
self._row("bundle", b.name, 1, run(b.items, Subtotal()).total)
self.depth += 1
for child in b.items:
child.accept(self)
self.depth -= 1
def run(items, visitor):
for item in items:
item.accept(visitor)
return visitor
# Added last, in another file in a real project: no item class is touched.
class Weight(Visitor):
GRAMS = {"grocery": 500, "gadget": 200, "book": 400} # per unit
def __init__(self):
self.total = 0
def visit_grocery(self, g):
self.total += self.GRAMS["grocery"] * g.qty
def visit_gadget(self, g):
self.total += self.GRAMS["gadget"] * g.qty
def visit_book(self, b):
self.total += self.GRAMS["book"] * b.qty
def visit_bundle(self, b):
for child in b.items:
child.accept(self)
cart = [
Grocery("rice", 120, 2),
Gadget("earbuds", 1499, 1),
Book("atlas", 350, 1),
Bundle("gift set", [Grocery("tea", 150, 1), Book("diary", 200, 1)]),
]
subtotal, tax, discount = (run(cart, v).total for v in (Subtotal(), Tax(), Discount()))
print("subtotal", subtotal)
print("tax", tax)
print("discount", discount)
print("to pay", subtotal - discount + tax)
print("export")
print("\n".join(run(cart, Export()).lines))
print("weight in grams", run(cart, Weight()).total)// CartVisitor.java: the item classes only say "visit me"; each operation is a visitor
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
class CartVisitor {
/** One method per kind of item: this list is what makes a new kind expensive. */
interface Visitor {
void visitGrocery(Grocery g);
void visitGadget(Gadget g);
void visitBook(Book b);
void visitBundle(Bundle b);
}
interface Item {
void accept(Visitor visitor);
}
static final class Grocery implements Item {
final String name; final int price, qty;
Grocery(String name, int price, int qty) { this.name = name; this.price = price; this.qty = qty; }
public void accept(Visitor v) { v.visitGrocery(this); }
}
static final class Gadget implements Item {
final String name; final int price, qty;
Gadget(String name, int price, int qty) { this.name = name; this.price = price; this.qty = qty; }
public void accept(Visitor v) { v.visitGadget(this); }
}
static final class Book implements Item {
final String name; final int price, qty;
Book(String name, int price, int qty) { this.name = name; this.price = price; this.qty = qty; }
public void accept(Visitor v) { v.visitBook(this); }
}
static final class Bundle implements Item {
final String name; final List<Item> items;
Bundle(String name, List<Item> items) { this.name = name; this.items = items; }
public void accept(Visitor v) { v.visitBundle(this); }
}
/** Visitors that only walk into a bundle share this base. */
static abstract class Walker implements Visitor {
public void visitBundle(Bundle b) {
for (Item child : b.items) child.accept(this); // the visitor decides to walk into a bundle
}
}
static final class Subtotal extends Walker {
int total;
public void visitGrocery(Grocery g) { total += g.price * g.qty; }
public void visitGadget(Gadget g) { total += g.price * g.qty; }
public void visitBook(Book b) { total += b.price * b.qty; }
}
static final class Tax extends Walker {
static final Map<String, Integer> RATES = Map.of("grocery", 5, "gadget", 18, "book", 0); // percent, rounded down per line
int total;
public void visitGrocery(Grocery g) { total += g.price * g.qty * RATES.get("grocery") / 100; }
public void visitGadget(Gadget g) { total += g.price * g.qty * RATES.get("gadget") / 100; }
public void visitBook(Book b) { total += b.price * b.qty * RATES.get("book") / 100; }
}
static final class Discount extends Walker {
int total;
public void visitGrocery(Grocery g) { total += g.qty >= 2 ? 5 * g.qty : 0; } // 5 rupees off each unit when two or more are bought
public void visitGadget(Gadget g) { total += g.price * g.qty * 10 / 100; } // 10 percent off, rounded down
public void visitBook(Book b) {}
}
static final class Export implements Visitor {
final List<String> lines = new ArrayList<>();
int depth;
private void row(String kind, String name, int qty, int total) {
lines.add(" ".repeat(depth) + kind + "," + name + "," + qty + "," + total);
}
public void visitGrocery(Grocery g) { row("grocery", g.name, g.qty, g.price * g.qty); }
public void visitGadget(Gadget g) { row("gadget", g.name, g.qty, g.price * g.qty); }
public void visitBook(Book b) { row("book", b.name, b.qty, b.price * b.qty); }
public void visitBundle(Bundle b) {
Subtotal s = new Subtotal();
run(b.items, s);
row("bundle", b.name, 1, s.total);
depth++;
for (Item child : b.items) child.accept(this);
depth--;
}
}
static <V extends Visitor> V run(List<Item> items, V visitor) {
for (Item item : items) item.accept(visitor);
return visitor;
}
// Added last, in another file in a real project: no item class is touched.
static final class Weight extends Walker {
static final Map<String, Integer> GRAMS = Map.of("grocery", 500, "gadget", 200, "book", 400); // per unit
int total;
public void visitGrocery(Grocery g) { total += GRAMS.get("grocery") * g.qty; }
public void visitGadget(Gadget g) { total += GRAMS.get("gadget") * g.qty; }
public void visitBook(Book b) { total += GRAMS.get("book") * b.qty; }
}
public static void main(String[] args) {
List<Item> cart = List.of(
new Grocery("rice", 120, 2),
new Gadget("earbuds", 1499, 1),
new Book("atlas", 350, 1),
new Bundle("gift set", List.of(new Grocery("tea", 150, 1), new Book("diary", 200, 1))));
int subtotal = run(cart, new Subtotal()).total;
int tax = run(cart, new Tax()).total;
int discount = run(cart, new Discount()).total;
System.out.println("subtotal " + subtotal);
System.out.println("tax " + tax);
System.out.println("discount " + discount);
System.out.println("to pay " + (subtotal - discount + tax));
System.out.println("export");
System.out.println(String.join("\n", run(cart, new Export()).lines));
System.out.println("weight in grams " + run(cart, new Weight()).total);
}
}// cart_visitor.cpp: the item classes only say "visit me"; each operation is a visitor
#include <iostream>
#include <map>
#include <memory>
#include <string>
#include <utility>
#include <vector>
class Grocery;
class Gadget;
class Book;
class Bundle;
// One method per kind of item: this list is what makes a new kind expensive.
class Visitor {
public:
virtual ~Visitor() = default;
virtual void visitGrocery(const Grocery& g) = 0;
virtual void visitGadget(const Gadget& g) = 0;
virtual void visitBook(const Book& b) = 0;
virtual void visitBundle(const Bundle& b) = 0;
};
class Item {
public:
virtual ~Item() = default;
virtual void accept(Visitor& visitor) const = 0;
};
using Items = std::vector<std::unique_ptr<Item>>;
struct Line {
std::string name;
int price, qty;
};
class Grocery : public Item {
public:
Line line;
Grocery(std::string name, int price, int qty) : line{std::move(name), price, qty} {}
void accept(Visitor& v) const override { v.visitGrocery(*this); }
};
class Gadget : public Item {
public:
Line line;
Gadget(std::string name, int price, int qty) : line{std::move(name), price, qty} {}
void accept(Visitor& v) const override { v.visitGadget(*this); }
};
class Book : public Item {
public:
Line line;
Book(std::string name, int price, int qty) : line{std::move(name), price, qty} {}
void accept(Visitor& v) const override { v.visitBook(*this); }
};
class Bundle : public Item {
public:
std::string name;
Items items;
Bundle(std::string n, Items i) : name(std::move(n)), items(std::move(i)) {}
void accept(Visitor& v) const override { v.visitBundle(*this); }
};
// Visitors that only walk into a bundle share this base.
class Walker : public Visitor {
public:
void visitBundle(const Bundle& b) override {
for (const auto& child : b.items) child->accept(*this); // the visitor decides to walk into a bundle
}
};
class Subtotal : public Walker {
public:
int total = 0;
void visitGrocery(const Grocery& g) override { total += g.line.price * g.line.qty; }
void visitGadget(const Gadget& g) override { total += g.line.price * g.line.qty; }
void visitBook(const Book& b) override { total += b.line.price * b.line.qty; }
};
class Tax : public Walker {
std::map<std::string, int> rates_{{"grocery", 5}, {"gadget", 18}, {"book", 0}}; // percent, rounded down per line
public:
int total = 0;
void visitGrocery(const Grocery& g) override { total += g.line.price * g.line.qty * rates_["grocery"] / 100; }
void visitGadget(const Gadget& g) override { total += g.line.price * g.line.qty * rates_["gadget"] / 100; }
void visitBook(const Book& b) override { total += b.line.price * b.line.qty * rates_["book"] / 100; }
};
class Discount : public Walker {
public:
int total = 0;
void visitGrocery(const Grocery& g) override { total += g.line.qty >= 2 ? 5 * g.line.qty : 0; } // 5 rupees off each unit when two or more are bought
void visitGadget(const Gadget& g) override { total += g.line.price * g.line.qty * 10 / 100; } // 10 percent off, rounded down
void visitBook(const Book&) override {}
};
template <class V>
V& run(const Items& items, V& visitor) {
for (const auto& item : items) item->accept(visitor);
return visitor;
}
class Export : public Visitor {
int depth_ = 0;
void row(const std::string& kind, const std::string& name, int qty, int total) {
lines.push_back(std::string(depth_ * 2, ' ') + kind + "," + name + "," + std::to_string(qty) + "," + std::to_string(total));
}
public:
std::vector<std::string> lines;
void visitGrocery(const Grocery& g) override { row("grocery", g.line.name, g.line.qty, g.line.price * g.line.qty); }
void visitGadget(const Gadget& g) override { row("gadget", g.line.name, g.line.qty, g.line.price * g.line.qty); }
void visitBook(const Book& b) override { row("book", b.line.name, b.line.qty, b.line.price * b.line.qty); }
void visitBundle(const Bundle& b) override {
Subtotal s;
run(b.items, s);
row("bundle", b.name, 1, s.total);
depth_++;
for (const auto& child : b.items) child->accept(*this);
depth_--;
}
};
// Added last, in another file in a real project: no item class is touched.
class Weight : public Walker {
std::map<std::string, int> grams_{{"grocery", 500}, {"gadget", 200}, {"book", 400}}; // per unit
public:
int total = 0;
void visitGrocery(const Grocery& g) override { total += grams_["grocery"] * g.line.qty; }
void visitGadget(const Gadget& g) override { total += grams_["gadget"] * g.line.qty; }
void visitBook(const Book& b) override { total += grams_["book"] * b.line.qty; }
};
int main() {
Items gift;
gift.push_back(std::make_unique<Grocery>("tea", 150, 1));
gift.push_back(std::make_unique<Book>("diary", 200, 1));
Items cart;
cart.push_back(std::make_unique<Grocery>("rice", 120, 2));
cart.push_back(std::make_unique<Gadget>("earbuds", 1499, 1));
cart.push_back(std::make_unique<Book>("atlas", 350, 1));
cart.push_back(std::make_unique<Bundle>("gift set", std::move(gift)));
Subtotal subtotalV;
Tax taxV;
Discount discountV;
int subtotal = run(cart, subtotalV).total, tax = run(cart, taxV).total, discount = run(cart, discountV).total;
std::cout << "subtotal " << subtotal << "\n";
std::cout << "tax " << tax << "\n";
std::cout << "discount " << discount << "\n";
std::cout << "to pay " << subtotal - discount + tax << "\n";
std::cout << "export\n";
Export exportV;
for (const auto& l : run(cart, exportV).lines) std::cout << l << "\n";
Weight weightV;
std::cout << "weight in grams " << run(cart, weightV).total << "\n";
}// cart_visitor.js: the item classes only say "visit me"; each operation is a visitor
class Grocery {
constructor(name, price, qty) {
Object.assign(this, { name, price, qty });
}
accept(visitor) {
return visitor.visitGrocery(this);
}
}
class Gadget {
constructor(name, price, qty) {
Object.assign(this, { name, price, qty });
}
accept(visitor) {
return visitor.visitGadget(this);
}
}
class Book {
constructor(name, price, qty) {
Object.assign(this, { name, price, qty });
}
accept(visitor) {
return visitor.visitBook(this);
}
}
class Bundle {
constructor(name, items) {
Object.assign(this, { name, items });
}
accept(visitor) {
return visitor.visitBundle(this);
}
}
// No interfaces: a visitor is any object with these four methods. Test every visitor against all four.
class Walker {
visitBundle(b) {
for (const child of b.items) child.accept(this); // the visitor decides to walk into a bundle
}
}
class Subtotal extends Walker {
total = 0;
#line(item) {
this.total += item.price * item.qty;
}
visitGrocery(g) { this.#line(g); }
visitGadget(g) { this.#line(g); }
visitBook(b) { this.#line(b); }
}
class Tax extends Walker {
static RATES = { grocery: 5, gadget: 18, book: 0 }; // percent, rounded down per line
total = 0;
visitGrocery(g) {
this.total += Math.floor((g.price * g.qty * Tax.RATES.grocery) / 100);
}
visitGadget(g) {
this.total += Math.floor((g.price * g.qty * Tax.RATES.gadget) / 100);
}
visitBook(b) {
this.total += Math.floor((b.price * b.qty * Tax.RATES.book) / 100);
}
}
class Discount extends Walker {
total = 0;
visitGrocery(g) {
this.total += g.qty >= 2 ? 5 * g.qty : 0; // 5 rupees off each unit when two or more are bought
}
visitGadget(g) {
this.total += Math.floor((g.price * g.qty * 10) / 100); // 10 percent off, rounded down
}
visitBook() {}
}
class Export {
lines = [];
depth = 0;
#row(kind, name, qty, total) {
this.lines.push(" ".repeat(this.depth) + `${kind},${name},${qty},${total}`);
}
visitGrocery(g) { this.#row("grocery", g.name, g.qty, g.price * g.qty); }
visitGadget(g) { this.#row("gadget", g.name, g.qty, g.price * g.qty); }
visitBook(b) { this.#row("book", b.name, b.qty, b.price * b.qty); }
visitBundle(b) {
this.#row("bundle", b.name, 1, run(b.items, new Subtotal()).total);
this.depth++;
for (const child of b.items) child.accept(this);
this.depth--;
}
}
function run(items, visitor) {
for (const item of items) item.accept(visitor);
return visitor;
}
// Added last, in another file in a real project: no item class is touched.
class Weight extends Walker {
static GRAMS = { grocery: 500, gadget: 200, book: 400 }; // per unit
total = 0;
visitGrocery(g) {
this.total += Weight.GRAMS.grocery * g.qty;
}
visitGadget(g) {
this.total += Weight.GRAMS.gadget * g.qty;
}
visitBook(b) {
this.total += Weight.GRAMS.book * b.qty;
}
}
const cart = [
new Grocery("rice", 120, 2),
new Gadget("earbuds", 1499, 1),
new Book("atlas", 350, 1),
new Bundle("gift set", [new Grocery("tea", 150, 1), new Book("diary", 200, 1)]),
];
const [subtotal, tax, discount] = [new Subtotal(), new Tax(), new Discount()].map((v) => run(cart, v).total);
console.log("subtotal", subtotal);
console.log("tax", tax);
console.log("discount", discount);
console.log("to pay", subtotal - discount + tax);
console.log("export");
console.log(run(cart, new Export()).lines.join("\n"));
console.log("weight in grams", run(cart, new Weight()).total);All four print the same thing:
subtotal 2439
tax 288
discount 159
to pay 2568
export
grocery,rice,2,240
gadget,earbuds,1,1499
book,atlas,1,350
bundle,gift set,1,350
grocery,tea,1,150
book,diary,1,200
weight in grams 2500Check the numbers by hand. The tax is the same 288 as in the first version, because the rates are the same; only where they live has changed. To pay is 2,439 − 159 + 288 = 2,568. The export has one row per item, and the bundle's row shows 350, the sum of its two children (150 + 200), with the children indented below it. The weight is 2 rice at 500 g, 1 pair of earbuds at 200 g, the atlas at 400 g and a gift set of a 500 g tea and a 400 g diary: 1,000 + 200 + 400 + 900 = 2,500 g.
The Weight visitor is the point of the lesson: written last, with none of the four item classes opened. It could live in another team's repository.
What each language brings to the same design:
- Python:
ItemandVisitorare abstract base classes, so a visitor that forgets one of the four methods cannot even be created. The rates sit in a class dictionary, so the numbers are data and the methods are one line each. - Java: two interfaces, and
Walker, a small base class for the visitors that only need to walk into a bundle. A visitor that misses a method does not compile. - C++: the element classes need the visitor declared before them, so the
Visitorinterface names the four kinds first.runis a template, so each call returns the exact visitor type and itstotalcan be read directly. The methods are pure virtual, so creating a visitor that misses one is a compile error. - JavaScript: no interfaces, so a visitor is any object with the four methods. A missing one fails with
is not a functiononly when an item of that kind is visited, so test each visitor against every kind.
Who walks the structure?#
A bundle holds items, so something has to go into it. There are two places to put the loop, and the lesson's code makes a choice.
In the visitor (this lesson)
visit_bundleloops over the children itself- The visitor can skip children, reorder them, or carry state down
Exportadds 1 to its depth before the children and takes it back after, so they are indented- Each visitor repeats a small loop, or shares a base class
In the element
Bundle.acceptcalls the visitor, then each child'saccept- Visitors are shorter and cannot forget to walk
- The visitor cannot stop early or add anything between a parent and its children
- Every visitor sees the same order
The export is the reason for the first choice. The depth of a row is not a property of any item: it is a fact about the walk, and only the visitor that is doing the walk can keep it. If your visitors never need that, the second choice is simpler. The composite lesson has the tree, and visitors are how you run operations over it.
Two more practical facts. A visitor usually holds the answer (total, lines), so make a new one for each run: a reused visitor adds the second run to the first, and a shared one shares its total between threads. Or each visit_ method can return a value (in Java, a generic Visitor<R>), which is safer for threads; holding state is simpler when the operation accumulates, as tax and export do.
A visitor that loops over a bundle's children controls the walk, so Export can indent them; a loop inside the element is shorter but gives that control away.
Visitor or pattern matching?#
Visitor is a way to get a different visit_ method for each kind. Most languages now have a lighter way to ask what kind a value is. The second program in this lesson does not use a visitor: it keeps the data plain, and each operation is a function that checks the kind.
# cart_matching.py: no visitor: plain data, and a function per operation that checks the kind
from dataclasses import dataclass
from typing import List, Union
@dataclass(frozen=True)
class Grocery:
name: str
price: int
qty: int
@dataclass(frozen=True)
class Gadget:
name: str
price: int
qty: int
@dataclass(frozen=True)
class Book:
name: str
price: int
qty: int
@dataclass(frozen=True)
class Bundle:
name: str
items: List["Item"]
Item = Union[Grocery, Gadget, Book, Bundle]
def tax(item: Item) -> int:
if isinstance(item, Grocery):
return item.price * item.qty * 5 // 100
if isinstance(item, Gadget):
return item.price * item.qty * 18 // 100
if isinstance(item, Book):
return 0
if isinstance(item, Bundle):
return sum(tax(i) for i in item.items)
raise TypeError(f"unknown item: {type(item).__name__}") # only found when this line runs
def weight(item: Item) -> int:
if isinstance(item, Grocery):
return 500 * item.qty
if isinstance(item, Gadget):
return 200 * item.qty
if isinstance(item, Book):
return 400 * item.qty
if isinstance(item, Bundle):
return sum(weight(i) for i in item.items)
raise TypeError(f"unknown item: {type(item).__name__}")
cart = [
Grocery("rice", 120, 2),
Gadget("earbuds", 1499, 1),
Book("atlas", 350, 1),
Bundle("gift set", [Grocery("tea", 150, 1), Book("diary", 200, 1)]),
]
print("tax", sum(tax(i) for i in cart))
print("weight in grams", sum(weight(i) for i in cart))// CartMatching.java: no visitor: plain records, and a method per operation that checks the kind
import java.util.List;
class CartMatching {
sealed interface Item permits Grocery, Gadget, Book, Bundle {}
record Grocery(String name, int price, int qty) implements Item {}
record Gadget(String name, int price, int qty) implements Item {}
record Book(String name, int price, int qty) implements Item {}
record Bundle(String name, List<Item> items) implements Item {}
// Java 17 checks the kind with instanceof. From Java 21, a switch over the sealed type
// is checked by the compiler: a missing kind is a compile error, not a runtime one.
static int tax(Item item) {
if (item instanceof Grocery g) return g.price() * g.qty() * 5 / 100;
if (item instanceof Gadget g) return g.price() * g.qty() * 18 / 100;
if (item instanceof Book) return 0;
if (item instanceof Bundle b) return b.items().stream().mapToInt(CartMatching::tax).sum();
throw new IllegalArgumentException("unknown item: " + item.getClass().getSimpleName());
}
static int weight(Item item) {
if (item instanceof Grocery g) return 500 * g.qty();
if (item instanceof Gadget g) return 200 * g.qty();
if (item instanceof Book b) return 400 * b.qty();
if (item instanceof Bundle b) return b.items().stream().mapToInt(CartMatching::weight).sum();
throw new IllegalArgumentException("unknown item: " + item.getClass().getSimpleName());
}
public static void main(String[] args) {
List<Item> cart = List.of(
new Grocery("rice", 120, 2),
new Gadget("earbuds", 1499, 1),
new Book("atlas", 350, 1),
new Bundle("gift set", List.of(new Grocery("tea", 150, 1), new Book("diary", 200, 1))));
System.out.println("tax " + cart.stream().mapToInt(CartMatching::tax).sum());
System.out.println("weight in grams " + cart.stream().mapToInt(CartMatching::weight).sum());
}
}// cart_matching.cpp: no visitor: plain structs in a std::variant, and std::visit per operation
#include <iostream>
#include <string>
#include <variant>
#include <vector>
struct Grocery { std::string name; int price, qty; };
struct Gadget { std::string name; int price, qty; };
struct Book { std::string name; int price, qty; };
struct Bundle;
using Item = std::variant<Grocery, Gadget, Book, Bundle>;
struct Bundle { std::string name; std::vector<Item> items; };
template <class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template <class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
// std::visit needs a handler for every kind in the variant: leave one out and this does not compile.
int tax(const Item& item) {
return std::visit(overloaded{
[](const Grocery& g) { return g.price * g.qty * 5 / 100; },
[](const Gadget& g) { return g.price * g.qty * 18 / 100; },
[](const Book&) { return 0; },
[](const Bundle& b) { int sum = 0; for (const Item& i : b.items) sum += tax(i); return sum; },
}, item);
}
int weight(const Item& item) {
return std::visit(overloaded{
[](const Grocery& g) { return 500 * g.qty; },
[](const Gadget& g) { return 200 * g.qty; },
[](const Book& b) { return 400 * b.qty; },
[](const Bundle& b) { int sum = 0; for (const Item& i : b.items) sum += weight(i); return sum; },
}, item);
}
int main() {
std::vector<Item> cart{
Grocery{"rice", 120, 2},
Gadget{"earbuds", 1499, 1},
Book{"atlas", 350, 1},
Bundle{"gift set", {Grocery{"tea", 150, 1}, Book{"diary", 200, 1}}},
};
int taxTotal = 0, grams = 0;
for (const Item& i : cart) {
taxTotal += tax(i);
grams += weight(i);
}
std::cout << "tax " << taxTotal << "\n";
std::cout << "weight in grams " << grams << "\n";
}// cart_matching.js: no visitor: plain objects with a kind, and a function per operation that switches on it
const grocery = (name, price, qty) => ({ kind: "grocery", name, price, qty });
const gadget = (name, price, qty) => ({ kind: "gadget", name, price, qty });
const book = (name, price, qty) => ({ kind: "book", name, price, qty });
const bundle = (name, items) => ({ kind: "bundle", name, items });
function tax(item) {
switch (item.kind) {
case "grocery": return Math.floor((item.price * item.qty * 5) / 100);
case "gadget": return Math.floor((item.price * item.qty * 18) / 100);
case "book": return 0;
case "bundle": return item.items.reduce((sum, i) => sum + tax(i), 0);
default: throw new Error(`unknown item: ${item.kind}`); // only found when this line runs
}
}
function weight(item) {
switch (item.kind) {
case "grocery": return 500 * item.qty;
case "gadget": return 200 * item.qty;
case "book": return 400 * item.qty;
case "bundle": return item.items.reduce((sum, i) => sum + weight(i), 0);
default: throw new Error(`unknown item: ${item.kind}`);
}
}
const cart = [
grocery("rice", 120, 2),
gadget("earbuds", 1499, 1),
book("atlas", 350, 1),
bundle("gift set", [grocery("tea", 150, 1), book("diary", 200, 1)]),
];
console.log("tax", cart.reduce((sum, i) => sum + tax(i), 0));
console.log("weight in grams", cart.reduce((sum, i) => sum + weight(i), 0));All four print the same thing:
tax 288
weight in grams 2500Python 3.10 added structural pattern matching (match), but this lesson's code runs on 3.9, so it uses isinstance; either way Python checks nothing until the line runs, though a type checker such as mypy can flag a missing case. The Java version uses instanceof patterns, and from Java 21 a switch over the sealed interface is checked by the compiler. C++ uses std::variant and std::visit, and JavaScript a switch on a kind field.
The two designs make opposite things cheap, which is the heart of the pattern:
| Methods on items | Visitor | Functions that check the kind | |
|---|---|---|---|
| Add an operation (weight) | Edit every class | One new class, no edits | One new function, no edits |
| Add a kind (a gift card) | One new class | A new method in Visitor, and in every visitor |
A new case in every function |
| Does the compiler tell you what you missed? | Java and C++: yes | Java and C++: yes. Python: when the visitor is created. JavaScript: when that kind is visited | C++ std::visit and Java 21 switch over a sealed type: yes. Python and JavaScript: when that line runs |
| The kinds must be… | Anything | A fixed set the visitor interface can list | A fixed set, as a variant, a sealed type or a kind field |
Needs accept on every class |
No | Yes | No |
The compiler rows were tested: deleting the Gadget handler from the C++ std::visit stops it compiling, and so does a Java switch over the sealed type with a case missing. In Python and JavaScript the last raise or default: is reached only when an unknown kind turns up, which is when a user finds it.
So which one? When your language has sum types and a checked match, the plain function is often the better answer, because it needs no accept, no interface with a method per kind, and no loop between two class hierarchies. A visitor earns its place when the elements are not yours to change but already accept a visitor (a library's syntax tree, say), when the operations should be objects (they carry state, or are plugged in by name), or in a language without a checked match, where the visitor interface is the compiler's way of making you handle every kind.
Methods on items make a new kind cheap and a new operation costly; a visitor or a checked match reverses that.
Visitor and its look-alikes#
| Pattern | Looks like Visitor because | The difference |
|---|---|---|
| Iterator (dp-22) | both go over a structure | An iterator hands you the elements one at a time, and you decide what to do with each. A visitor brings the operation to each element, and picks the code by the element's kind. |
| Strategy (dp-16) | an object that holds an algorithm | A strategy is one operation with several ways of doing it, chosen from outside. A visitor is one operation with a different piece of code for each kind of element it meets. |
| Composite (dp-13) | the cart has bundles in it | Composite is about the tree: a bundle is an item. Visitor is about operations on it. They fit together: the visitor's visit_bundle recurses. |
| Command (dp-18) | an operation as an object | A command is one request, run later. It does not choose its behaviour by the type of what it works on. |
Plain instanceof |
it checks the kind | It is the same idea without the pattern: fine for one or two operations, and the visitor's accept loop is not worth it. |
When not to use it#
- The kinds keep changing. Every new kind edits
Visitorand every visitor. If you add a kind every sprint and an operation once a year, use methods on the items. - There are one or two operations. A visitor interface with a method per kind is a lot of structure for two functions. Write the methods, or two
matchfunctions. - Your language has checked pattern matching and you can edit the types. A function with a
switchover a sealed type gives you the same compile-time check, with noacceptand no second hierarchy. - The operations need an element's private data. A visitor sits outside the class, so the element has to expose what the visitor needs. If that means making everything public, the encapsulation you were protecting is gone.
- The structure is not stable. If you do not know the kinds yet, you do not know what the visitor interface should contain.
A visitor over kinds that keep changing is a mistake: every new kind adds a method to the visitor interface and to every concrete visitor.
Where it shows up in the case studies#
| Case study | What it is |
|---|---|
| In-memory file system | A follow-up, not part of the code. The lesson's patterns table lists "Iterator / Visitor": find, du --max-depth and a backup job would each be a visitor of the same tree, so the operations stay out of File and Directory, which already have size() from the composite. Visitor fits because there are two kinds of node and a growing list of things to do with them. |
No other round uses it. Outside the 22 rounds, the Week 7 capstone draws its legal query parser producing a tree of BoolNode, ProximityNode, TermNode and NotNode (its code skips the tree and builds the search-engine query straight from the tokens). Give it that tree, and building the query, printing it and checking it become operations over a stable set of node kinds: the shape Visitor suits. Another classic home for Visitor is a compiler or a tool that walks a syntax tree, where the kinds of node are fixed by the language and the operations (type checking, printing, optimising) keep coming. In an interview, it is the right answer to "I have a tree of different node types and I want to add operations without touching the nodes", and often the wrong answer to everything else.
In the interview#
Say the pattern with the reason, in one breath: "The cart's item kinds are stable and the operations keep growing, so I'll make each operation a visitor: items only have accept, and a new operation is a new class that never touches them. The price is that a new kind of item edits every visitor." Naming the price is what scores.
Then expect the follow-ups:
- "Add a new kind of item, a gift card." A new
GiftCardclass withaccept, a newvisit_gift_cardinVisitor, and that method in all five visitors. In Java and C++ the compiler lists each visitor that is missing it; in Python a visitor without it cannot be created; in JavaScript it fails when a gift card is visited. - "Why not just use
instanceof?" For one or two operations, do. A visitor is for a growing set of operations over a fixed set of kinds. With a checkedswitchover a sealed type, the language gives you the same guarantee. - "Where does the traversal go?" In the visitor if it needs to control the walk (depth, order, skipping), in the element if every visitor should see the same walk.
- "What if the visitor needs a private field?" Expose a narrow read-only view from the element, or reconsider: a visitor that needs the internals may belong inside the class.
- "How does the visitor return a value?" Hold it in a field and make a new visitor per run, or return it from each
visit_method with a genericVisitor<R>. The second suits concurrent use. - "How is this different from Strategy?" A strategy is one job done several ways, chosen from outside. A visitor is one job with a different piece of code for each kind of element.
Common mistakes#
- A visitor over kinds that keep changing. Every new kind edits all of them. Say which list grows before you pick it.
- Skipping
acceptand usingisinstanceinside the visitor. That is the ladder the pattern exists to remove. - A visitor that forgets to walk into a bundle. The children are silently left out of the total. Test with a nested item.
- Reusing one stateful visitor for two runs. The second total includes the first. Make a new visitor per run.
- Sharing a stateful visitor between threads. Its
totalis shared too. - Making every field public so visitors can read it. That gives up the encapsulation the elements had.
- Using Visitor where a checked
switchwould do. A language with sealed types and exhaustive matching often needs no visitor at all.
Visitor
Q1/4 In the first version, each operation is a method on every item class. What does adding a shipping-weight operation cost?
Answer & explanation
Answer: C
A method per operation per class makes a new operation touch every class. A new kind is the cheap change in that design, and the visitor reverses it.
Q2/4 A gadget is in the cart and you call
item.accept(tax). Which two choices decide what runs?Answer & explanation
Answer: B
That is double dispatch: the first choice is made by the element's class (
Gadget.accept), the second by the visitor's class (Tax.visit_gadget). Nothing in the visitor checks a type.Q3/4 A
GiftCardkind is added to a cart that has five visitors. What has to change?Answer & explanation
Answer: C
This is the price of the pattern. A new operation is cheap, and a new kind edits the visitor interface and every concrete visitor.
Q4/4 Which situation fits a visitor best?
Answer & explanation
Answer: C
Visitor suits a fixed set of kinds with a growing set of operations. A frequently changing set of kinds, or a single operation, is better served by methods on the elements or a plain function.
this lesson in shorts · 3
play them all →One idea from this lesson in about a minute each, made for your phone.
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.


