Two ways to keep a design simple when objects have to coordinate and when they have to go back: one room that every part talks to, and a sealed snapshot that only its owner can open.
Three friends open a shared cart for dinner. Whoever joins changes the head count, and whoever adds a biryani changes the total. When the host taps Lock cart, nobody can add or join any more, the payment screen opens, and a banner shows where things stand. Each part depends on what the others have done.
Two separate problems show up in a screen like this, and the Gang of Four catalogue has a pattern for each. Mediator puts the rules about how the parts affect each other in one object, so the parts only talk to it. Memento lets an object hand out a sealed snapshot of its own state, so something else can keep it and give it back later (an autosave, a restore point) without being able to read or change what is inside.
There is no case study that uses Mediator by name. The elevator has the right shape: ElevatorController is the one class where button presses and cars meet. Memento is in the text editor as Snapshot, save() and restore(), and that lesson also measures what snapshots cost. This lesson builds both in one small app, in Python, Java, C++ and JavaScript.
An airport control tower is Mediator: pilots talk to the tower, never to each other, and the rules about who lands first live in one place.
The first version: widgets that call each other#
The first version gives each part the references it needs. The cart tells the banner to refresh; the roster asks the cart whether it is locked; the pay desk reads both the cart and the roster. It is the natural thing to write, because each class does only what it needs to, one call at a time:
# room_v1.py: four widgets that call each other, so every new rule edits several of them
class Cart:
def __init__(self):
self.items, self.locked, self.banner = [], False, None
def add(self, name, price):
if self.locked:
raise ValueError("the cart is locked")
self.items.append((name, price))
self.banner.refresh()
def total(self):
return sum(price for _, price in self.items)
class Roster:
def __init__(self):
self.names, self.cart, self.banner = [], None, None
def join(self, name):
if self.cart.locked:
raise ValueError("the cart is locked")
self.names.append(name)
self.banner.refresh()
class PayDesk:
def __init__(self):
self.shares, self.paid, self.cart, self.roster, self.banner = {}, [], None, None, None
def lock(self):
total, count = self.cart.total(), len(self.roster.names)
base, extra = divmod(total, count)
for i, name in enumerate(self.roster.names):
self.shares[name] = base + (1 if i < extra else 0)
self.cart.locked = True
self.banner.refresh()
def pay(self, name):
if not self.cart.locked:
raise ValueError("lock the cart first")
self.paid.append(name)
self.banner.refresh()
return self.shares[name]
class Banner:
def __init__(self):
self.text, self.cart, self.roster, self.desk = "", None, None, None
def refresh(self):
total, count = self.cart.total(), len(self.roster.names)
if not self.cart.locked:
self.text = f"open: {count} members, {len(self.cart.items)} items, total {total}"
elif len(self.desk.paid) == count:
self.text = f"placed: total {total}, paid by {count}"
else:
self.text = f"locked: total {total}, {len(self.desk.paid)} of {count} paid"
cart, roster, desk, banner = Cart(), Roster(), PayDesk(), Banner()
cart.banner = banner # the wiring: nine references among four classes
roster.cart, roster.banner = cart, banner
desk.cart, desk.roster, desk.banner = cart, roster, banner
banner.cart, banner.roster, banner.desk = cart, roster, desk
for who in ["Asha", "Ben", "Chitra"]:
roster.join(who)
for item, price in [("biryani", 250), ("paneer", 180), ("lassi", 60)]:
cart.add(item, price)
print(banner.text)
try:
desk.pay("Asha")
except ValueError as e:
print("pay before lock: refused (" + str(e) + ")")
desk.lock()
print(banner.text)// RoomV1.java: four widgets that call each other, so every new rule edits several of them
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
class RoomV1 {
record Item(String name, int price) {}
static final class Cart {
final List<Item> items = new ArrayList<>();
boolean locked;
Banner banner;
void add(String name, int price) {
if (locked) throw new IllegalStateException("the cart is locked");
items.add(new Item(name, price));
banner.refresh();
}
int total() { return items.stream().mapToInt(Item::price).sum(); }
}
static final class Roster {
final List<String> names = new ArrayList<>();
Cart cart;
Banner banner;
void join(String name) {
if (cart.locked) throw new IllegalStateException("the cart is locked");
names.add(name);
banner.refresh();
}
}
static final class PayDesk {
final Map<String, Integer> shares = new LinkedHashMap<>();
final List<String> paid = new ArrayList<>();
Cart cart;
Roster roster;
Banner banner;
void lock() {
int total = cart.total(), count = roster.names.size();
int base = total / count, extra = total % count;
for (int i = 0; i < count; i++) shares.put(roster.names.get(i), base + (i < extra ? 1 : 0));
cart.locked = true;
banner.refresh();
}
int pay(String name) {
if (!cart.locked) throw new IllegalStateException("lock the cart first");
paid.add(name);
banner.refresh();
return shares.get(name);
}
}
static final class Banner {
String text = "";
Cart cart;
Roster roster;
PayDesk desk;
void refresh() {
int total = cart.total(), count = roster.names.size();
if (!cart.locked) text = "open: " + count + " members, " + cart.items.size() + " items, total " + total;
else if (desk.paid.size() == count) text = "placed: total " + total + ", paid by " + count;
else text = "locked: total " + total + ", " + desk.paid.size() + " of " + count + " paid";
}
}
public static void main(String[] args) {
Cart cart = new Cart();
Roster roster = new Roster();
PayDesk desk = new PayDesk();
Banner banner = new Banner();
cart.banner = banner; // the wiring: nine references among four classes
roster.cart = cart; roster.banner = banner;
desk.cart = cart; desk.roster = roster; desk.banner = banner;
banner.cart = cart; banner.roster = roster; banner.desk = desk;
for (String who : List.of("Asha", "Ben", "Chitra")) roster.join(who);
cart.add("biryani", 250);
cart.add("paneer", 180);
cart.add("lassi", 60);
System.out.println(banner.text);
try {
desk.pay("Asha");
} catch (IllegalStateException e) {
System.out.println("pay before lock: refused (" + e.getMessage() + ")");
}
desk.lock();
System.out.println(banner.text);
}
}// room_v1.cpp: four widgets that call each other, so every new rule edits several of them
#include <iostream>
#include <map>
#include <numeric>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
struct Banner;
struct Cart {
std::vector<std::pair<std::string, int>> items;
bool locked = false;
Banner* banner = nullptr;
void add(const std::string& name, int price);
int total() const {
return std::accumulate(items.begin(), items.end(), 0, [](int sum, const auto& item) { return sum + item.second; });
}
};
struct Roster {
std::vector<std::string> names;
Cart* cart = nullptr;
Banner* banner = nullptr;
void join(const std::string& name);
};
struct PayDesk {
std::map<std::string, int> shares;
std::vector<std::string> paid;
Cart* cart = nullptr;
Roster* roster = nullptr;
Banner* banner = nullptr;
void lock();
int pay(const std::string& name);
};
struct Banner {
std::string text;
Cart* cart = nullptr;
Roster* roster = nullptr;
PayDesk* desk = nullptr;
void refresh() {
int total = cart->total(), count = static_cast<int>(roster->names.size());
if (!cart->locked) {
text = "open: " + std::to_string(count) + " members, " + std::to_string(cart->items.size()) + " items, total " + std::to_string(total);
} else if (static_cast<int>(desk->paid.size()) == count) {
text = "placed: total " + std::to_string(total) + ", paid by " + std::to_string(count);
} else {
text = "locked: total " + std::to_string(total) + ", " + std::to_string(desk->paid.size()) + " of " + std::to_string(count) + " paid";
}
}
};
void Cart::add(const std::string& name, int price) {
if (locked) throw std::runtime_error("the cart is locked");
items.emplace_back(name, price);
banner->refresh();
}
void Roster::join(const std::string& name) {
if (cart->locked) throw std::runtime_error("the cart is locked");
names.push_back(name);
banner->refresh();
}
void PayDesk::lock() {
int total = cart->total(), count = static_cast<int>(roster->names.size());
int base = total / count, extra = total % count;
for (int i = 0; i < count; i++) shares[roster->names[i]] = base + (i < extra ? 1 : 0);
cart->locked = true;
banner->refresh();
}
int PayDesk::pay(const std::string& name) {
if (!cart->locked) throw std::runtime_error("lock the cart first");
paid.push_back(name);
banner->refresh();
return shares[name];
}
int main() {
Cart cart;
Roster roster;
PayDesk desk;
Banner banner;
cart.banner = &banner; // the wiring: nine references among four classes
roster.cart = &cart; roster.banner = &banner;
desk.cart = &cart; desk.roster = &roster; desk.banner = &banner;
banner.cart = &cart; banner.roster = &roster; banner.desk = &desk;
for (const char* who : {"Asha", "Ben", "Chitra"}) roster.join(who);
cart.add("biryani", 250);
cart.add("paneer", 180);
cart.add("lassi", 60);
std::cout << banner.text << "\n";
try {
desk.pay("Asha");
} catch (const std::runtime_error& e) {
std::cout << "pay before lock: refused (" << e.what() << ")\n";
}
desk.lock();
std::cout << banner.text << "\n";
}// room_v1.js: four widgets that call each other, so every new rule edits several of them
class Cart {
constructor() {
Object.assign(this, { items: [], locked: false, banner: null });
}
add(name, price) {
if (this.locked) throw new Error("the cart is locked");
this.items.push({ name, price });
this.banner.refresh();
}
total() {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
class Roster {
constructor() {
Object.assign(this, { names: [], cart: null, banner: null });
}
join(name) {
if (this.cart.locked) throw new Error("the cart is locked");
this.names.push(name);
this.banner.refresh();
}
}
class PayDesk {
constructor() {
Object.assign(this, { shares: {}, paid: [], cart: null, roster: null, banner: null });
}
lock() {
const total = this.cart.total();
const count = this.roster.names.length;
const base = Math.floor(total / count);
const extra = total % count;
this.roster.names.forEach((name, i) => {
this.shares[name] = base + (i < extra ? 1 : 0);
});
this.cart.locked = true;
this.banner.refresh();
}
pay(name) {
if (!this.cart.locked) throw new Error("lock the cart first");
this.paid.push(name);
this.banner.refresh();
return this.shares[name];
}
}
class Banner {
constructor() {
Object.assign(this, { text: "", cart: null, roster: null, desk: null });
}
refresh() {
const total = this.cart.total();
const count = this.roster.names.length;
if (!this.cart.locked) this.text = `open: ${count} members, ${this.cart.items.length} items, total ${total}`;
else if (this.desk.paid.length === count) this.text = `placed: total ${total}, paid by ${count}`;
else this.text = `locked: total ${total}, ${this.desk.paid.length} of ${count} paid`;
}
}
const cart = new Cart();
const roster = new Roster();
const desk = new PayDesk();
const banner = new Banner();
cart.banner = banner; // the wiring: nine references among four classes
Object.assign(roster, { cart, banner });
Object.assign(desk, { cart, roster, banner });
Object.assign(banner, { cart, roster, desk });
for (const who of ["Asha", "Ben", "Chitra"]) roster.join(who);
for (const [item, price] of [["biryani", 250], ["paneer", 180], ["lassi", 60]]) cart.add(item, price);
console.log(banner.text);
try {
desk.pay("Asha");
} catch (e) {
console.log(`pay before lock: refused (${e.message})`);
}
desk.lock();
console.log(banner.text);open: 3 members, 3 items, total 490
pay before lock: refused (lock the cart first)
locked: total 490, 0 of 3 paidIt runs, and with four widgets it is still easy to follow. Count the wiring at the bottom: nine references, each a class that knows another exists. The problems arrive with the next feature:
- Every new panel edits the old ones. A chat line that says "Ben joined" has to be called from the roster, the cart and the pay desk. Three classes that work today get edited so a fourth can listen.
- The rules are nowhere. "A locked cart refuses joins and adds" is written twice, once in
Cart.addand once inRoster.join, andRosterreaches into the cart'slockedfield to do it. - Nothing can be tested alone. A roster test has to build a cart and a banner first.
- The number of ways it can go wrong grows faster than the number of classes. Four classes can hold at most 12 references to one another; six can hold 30.
Before the refactor: add a chat line
The product team wants a fifth widget, a chat line that says "Ben joined", "biryani added", "cart locked" and "Asha paid". In the first version, which existing classes change, and how many references does the program have afterwards?
One strong answer
Three classes change, and the program ends with twelve references. The roster raises the join, the cart raises the add, and the pay desk raises the lock and the payment, so each of the three gets a reference to the chat line: 9 + 3 = 12. The chat itself needs none of them. With the refactor in the next section the answer is one line in one class.
The pattern: Mediator#
The book that named the pattern (Design Patterns, 1994, by Gamma, Helm, Johnson and Vlissides) states the intent of Mediator in two sentences: "Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently."
In the group-order room that gives three roles:
| Role | In the group-order room | Its one job |
|---|---|---|
| Mediator (the interface) | Mediator with notify(sender, event, name, amount) |
Say what a part may tell the room: that something happened |
| Concrete mediator | Room |
Know every part, decide what each event means, and apply the rules |
| Colleagues | Cart, Roster, PayDesk, Banner |
Do their own small job when asked, and report user actions to the room instead of to each other |
%% title: Mediator: the group-order room
classDiagram
class Mediator {
<<interface>>
+notify(sender, event, name, amount)
}
class Room {
-locked bool
+heard map
+notify(sender, event, name, amount)
}
class Cart {
+add(name, price)
+put(name, price)
+total() int
}
class Roster {
+join(name)
+admit(name)
}
class PayDesk {
+lock()
+pay(name) int
+split(total, names)
}
class Banner {
+show(text)
}
Mediator <|.. Room
Cart --> Mediator : tells
Roster --> Mediator : tells
PayDesk --> Mediator : tells
Room "1" *-- "1" Cart : asks
Room "1" *-- "1" Roster : asks
Room "1" *-- "1" PayDesk : asks
Room "1" *-- "1" Banner : asksRead the arrows. There is no arrow between two colleagues: that is the point. Arrows into the room go through the Mediator interface, so a colleague can be tested with a fake room. Arrows out of the room are the room asking each part to do its piece. Each colleague has two kinds of method: a user action (add, join, lock, pay) that only reports to the room, and a plain operation (put, admit, split) that the room calls when its rules say yes. Banner only receives, so it needs no reference to the room.
When the host taps lock, the pay desk only tells the room; the room asks the cart for the total, splits it, and updates the banner.
The code: a room that coordinates#
Each colleague keeps its own data and does what it is asked. A user action never changes anything directly: it says what happened and lets the room decide. The room checks the rules, calls the right colleagues, counts the event and updates the banner. The program ends with the same demo as the first version, a little longer.
# group_room.py: the room is the only class that knows all the others
from abc import ABC, abstractmethod
class Mediator(ABC):
"""What a colleague knows about the room: how to tell it that something happened."""
@abstractmethod
def notify(self, sender: str, event: str, name: str = "", amount: int = 0) -> None: ...
class Cart:
def __init__(self, room: Mediator) -> None:
self.room, self.items = room, []
def add(self, name, price): # a user action: tell the room, change nothing here
self.room.notify("cart", "add", name, price)
def put(self, name, price): # what the room asks the cart to do
self.items.append((name, price))
def total(self):
return sum(price for _, price in self.items)
class Roster:
def __init__(self, room: Mediator) -> None:
self.room, self.names = room, []
def join(self, name):
self.room.notify("roster", "join", name)
def admit(self, name):
self.names.append(name)
class PayDesk:
def __init__(self, room: Mediator) -> None:
self.room, self.shares, self.paid = room, {}, []
def lock(self):
self.room.notify("desk", "lock")
def pay(self, name):
self.room.notify("desk", "pay", name)
return self.shares[name]
def split(self, total, names): # equal shares; the first few members cover the remainder
base, extra = divmod(total, len(names))
self.shares = {n: base + (1 if i < extra else 0) for i, n in enumerate(names)}
class Banner:
def __init__(self) -> None:
self.text = ""
def show(self, text):
self.text = text
class Room(Mediator):
"""The mediator: every rule about how the parts affect each other lives here."""
def __init__(self) -> None:
self.cart, self.roster = Cart(self), Roster(self)
self.desk, self.banner = PayDesk(self), Banner()
self.locked, self.heard = False, {} # events heard, by sender
self._rules = {"add": self._add, "join": self._join, "lock": self._lock, "pay": self._pay}
def notify(self, sender, event, name="", amount=0):
self._rules[event](name, amount)
self.heard[sender] = self.heard.get(sender, 0) + 1 # counted only when the rule did not refuse
self.banner.show(self._summary())
def _add(self, name, amount):
if self.locked:
raise ValueError("the cart is locked")
self.cart.put(name, amount)
def _join(self, name, amount):
if self.locked:
raise ValueError("the cart is locked")
self.roster.admit(name)
def _lock(self, name, amount):
if not self.roster.names:
raise ValueError("nobody has joined")
self.desk.split(self.cart.total(), self.roster.names)
self.locked = True
def _pay(self, name, amount):
if not self.locked:
raise ValueError("lock the cart first")
self.desk.paid.append(name)
def _summary(self):
total, count = self.cart.total(), len(self.roster.names)
if not self.locked:
return f"open: {count} members, {len(self.cart.items)} items, total {total}"
if len(self.desk.paid) == count:
return f"placed: total {total}, paid by {count}"
return f"locked: total {total}, {len(self.desk.paid)} of {count} paid"
def attempt(label, action):
try:
action()
except ValueError as e:
print(f"{label}: refused ({e})")
room = Room()
for who in ["Asha", "Ben", "Chitra"]:
room.roster.join(who)
for item, price in [("biryani", 250), ("paneer", 180), ("lassi", 60)]:
room.cart.add(item, price)
print(room.banner.text)
attempt("pay before lock", lambda: room.desk.pay("Asha"))
room.desk.lock()
print("shares: " + ", ".join(f"{n} {room.desk.shares[n]}" for n in room.roster.names))
attempt("add after lock", lambda: room.cart.add("naan", 40))
attempt("join after lock", lambda: room.roster.join("Dev"))
for who in ["Asha", "Ben"]:
print(f"{who} pays {room.desk.pay(who)}")
print(room.banner.text)
print(f"Chitra pays {room.desk.pay('Chitra')}")
print(room.banner.text)
print("events through the room:", ", ".join(f"{k} {v}" for k, v in sorted(room.heard.items())))// GroupRoom.java: the room is the only class that knows all the others
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
class GroupRoom {
record Item(String name, int price) {}
/** What a colleague knows about the room: how to tell it that something happened. */
interface Mediator {
void notify(String sender, String event, String name, int amount);
}
static final class Cart {
private final Mediator room;
final List<Item> items = new ArrayList<>();
Cart(Mediator room) { this.room = room; }
void add(String name, int price) { room.notify("cart", "add", name, price); } // a user action: tell the room
void put(String name, int price) { items.add(new Item(name, price)); } // what the room asks for
int total() { return items.stream().mapToInt(Item::price).sum(); }
}
static final class Roster {
private final Mediator room;
final List<String> names = new ArrayList<>();
Roster(Mediator room) { this.room = room; }
void join(String name) { room.notify("roster", "join", name, 0); }
void admit(String name) { names.add(name); }
}
static final class PayDesk {
private final Mediator room;
final Map<String, Integer> shares = new LinkedHashMap<>();
final List<String> paid = new ArrayList<>();
PayDesk(Mediator room) { this.room = room; }
void lock() { room.notify("desk", "lock", "", 0); }
int pay(String name) {
room.notify("desk", "pay", name, 0);
return shares.get(name);
}
void split(int total, List<String> names) { // equal shares; the first few members cover the remainder
int base = total / names.size(), extra = total % names.size();
for (int i = 0; i < names.size(); i++) shares.put(names.get(i), base + (i < extra ? 1 : 0));
}
}
static final class Banner {
String text = "";
void show(String text) { this.text = text; }
}
/** The mediator: every rule about how the parts affect each other lives here. */
static final class Room implements Mediator {
final Cart cart = new Cart(this);
final Roster roster = new Roster(this);
final PayDesk desk = new PayDesk(this);
final Banner banner = new Banner();
final Map<String, Integer> heard = new TreeMap<>(); // events heard, by sender
boolean locked;
public void notify(String sender, String event, String name, int amount) {
switch (event) {
case "add" -> { requireOpen(); cart.put(name, amount); }
case "join" -> { requireOpen(); roster.admit(name); }
case "lock" -> {
if (roster.names.isEmpty()) throw new IllegalStateException("nobody has joined");
desk.split(cart.total(), roster.names);
locked = true;
}
case "pay" -> {
if (!locked) throw new IllegalStateException("lock the cart first");
desk.paid.add(name);
}
default -> throw new IllegalArgumentException("unknown event: " + event);
}
heard.merge(sender, 1, Integer::sum); // counted only when the rule did not refuse
banner.show(summary());
}
private void requireOpen() {
if (locked) throw new IllegalStateException("the cart is locked");
}
private String summary() {
int total = cart.total(), count = roster.names.size();
if (!locked) return "open: " + count + " members, " + cart.items.size() + " items, total " + total;
if (desk.paid.size() == count) return "placed: total " + total + ", paid by " + count;
return "locked: total " + total + ", " + desk.paid.size() + " of " + count + " paid";
}
}
static void attempt(String label, Runnable action) {
try {
action.run();
} catch (IllegalStateException e) {
System.out.println(label + ": refused (" + e.getMessage() + ")");
}
}
public static void main(String[] args) {
Room room = new Room();
for (String who : List.of("Asha", "Ben", "Chitra")) room.roster.join(who);
room.cart.add("biryani", 250);
room.cart.add("paneer", 180);
room.cart.add("lassi", 60);
System.out.println(room.banner.text);
attempt("pay before lock", () -> room.desk.pay("Asha"));
room.desk.lock();
List<String> shares = new ArrayList<>();
for (String n : room.roster.names) shares.add(n + " " + room.desk.shares.get(n));
System.out.println("shares: " + String.join(", ", shares));
attempt("add after lock", () -> room.cart.add("naan", 40));
attempt("join after lock", () -> room.roster.join("Dev"));
for (String who : List.of("Asha", "Ben")) System.out.println(who + " pays " + room.desk.pay(who));
System.out.println(room.banner.text);
System.out.println("Chitra pays " + room.desk.pay("Chitra"));
System.out.println(room.banner.text);
List<String> counts = new ArrayList<>();
room.heard.forEach((k, v) -> counts.add(k + " " + v));
System.out.println("events through the room: " + String.join(", ", counts));
}
}// group_room.cpp: the room is the only class that knows all the others
#include <functional>
#include <iostream>
#include <map>
#include <numeric>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
// What a colleague knows about the room: how to tell it that something happened.
class Mediator {
public:
virtual ~Mediator() = default;
virtual void notify(const std::string& sender, const std::string& event, const std::string& name, int amount) = 0;
};
class Cart {
Mediator& room_;
public:
std::vector<std::pair<std::string, int>> items;
explicit Cart(Mediator& room) : room_(room) {}
void add(const std::string& name, int price) { room_.notify("cart", "add", name, price); } // a user action: tell the room
void put(const std::string& name, int price) { items.emplace_back(name, price); } // what the room asks for
int total() const {
return std::accumulate(items.begin(), items.end(), 0, [](int sum, const auto& item) { return sum + item.second; });
}
};
class Roster {
Mediator& room_;
public:
std::vector<std::string> names;
explicit Roster(Mediator& room) : room_(room) {}
void join(const std::string& name) { room_.notify("roster", "join", name, 0); }
void admit(const std::string& name) { names.push_back(name); }
};
class PayDesk {
Mediator& room_;
public:
std::map<std::string, int> shares;
std::vector<std::string> paid;
explicit PayDesk(Mediator& room) : room_(room) {}
void lock() { room_.notify("desk", "lock", "", 0); }
int pay(const std::string& name) {
room_.notify("desk", "pay", name, 0);
return shares[name];
}
void split(int total, const std::vector<std::string>& names) { // equal shares; the first few members cover the remainder
int count = static_cast<int>(names.size()), base = total / count, extra = total % count;
for (int i = 0; i < count; i++) shares[names[i]] = base + (i < extra ? 1 : 0);
}
};
struct Banner {
std::string text;
void show(const std::string& t) { text = t; }
};
// The mediator: every rule about how the parts affect each other lives here.
class Room : public Mediator {
public:
Cart cart{*this};
Roster roster{*this};
PayDesk desk{*this};
Banner banner;
std::map<std::string, int> heard; // events heard, by sender
bool locked = false;
void notify(const std::string& sender, const std::string& event, const std::string& name, int amount) override {
if (event == "add") {
requireOpen();
cart.put(name, amount);
} else if (event == "join") {
requireOpen();
roster.admit(name);
} else if (event == "lock") {
if (roster.names.empty()) throw std::runtime_error("nobody has joined");
desk.split(cart.total(), roster.names);
locked = true;
} else if (event == "pay") {
if (!locked) throw std::runtime_error("lock the cart first");
desk.paid.push_back(name);
} else {
throw std::invalid_argument("unknown event: " + event);
}
heard[sender] += 1; // counted only when the rule did not refuse
banner.show(summary());
}
private:
void requireOpen() const {
if (locked) throw std::runtime_error("the cart is locked");
}
std::string summary() const {
int total = cart.total(), count = static_cast<int>(roster.names.size()), paidCount = static_cast<int>(desk.paid.size());
if (!locked) return "open: " + std::to_string(count) + " members, " + std::to_string(cart.items.size()) + " items, total " + std::to_string(total);
if (paidCount == count) return "placed: total " + std::to_string(total) + ", paid by " + std::to_string(count);
return "locked: total " + std::to_string(total) + ", " + std::to_string(paidCount) + " of " + std::to_string(count) + " paid";
}
};
void attempt(const std::string& label, const std::function<void()>& action) {
try {
action();
} catch (const std::runtime_error& e) {
std::cout << label << ": refused (" << e.what() << ")\n";
}
}
int main() {
Room room;
for (const char* who : {"Asha", "Ben", "Chitra"}) room.roster.join(who);
room.cart.add("biryani", 250);
room.cart.add("paneer", 180);
room.cart.add("lassi", 60);
std::cout << room.banner.text << "\n";
attempt("pay before lock", [&] { room.desk.pay("Asha"); });
room.desk.lock();
std::cout << "shares: ";
for (size_t i = 0; i < room.roster.names.size(); i++) {
const std::string& n = room.roster.names[i];
std::cout << (i ? ", " : "") << n << " " << room.desk.shares[n];
}
std::cout << "\n";
attempt("add after lock", [&] { room.cart.add("naan", 40); });
attempt("join after lock", [&] { room.roster.join("Dev"); });
for (const char* who : {"Asha", "Ben"}) std::cout << who << " pays " << room.desk.pay(who) << "\n";
std::cout << room.banner.text << "\n";
std::cout << "Chitra pays " << room.desk.pay("Chitra") << "\n";
std::cout << room.banner.text << "\n";
std::cout << "events through the room: ";
bool first = true;
for (const auto& [sender, count] : room.heard) {
std::cout << (first ? "" : ", ") << sender << " " << count;
first = false;
}
std::cout << "\n";
}// group_room.js: the room is the only class that knows all the others
class Cart {
constructor(room) {
Object.assign(this, { room, items: [] });
}
add(name, price) {
this.room.notify("cart", "add", name, price); // a user action: tell the room, change nothing here
}
put(name, price) {
this.items.push({ name, price }); // what the room asks the cart to do
}
total() {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
class Roster {
constructor(room) {
Object.assign(this, { room, names: [] });
}
join(name) {
this.room.notify("roster", "join", name);
}
admit(name) {
this.names.push(name);
}
}
class PayDesk {
constructor(room) {
Object.assign(this, { room, shares: {}, paid: [] });
}
lock() {
this.room.notify("desk", "lock");
}
pay(name) {
this.room.notify("desk", "pay", name);
return this.shares[name];
}
split(total, names) {
// equal shares; the first few members cover the remainder
const base = Math.floor(total / names.length);
const extra = total % names.length;
names.forEach((n, i) => {
this.shares[n] = base + (i < extra ? 1 : 0);
});
}
}
class Banner {
text = "";
show(text) {
this.text = text;
}
}
/** The mediator: every rule about how the parts affect each other lives here. */
class Room {
constructor() {
this.cart = new Cart(this);
this.roster = new Roster(this);
this.desk = new PayDesk(this);
this.banner = new Banner();
this.locked = false;
this.heard = new Map(); // events heard, by sender
this.rules = {
add: (name, amount) => {
this.requireOpen();
this.cart.put(name, amount);
},
join: (name) => {
this.requireOpen();
this.roster.admit(name);
},
lock: () => {
if (this.roster.names.length === 0) throw new Error("nobody has joined");
this.desk.split(this.cart.total(), this.roster.names);
this.locked = true;
},
pay: (name) => {
if (!this.locked) throw new Error("lock the cart first");
this.desk.paid.push(name);
},
};
}
notify(sender, event, name = "", amount = 0) {
this.rules[event](name, amount);
this.heard.set(sender, (this.heard.get(sender) ?? 0) + 1); // counted only when the rule did not refuse
this.banner.show(this.summary());
}
requireOpen() {
if (this.locked) throw new Error("the cart is locked");
}
summary() {
const total = this.cart.total();
const count = this.roster.names.length;
if (!this.locked) return `open: ${count} members, ${this.cart.items.length} items, total ${total}`;
if (this.desk.paid.length === count) return `placed: total ${total}, paid by ${count}`;
return `locked: total ${total}, ${this.desk.paid.length} of ${count} paid`;
}
}
function attempt(label, action) {
try {
action();
} catch (e) {
console.log(`${label}: refused (${e.message})`);
}
}
const room = new Room();
for (const who of ["Asha", "Ben", "Chitra"]) room.roster.join(who);
for (const [item, price] of [["biryani", 250], ["paneer", 180], ["lassi", 60]]) room.cart.add(item, price);
console.log(room.banner.text);
attempt("pay before lock", () => room.desk.pay("Asha"));
room.desk.lock();
console.log("shares: " + room.roster.names.map((n) => `${n} ${room.desk.shares[n]}`).join(", "));
attempt("add after lock", () => room.cart.add("naan", 40));
attempt("join after lock", () => room.roster.join("Dev"));
for (const who of ["Asha", "Ben"]) console.log(`${who} pays ${room.desk.pay(who)}`);
console.log(room.banner.text);
console.log(`Chitra pays ${room.desk.pay("Chitra")}`);
console.log(room.banner.text);
console.log("events through the room: " + [...room.heard].sort(([a], [b]) => (a < b ? -1 : 1)).map(([k, v]) => `${k} ${v}`).join(", "));All four print the same thing:
open: 3 members, 3 items, total 490
pay before lock: refused (lock the cart first)
shares: Asha 164, Ben 163, Chitra 163
add after lock: refused (the cart is locked)
join after lock: refused (the cart is locked)
Asha pays 164
Ben pays 163
locked: total 490, 2 of 3 paid
Chitra pays 163
placed: total 490, paid by 3
events through the room: cart 3, desk 4, roster 3Check the numbers by hand. The cart holds 250 + 180 + 60 = 490 rupees and three people have joined. 490 divided by 3 is 163 with 1 left over, so the first member, Asha, pays 164 and the other two pay 163: 164 + 163 + 163 = 490. The room heard ten events: 3 join from the roster, 3 add from the cart, and from the pay desk 1 lock and 3 pay. The three refused calls (a payment before the lock, an add and a join after it) are not counted: the rule raised before the room counted anything, so a refusal leaves no trace.
What each language brings to the same design:
- Python: an
ABCwith one abstract method documents the colleague's view of the room. The rule table is a dictionary of bound methods, so a new event is a new entry. - Java:
Mediatoris an interface, andRoomimplements it. Every colleague takes the interface in its constructor, so a test passes a fake. Aswitchon the event string keeps the four rules in one block. - C++: colleagues hold a
Mediator&, so a colleague cannot exist without a room;Roomholds them as members, so they cannot outlive it either.Roombuilds them with*this. That works because the colleagues only store the reference and never call through it during construction. - JavaScript: no interfaces, so the contract is the method name
notify. Rules sit in an object of arrow functions, which keepsthispointing at the room.
Where the rules go, and when the room gets too big#
Mediator trades a many-to-many tangle for a hub. That is a good trade for a handful of events, and a bad one if the hub keeps growing.
A room that stays small
- Four events, four rules, each a few lines
- Colleagues report what happened, not what to do
- A new rule is one entry in the rule table
- Easy to test: send events, check the state and the banner
A room that becomes a god class
- Forty events in one function, and a rule for every pair of parts
- Colleagues call the room to ask it to do their work
- Every feature edits the room
- Nobody can say what a change will break
When the room grows, split it by concern instead of by size: one room for the cart rules and another for the payment rules still keeps the parts apart, and neither is the whole app. A rule that varies, such as a pricing rule, belongs in a strategy that the room consults.
The same shape appears between services. A saga orchestrator (Week 5, Day 3) is a mediator for services: each does its step and reports back, and the orchestrator decides what happens next, so the services never call each other.
A mediator that every feature edits becomes a god class; keep it to the rules between the parts, and split it by concern when it grows.
Memento: save a state without opening it#
The host drafts the order before it is shared: adding lines, putting notes on them ("no onion"), removing some. An autosave takes a copy now and then, so a restore can undo a bad few minutes. Two rules shape the design. The autosave must not know what a draft looks like inside, or every change to the draft would break it. And a snapshot must really be a snapshot: if a later edit can change it, it is not a restore point.
The second rule is where the first attempt usually goes wrong. Copying the list of lines is not the same as copying the lines:
# shallow_snapshot.py: copying the list is not copying what is in it
import copy
def render(lines):
return "; ".join(f"{l['name']} x{l['qty']} [{', '.join(l['notes'])}]" for l in lines)
draft = [{"name": "biryani", "qty": 2, "notes": ["no onion"]}]
shallow = list(draft) # a new list that holds the same line objects
deep = copy.deepcopy(draft) # new lines with new notes
draft[0]["notes"].append("extra gravy") # a later edit
print("draft now: " + render(draft))
print("shallow snapshot: " + render(shallow))
print("deep snapshot: " + render(deep))// ShallowSnapshot.java: copying the list is not copying what is in it
import java.util.ArrayList;
import java.util.List;
class ShallowSnapshot {
static final class Line {
final String name;
final int qty;
final List<String> notes = new ArrayList<>();
Line(String name, int qty) { this.name = name; this.qty = qty; }
Line copy() {
Line c = new Line(name, qty);
c.notes.addAll(notes);
return c;
}
}
static String render(List<Line> lines) {
List<String> parts = new ArrayList<>();
for (Line l : lines) parts.add(l.name + " x" + l.qty + " [" + String.join(", ", l.notes) + "]");
return String.join("; ", parts);
}
public static void main(String[] args) {
List<Line> draft = new ArrayList<>();
Line biryani = new Line("biryani", 2);
biryani.notes.add("no onion");
draft.add(biryani);
List<Line> shallow = new ArrayList<>(draft); // a new list that holds the same Line objects
List<Line> deep = new ArrayList<>(); // new Lines with new notes
for (Line l : draft) deep.add(l.copy());
biryani.notes.add("extra gravy"); // a later edit
System.out.println("draft now: " + render(draft));
System.out.println("shallow snapshot: " + render(shallow));
System.out.println("deep snapshot: " + render(deep));
}
}// shallow_snapshot.cpp: copying the list is not copying what is in it
#include <iostream>
#include <memory>
#include <string>
#include <vector>
struct Line {
std::string name;
int qty;
std::vector<std::string> notes;
};
// The lines are held by pointer, so a copy of the vector copies the pointers, not the lines.
using Lines = std::vector<std::shared_ptr<Line>>;
std::string render(const Lines& lines) {
std::string out;
for (const auto& l : lines) {
if (!out.empty()) out += "; ";
out += l->name + " x" + std::to_string(l->qty) + " [";
for (size_t i = 0; i < l->notes.size(); i++) out += (i ? ", " : "") + l->notes[i];
out += "]";
}
return out;
}
int main() {
Lines draft{std::make_shared<Line>(Line{"biryani", 2, {"no onion"}})};
Lines shallow = draft; // a new vector that holds the same Line objects
Lines deep; // new Lines with new notes
for (const auto& l : draft) deep.push_back(std::make_shared<Line>(*l));
draft[0]->notes.push_back("extra gravy"); // a later edit
std::cout << "draft now: " << render(draft) << "\n";
std::cout << "shallow snapshot: " << render(shallow) << "\n";
std::cout << "deep snapshot: " << render(deep) << "\n";
}// shallow_snapshot.js: copying the list is not copying what is in it
const render = (lines) => lines.map((l) => `${l.name} x${l.qty} [${l.notes.join(", ")}]`).join("; ");
const draft = [{ name: "biryani", qty: 2, notes: ["no onion"] }];
const shallow = [...draft]; // a new array that holds the same line objects
const deep = structuredClone(draft); // new lines with new notes
draft[0].notes.push("extra gravy"); // a later edit
console.log("draft now: " + render(draft));
console.log("shallow snapshot: " + render(shallow));
console.log("deep snapshot: " + render(deep));All four print the same thing:
draft now: biryani x2 [no onion, extra gravy]
shallow snapshot: biryani x2 [no onion, extra gravy]
deep snapshot: biryani x2 [no onion]The snapshot taken before the edit now shows the edit. The shallow copy made a new list, but the list holds the same line objects as the draft, so a note added to the draft's line is also added to the "snapshot". The fix is a deep copy: new lines and new notes. This trap is real in Python, Java and JavaScript, where a list of objects holds references. In C++ a vector of lines copies the lines themselves, so the trap only appears when the lines are held through pointers, as they are here to show it.
A shallow snapshot copies the list but not its lines, so a later edit changes the snapshot; copy deeply on the way in and on the way out.
The pattern's intent, in the book's words: "Without violating encapsulation, capture and externalize an object's internal state so that the object can be restored to this state later." Three roles:
| Role | In the autosaved draft | Its one job |
|---|---|---|
| Originator | Draft |
Make a snapshot of its own state, and restore itself from one |
| Memento | Memento |
Hold a snapshot, and let only the originator read it |
| Caretaker | Autosave |
Decide when to save and how many to keep, and hand snapshots back, never reading them |
%% title: Memento: the autosaved draft
classDiagram
class Draft {
-lines list
+add(name, qty)
+note(name, text)
+remove(name)
+render() str
+save() Memento
+restore(memento)
}
class Memento {
<<sealed>>
-state list
}
class Autosave {
+every int
+keep int
+kept list
+edited()
+restore(back)
}
Draft ..> Memento : creates and opens
Autosave "1" o-- "many" Memento : holds
Autosave --> Draft : asks to save and restoreRead the arrows. Draft is the only class that opens a memento. Autosave holds many of them (the hollow diamond) and asks the draft to make and restore them, but has no arrow that reads one. So the draft can change what it stores tomorrow, and the autosave will not notice.
A game's save slots are Memento: the save screen lists, keeps and deletes slots, but only the game can write one or read one back.
# autosave.py: the draft seals a snapshot; the autosave keeps snapshots without opening one
import copy
class Memento:
"""A sealed snapshot. Only Draft reads _state; everyone else just holds it."""
def __init__(self, state):
self._state = state
class Draft:
"""The originator: it alone knows what its state looks like inside."""
def __init__(self):
self._lines = []
def add(self, name, qty):
self._lines.append({"name": name, "qty": qty, "notes": []})
def note(self, name, text):
self._find(name)["notes"].append(text)
def remove(self, name):
self._lines.remove(self._find(name))
def render(self):
return "; ".join(f"{l['name']} x{l['qty']}" + (f" [{', '.join(l['notes'])}]" if l["notes"] else "") for l in self._lines) or "(empty)"
def save(self):
return Memento(copy.deepcopy(self._lines))
def restore(self, memento):
self._lines = copy.deepcopy(memento._state) # copy again, so the memento stays good for next time
def _find(self, name):
return next(l for l in self._lines if l["name"] == name)
class Autosave:
"""The caretaker: it decides when to save and how many to keep, and never looks inside."""
def __init__(self, draft, every=3, keep=2):
self.draft, self.every, self.keep = draft, every, keep
self.edits, self.saves, self.kept = 0, 0, []
def edited(self):
self.edits += 1
if self.edits % self.every == 0:
self.kept.append(self.draft.save())
self.saves += 1
del self.kept[:-self.keep] # drop the oldest beyond the limit
def restore(self, back=1): # 1 is the newest autosave, 2 the one before it
self.draft.restore(self.kept[-back])
draft = Draft()
auto = Autosave(draft)
def edit(action):
action()
auto.edited()
edit(lambda: draft.add("biryani", 2))
edit(lambda: draft.add("lassi", 1))
edit(lambda: draft.note("biryani", "no onion"))
print(f"after {auto.edits} edits: {draft.render()} | saved {auto.saves}, kept {len(auto.kept)}")
edit(lambda: draft.remove("lassi"))
edit(lambda: draft.add("paneer", 1))
edit(lambda: draft.note("paneer", "extra spicy"))
print(f"after {auto.edits} edits: {draft.render()} | saved {auto.saves}, kept {len(auto.kept)}")
edit(lambda: draft.add("naan", 2))
edit(lambda: draft.note("naan", "butter"))
edit(lambda: draft.remove("biryani"))
print(f"after {auto.edits} edits: {draft.render()} | saved {auto.saves}, kept {len(auto.kept)}")
auto.restore(2)
print("restored the older save:", draft.render())
draft.add("tea", 1)
auto.restore(2)
print("restored it again: ", draft.render())// Autosave.java: the draft seals a snapshot; the autosave keeps snapshots without opening one
import java.util.ArrayList;
import java.util.List;
class Autosave {
/** What anyone but Draft sees of a snapshot: nothing to read. */
interface Memento {}
static final class Line {
final String name;
final int qty;
final List<String> notes = new ArrayList<>();
Line(String name, int qty) { this.name = name; this.qty = qty; }
Line copy() {
Line c = new Line(name, qty);
c.notes.addAll(notes);
return c;
}
}
/** The originator: it alone knows what its state looks like inside. */
static final class Draft {
private record Saved(List<Line> lines) implements Memento {}
private List<Line> lines = new ArrayList<>();
void add(String name, int qty) { lines.add(new Line(name, qty)); }
void note(String name, String text) { find(name).notes.add(text); }
void remove(String name) { lines.remove(find(name)); }
String render() {
List<String> parts = new ArrayList<>();
for (Line l : lines) parts.add(l.name + " x" + l.qty + (l.notes.isEmpty() ? "" : " [" + String.join(", ", l.notes) + "]"));
return parts.isEmpty() ? "(empty)" : String.join("; ", parts);
}
Memento save() { return new Saved(copyOf(lines)); }
void restore(Memento m) { lines = copyOf(((Saved) m).lines()); } // copy again, so the memento stays good
private Line find(String name) {
return lines.stream().filter(l -> l.name.equals(name)).findFirst().orElseThrow();
}
private static List<Line> copyOf(List<Line> source) {
List<Line> out = new ArrayList<>();
for (Line l : source) out.add(l.copy());
return out;
}
}
/** The caretaker: it decides when to save and how many to keep, and never looks inside. */
static final class Saver {
final Draft draft;
final int every, keep;
final List<Memento> kept = new ArrayList<>();
int edits, saves;
Saver(Draft draft, int every, int keep) { this.draft = draft; this.every = every; this.keep = keep; }
void edited() {
edits++;
if (edits % every == 0) {
kept.add(draft.save());
saves++;
if (kept.size() > keep) kept.remove(0); // drop the oldest beyond the limit
}
}
void restore(int back) { draft.restore(kept.get(kept.size() - back)); } // 1 is the newest, 2 the one before
}
static void edit(Saver saver, Runnable action) {
action.run();
saver.edited();
}
static void report(Saver s) {
System.out.println("after " + s.edits + " edits: " + s.draft.render() + " | saved " + s.saves + ", kept " + s.kept.size());
}
public static void main(String[] args) {
Draft draft = new Draft();
Saver auto = new Saver(draft, 3, 2);
edit(auto, () -> draft.add("biryani", 2));
edit(auto, () -> draft.add("lassi", 1));
edit(auto, () -> draft.note("biryani", "no onion"));
report(auto);
edit(auto, () -> draft.remove("lassi"));
edit(auto, () -> draft.add("paneer", 1));
edit(auto, () -> draft.note("paneer", "extra spicy"));
report(auto);
edit(auto, () -> draft.add("naan", 2));
edit(auto, () -> draft.note("naan", "butter"));
edit(auto, () -> draft.remove("biryani"));
report(auto);
auto.restore(2);
System.out.println("restored the older save: " + draft.render());
draft.add("tea", 1);
auto.restore(2);
System.out.println("restored it again: " + draft.render());
}
}// autosave.cpp: the draft seals a snapshot; the autosave keeps snapshots without opening one
#include <algorithm>
#include <functional>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
struct Line {
std::string name;
int qty;
std::vector<std::string> notes;
};
// The originator: it alone knows what its state looks like inside.
class Draft {
std::vector<Line> lines_; // held by value, so copying the vector copies the lines too
Line& find(const std::string& name) {
auto it = std::find_if(lines_.begin(), lines_.end(), [&](const Line& l) { return l.name == name; });
if (it == lines_.end()) throw std::invalid_argument("no line " + name);
return *it;
}
public:
// A sealed snapshot: only Draft can build one or open one.
class Memento {
friend class Draft;
std::vector<Line> state_;
explicit Memento(std::vector<Line> state) : state_(std::move(state)) {}
};
void add(const std::string& name, int qty) { lines_.push_back({name, qty, {}}); }
void note(const std::string& name, const std::string& text) { find(name).notes.push_back(text); }
void remove(const std::string& name) { lines_.erase(lines_.begin() + (&find(name) - lines_.data())); }
std::string render() const {
std::string out;
for (const Line& l : lines_) {
if (!out.empty()) out += "; ";
out += l.name + " x" + std::to_string(l.qty);
for (size_t i = 0; i < l.notes.size(); i++) out += (i ? ", " : " [") + l.notes[i] + (i + 1 == l.notes.size() ? "]" : "");
}
return out.empty() ? "(empty)" : out;
}
Memento save() const { return Memento(lines_); }
void restore(const Memento& m) { lines_ = m.state_; } // a copy, so the memento stays good for next time
};
// The caretaker: it decides when to save and how many to keep, and never looks inside.
class Autosave {
public:
Draft& draft;
int every, keep, edits = 0, saves = 0;
std::vector<Draft::Memento> kept;
Autosave(Draft& d, int every_, int keep_) : draft(d), every(every_), keep(keep_) {}
void edited() {
edits++;
if (edits % every == 0) {
kept.push_back(draft.save());
saves++;
if (static_cast<int>(kept.size()) > keep) kept.erase(kept.begin()); // drop the oldest beyond the limit
}
}
void restore(int back) { draft.restore(kept[kept.size() - back]); } // 1 is the newest, 2 the one before
};
void edit(Autosave& auto_, const std::function<void()>& action) {
action();
auto_.edited();
}
void report(const Autosave& a) {
std::cout << "after " << a.edits << " edits: " << a.draft.render() << " | saved " << a.saves << ", kept " << a.kept.size() << "\n";
}
int main() {
Draft draft;
Autosave autosave(draft, 3, 2);
edit(autosave, [&] { draft.add("biryani", 2); });
edit(autosave, [&] { draft.add("lassi", 1); });
edit(autosave, [&] { draft.note("biryani", "no onion"); });
report(autosave);
edit(autosave, [&] { draft.remove("lassi"); });
edit(autosave, [&] { draft.add("paneer", 1); });
edit(autosave, [&] { draft.note("paneer", "extra spicy"); });
report(autosave);
edit(autosave, [&] { draft.add("naan", 2); });
edit(autosave, [&] { draft.note("naan", "butter"); });
edit(autosave, [&] { draft.remove("biryani"); });
report(autosave);
autosave.restore(2);
std::cout << "restored the older save: " << draft.render() << "\n";
draft.add("tea", 1);
autosave.restore(2);
std::cout << "restored it again: " << draft.render() << "\n";
}// autosave.js: the draft seals a snapshot; the autosave keeps snapshots without opening one
const sealed = new WeakMap(); // memento -> state; only this module can open one
/** The originator: it alone knows what its state looks like inside. */
class Draft {
#lines = [];
add(name, qty) {
this.#lines.push({ name, qty, notes: [] });
}
note(name, text) {
this.#find(name).notes.push(text);
}
remove(name) {
this.#lines.splice(this.#lines.indexOf(this.#find(name)), 1);
}
render() {
const parts = this.#lines.map((l) => `${l.name} x${l.qty}` + (l.notes.length ? ` [${l.notes.join(", ")}]` : ""));
return parts.join("; ") || "(empty)";
}
save() {
const memento = Object.freeze({}); // an empty, frozen box: nothing to read from outside
sealed.set(memento, structuredClone(this.#lines));
return memento;
}
restore(memento) {
this.#lines = structuredClone(sealed.get(memento)); // copy again, so the memento stays good for next time
}
#find(name) {
return this.#lines.find((l) => l.name === name);
}
}
/** The caretaker: it decides when to save and how many to keep, and never looks inside. */
class Autosave {
constructor(draft, every = 3, keep = 2) {
Object.assign(this, { draft, every, keep, edits: 0, saves: 0, kept: [] });
}
edited() {
this.edits++;
if (this.edits % this.every === 0) {
this.kept.push(this.draft.save());
this.saves++;
if (this.kept.length > this.keep) this.kept.shift(); // drop the oldest beyond the limit
}
}
restore(back = 1) {
// 1 is the newest autosave, 2 the one before it
this.draft.restore(this.kept[this.kept.length - back]);
}
}
const draft = new Draft();
const auto = new Autosave(draft);
const edit = (action) => {
action();
auto.edited();
};
const report = () => console.log(`after ${auto.edits} edits: ${draft.render()} | saved ${auto.saves}, kept ${auto.kept.length}`);
edit(() => draft.add("biryani", 2));
edit(() => draft.add("lassi", 1));
edit(() => draft.note("biryani", "no onion"));
report();
edit(() => draft.remove("lassi"));
edit(() => draft.add("paneer", 1));
edit(() => draft.note("paneer", "extra spicy"));
report();
edit(() => draft.add("naan", 2));
edit(() => draft.note("naan", "butter"));
edit(() => draft.remove("biryani"));
report();
auto.restore(2);
console.log("restored the older save: " + draft.render());
draft.add("tea", 1);
auto.restore(2);
console.log("restored it again: " + draft.render());All four print the same thing:
after 3 edits: biryani x2 [no onion]; lassi x1 | saved 1, kept 1
after 6 edits: biryani x2 [no onion]; paneer x1 [extra spicy] | saved 2, kept 2
after 9 edits: paneer x1 [extra spicy]; naan x2 [butter] | saved 3, kept 2
restored the older save: biryani x2 [no onion]; paneer x1 [extra spicy]
restored it again: biryani x2 [no onion]; paneer x1 [extra spicy]Check by hand. The autosave saves every third edit and keeps two. Edits 3, 6 and 9 each make a save, so three were taken and, after the third, only the saves from edits 6 and 9 are kept: the first was dropped. restore(2) asks for the older of the two, which is the draft as it stood after edit 6: biryani x2 [no onion]; paneer x1 [extra spicy]. After that the program adds a tea and restores the same save again. The tea is gone and the draft comes back identical, because restore copies the snapshot instead of taking it over. A restore that reused the memento itself would let the next edit change it.
What each language brings:
- Python: nothing can be sealed; a leading underscore on
_stateis the only barrier, and it is a convention.copy.deepcopymakes the deep copy in one call. - Java:
Draft.save()returns the empty interfaceMemento, so the caretaker has nothing to call. The real state sits in a private record that onlyDraftcasts to. Because all the classes here share one outer class, Java would let the caretaker reach it; in separate files it cannot. - C++:
Mementohas a private constructor and private data, andDraftis itsfriend. The caretaker can store and copy mementos but cannot build or open one, and the compiler enforces that. - JavaScript: the state is kept in a module-private
WeakMapkeyed by a frozen empty object, so what the caretaker holds has nothing in it to read.structuredClonemakes the deep copy.
Which save?
The autosave saves every 3rd edit and keeps 2. After 9 edits the draft shows paneer x1 [extra spicy]; naan x2 [butter]. What does restore(1) give, and what does restore(2) give?
One strong answer
restore(1) gives the draft as it was after edit 9, which is what it shows now; restore(2) gives the draft after edit 6. The three saves were taken at edits 3, 6 and 9, and only the last two are kept, so the state after edit 3 is gone for good. The limit is what stops memory growing, and the cost is that you cannot go back past the oldest save you kept.
Memento or Command?#
Both give you a way back, at very different costs. A snapshot stores the whole state; a command stores only what it needs to reverse one change. The text editor lesson measures the difference on a 50,000-character document: after 200 edits with the last 100 kept, commands hold 100 characters and snapshots hold 5,014,950.
| Memento (a snapshot) | Command with undo (dp-18) | |
|---|---|---|
| What is stored | The whole state, once per save | Only what changed, once per edit |
| Cost grows with | The size of the state, times the number of saves | The size of the change, times the number of edits |
| Getting back | Put the snapshot back, any one of them | Run the inverses in reverse order |
| Easy to get wrong | Forgetting a deep copy | Writing an inverse that does not undo exactly |
| Fits | Small state, or checkpoints now and then: autosave, crash recovery, "discard changes" | Keystroke-level undo on large documents |
The editor lesson draws the same line: commands for keystroke undo, snapshots for autosave and crash recovery.
Mediator, Memento and their look-alikes#
| Pattern | Looks like it because | The difference |
|---|---|---|
| Observer (dp-17) | an object tells others that something changed | Observer is one-way: one subject, many listeners, and the listeners do not coordinate. A mediator is two-way, and it holds the rules that connect the parts. Many mediators are built on observers: the parts publish, the room subscribes. |
| Facade (dp-12) | one object stands in front of several | A facade is for outsiders: the subsystem does not know it exists. A mediator is known by the parts, which send everything through it. |
| Message broker (Week 3) | messages pass through a middle | A broker moves messages between processes and decouples them in time. A mediator is an object in one process that applies rules. |
| Command (dp-18) | both help you go back | See the table above: Command stores the change, Memento stores the state. |
| Prototype (dp-09) | both copy an object | Prototype copies to make a new object. Memento copies to put the same object back as it was. |
| Serialisation | both externalise state | Serialisation turns state into bytes for storage or transport. A memento stays in memory and is opaque to its holder; it may be serialised, but it need not be. |
When not to use them#
- Two parts talk to each other and no one else. A direct call is clearer than a room with one rule.
- The parts do not affect each other. Nothing needs coordinating.
- The room is turning into the application. If every feature edits it, split it by concern, or let the parts publish events and keep the rules with their owners.
- The state is large and the change is small. Use a command with an inverse, or store a difference.
- The state includes things you cannot take back. Restoring a draft does not refund a payment or unsend an email. A snapshot only restores what the object owns.
- The state is private or sensitive. A snapshot is another copy of it, in memory or on disk, and has to be protected, expired and deleted like the original.
Where it shows up in the case studies#
| Case study | The pattern | What it is |
|---|---|---|
| Text editor | Memento: Snapshot, save(), restore() |
A frozen snapshot of the text and cursor, opaque to everyone but the editor. The lesson uses it for autosave and measures it against commands. |
| Elevator | Mediator, by shape and not by name | ElevatorController receives every hall call and car call and picks a car through its DispatchStrategy. Calls come to the controller, not to a car. It is the closest case study to a mediator, though the lesson does not call it one. |
Mediator suits a screen or a workflow whose parts react to one another; most of the 22 rounds are data structures and services, where an observer or a facade is the more usual shape. Memento is the one to think of when a prompt says "save", "restore" or "discard changes".
In the interview#
Say each pattern with its reason, in one breath. For Mediator: "The cart, the roster and the pay desk all affect each other, so I'll give them one room: each part tells the room what happened, and the room applies the rules, so no part holds a reference to another." For Memento: "Autosave needs a restore point without knowing what a draft looks like inside, so the draft makes a sealed snapshot and the autosave only keeps it."
Then expect the follow-ups:
- "Isn't the mediator a god class?" It can become one. Keep it to the rules about how the parts interact, keep each part's own work inside the part, and split the room by concern if it grows.
- "How is this different from Observer?" Observer broadcasts one way. A mediator holds the rules between the parts, and the parts report to it. Often the mediator is built on an observer.
- "Add a chat line." A new colleague, plus one line in the room that tells it about each event. The cart, the roster and the pay desk do not change.
- "Autosave a 50,000-character document on every keystroke?" That is 5,014,950 characters of snapshots in the editor lesson's numbers. Save on a timer or after a pause, or keep a difference since the last save.
- "What does restore not undo?" Anything outside the object: a payment made, a message sent. Those need a compensating action, not a restore.
Common mistakes#
- A mediator that every feature edits. It has become the application.
- Colleagues that keep a reference to each other "just this once". The tangle comes back through a side door.
- A snapshot that shares state with the live object. A later edit changes the restore point.
- A restore that takes over the memento. Copy on the way out as well as on the way in.
- A caretaker that reads inside the memento. Then every change to the state breaks it.
- Unlimited autosaves. Say how many you keep and what falls off.
- Counting a refused event. A rule that refuses must leave the state, and the counts, as they were.
Mediator and Memento
Q1/4 In the first version the four widgets hold nine references to one another. After the refactor to a mediator, how many references run from one colleague to another?
Answer & explanation
Answer: C
Colleagues point only at the room. The rules about how the parts affect each other moved into the room, and the references between parts are gone.
Q2/4 What is the main risk of a mediator?
Answer & explanation
Answer: C
A mediator gathers the rules into one place, which is its benefit. If every feature adds to it, it becomes the application. Split it by concern, and keep each part's own work in the part.
Q3/4 A snapshot is taken with a shallow copy of a list of lines. A later edit adds a note to one line. What does the snapshot show?
Answer & explanation
Answer: C
Copying the list makes a new list, but the lines inside it are the same objects. A memento needs a deep copy, on the way in and on the way out.
Q4/4 An autosave keeps the last two of three saves. After the third save, what can
restorereach?Answer & explanation
Answer: C
The limit exists to bound memory. Once the third save is taken, the oldest falls off, so you cannot go back past the oldest save you kept.
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.


