Bridge: two things that vary, kept apart so they can grow separately. Flyweight: the part that thousands of small objects have in common, kept once.
A notification app sends an order update, a one-time code and a promotion, by email, text message and push. Written as one class per pair, every new channel needs a class for every kind. A cinema app draws a seat map of 2,000 seats, and each seat carries its own copy of its kind's name, price and legroom, which are the same for every seat of that kind.
Bridge splits what varies along two lines into two hierarchies joined by one reference. Flyweight keeps one copy of what thousands of small objects have in common. You will meet both far less often than Strategy or Facade, which is why the lesson is marked Hard: the trap is using them where a simpler tool does. No case study uses either by name.
Remotes are one family and TVs another, and they meet at one signal. A new brand or a new remote is one class, and neither touches the other.
Bridge: the first version, a class for every pair#
The first notification code has a base class and one subclass for each kind and channel it has needed so far:
# first_version.py: one class for every kind and channel pair: 3 kinds x 3 channels = 9 classes
class Notification: # the base: it knows who to send to and one detail (an order id or a code), and nothing else
def __init__(self, to: str, detail: int = 0) -> None:
self.to, self.detail = to, detail
class OrderShippedEmail(Notification):
def send(self) -> str:
return f"[email to {self.to}] Order shipped: Order {self.detail} is on its way"
class OrderShippedSms(Notification): # the same words again, cut at 40 characters
def send(self) -> str:
return f"[sms to {self.to}] " + f"Order shipped: Order {self.detail} is on its way"[:40]
class OrderShippedPush(Notification):
def send(self) -> str:
return f"[push to {self.to}] Order shipped / Order {self.detail} is on its way"
class OtpEmail(Notification):
def send(self) -> str:
return f"[email to {self.to}] Your code: {self.detail} is valid for 5 minutes"
class OtpSms(Notification):
def send(self) -> str:
return f"[sms to {self.to}] " + f"Your code: {self.detail} is valid for 5 minutes"[:40]
class OtpPush(Notification):
def send(self) -> str:
return f"[push to {self.to}] Your code / {self.detail} is valid for 5 minutes"
class PromoEmail(Notification):
def send(self) -> str:
return f"[email to {self.to}] Weekend offer: 20 percent off wraps till Sunday"
class PromoSms(Notification):
def send(self) -> str:
return f"[sms to {self.to}] " + "Weekend offer: 20 percent off wraps till Sunday"[:40]
class PromoPush(Notification):
def send(self) -> str:
return f"[push to {self.to}] Weekend offer / 20 percent off wraps till Sunday"
for message in (OrderShippedEmail("asha", 77), OrderShippedSms("asha", 77), OtpSms("asha", 482913), PromoSms("asha"), PromoPush("asha")):
print(message.send())// FirstVersion.java: one class for every kind and channel pair: 3 kinds x 3 channels = 9 classes
class FirstVersion {
static abstract class Notification { // the base: it knows who to send to and one detail (an order id or a code), and nothing else
final String to;
final int detail;
Notification(String to, int detail) {
this.to = to;
this.detail = detail;
}
abstract String send();
}
static String cut(String text) { return text.length() > 40 ? text.substring(0, 40) : text; }
static final class OrderShippedEmail extends Notification {
OrderShippedEmail(String to, int detail) { super(to, detail); }
String send() { return "[email to " + to + "] Order shipped: Order " + detail + " is on its way"; }
}
static final class OrderShippedSms extends Notification { // the same words again, cut at 40 characters
OrderShippedSms(String to, int detail) { super(to, detail); }
String send() { return "[sms to " + to + "] " + cut("Order shipped: Order " + detail + " is on its way"); }
}
static final class OrderShippedPush extends Notification {
OrderShippedPush(String to, int detail) { super(to, detail); }
String send() { return "[push to " + to + "] Order shipped / Order " + detail + " is on its way"; }
}
static final class OtpEmail extends Notification {
OtpEmail(String to, int detail) { super(to, detail); }
String send() { return "[email to " + to + "] Your code: " + detail + " is valid for 5 minutes"; }
}
static final class OtpSms extends Notification {
OtpSms(String to, int detail) { super(to, detail); }
String send() { return "[sms to " + to + "] " + cut("Your code: " + detail + " is valid for 5 minutes"); }
}
static final class OtpPush extends Notification {
OtpPush(String to, int detail) { super(to, detail); }
String send() { return "[push to " + to + "] Your code / " + detail + " is valid for 5 minutes"; }
}
static final class PromoEmail extends Notification {
PromoEmail(String to, int detail) { super(to, detail); }
String send() { return "[email to " + to + "] Weekend offer: 20 percent off wraps till Sunday"; }
}
static final class PromoSms extends Notification {
PromoSms(String to, int detail) { super(to, detail); }
String send() { return "[sms to " + to + "] " + cut("Weekend offer: 20 percent off wraps till Sunday"); }
}
static final class PromoPush extends Notification {
PromoPush(String to, int detail) { super(to, detail); }
String send() { return "[push to " + to + "] Weekend offer / 20 percent off wraps till Sunday"; }
}
public static void main(String[] args) {
Notification[] messages = {new OrderShippedEmail("asha", 77), new OrderShippedSms("asha", 77), new OtpSms("asha", 482913), new PromoSms("asha", 0), new PromoPush("asha", 0)};
for (Notification message : messages) System.out.println(message.send());
}
}// first_version.cpp: one class for every kind and channel pair: 3 kinds x 3 channels = 9 classes
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <vector>
class Notification { // the base: it knows who to send to and one detail (an order id or a code), and nothing else
public:
explicit Notification(std::string to, int detail = 0) : to_(std::move(to)), detail_(detail) {}
virtual ~Notification() = default;
virtual std::string send() const = 0;
protected:
std::string to_;
int detail_;
};
std::string cut(const std::string& text) { return text.substr(0, 40); }
class OrderShippedEmail : public Notification {
public:
using Notification::Notification;
std::string send() const override { return "[email to " + to_ + "] Order shipped: Order " + std::to_string(detail_) + " is on its way"; }
};
class OrderShippedSms : public Notification { // the same words again, cut at 40 characters
public:
using Notification::Notification;
std::string send() const override { return "[sms to " + to_ + "] " + cut("Order shipped: Order " + std::to_string(detail_) + " is on its way"); }
};
class OrderShippedPush : public Notification {
public:
using Notification::Notification;
std::string send() const override { return "[push to " + to_ + "] Order shipped / Order " + std::to_string(detail_) + " is on its way"; }
};
class OtpEmail : public Notification {
public:
using Notification::Notification;
std::string send() const override { return "[email to " + to_ + "] Your code: " + std::to_string(detail_) + " is valid for 5 minutes"; }
};
class OtpSms : public Notification {
public:
using Notification::Notification;
std::string send() const override { return "[sms to " + to_ + "] " + cut("Your code: " + std::to_string(detail_) + " is valid for 5 minutes"); }
};
class OtpPush : public Notification {
public:
using Notification::Notification;
std::string send() const override { return "[push to " + to_ + "] Your code / " + std::to_string(detail_) + " is valid for 5 minutes"; }
};
class PromoEmail : public Notification {
public:
using Notification::Notification;
std::string send() const override { return "[email to " + to_ + "] Weekend offer: 20 percent off wraps till Sunday"; }
};
class PromoSms : public Notification {
public:
using Notification::Notification;
std::string send() const override { return "[sms to " + to_ + "] " + cut("Weekend offer: 20 percent off wraps till Sunday"); }
};
class PromoPush : public Notification {
public:
using Notification::Notification;
std::string send() const override { return "[push to " + to_ + "] Weekend offer / 20 percent off wraps till Sunday"; }
};
int main() {
std::vector<std::unique_ptr<Notification>> messages;
messages.push_back(std::make_unique<OrderShippedEmail>("asha", 77));
messages.push_back(std::make_unique<OrderShippedSms>("asha", 77));
messages.push_back(std::make_unique<OtpSms>("asha", 482913));
messages.push_back(std::make_unique<PromoSms>("asha"));
messages.push_back(std::make_unique<PromoPush>("asha"));
for (const auto& message : messages) std::cout << message->send() << "\n";
}// firstVersion.js: one class for every kind and channel pair: 3 kinds x 3 channels = 9 classes
class Notification { // the base: it knows who to send to and one detail (an order id or a code), and nothing else
constructor(to, detail = 0) {
this.to = to;
this.detail = detail;
}
}
const cut = (text) => text.slice(0, 40);
class OrderShippedEmail extends Notification {
send() { return `[email to ${this.to}] Order shipped: Order ${this.detail} is on its way`; }
}
class OrderShippedSms extends Notification { // the same words again, cut at 40 characters
send() { return `[sms to ${this.to}] ` + cut(`Order shipped: Order ${this.detail} is on its way`); }
}
class OrderShippedPush extends Notification {
send() { return `[push to ${this.to}] Order shipped / Order ${this.detail} is on its way`; }
}
class OtpEmail extends Notification {
send() { return `[email to ${this.to}] Your code: ${this.detail} is valid for 5 minutes`; }
}
class OtpSms extends Notification {
send() { return `[sms to ${this.to}] ` + cut(`Your code: ${this.detail} is valid for 5 minutes`); }
}
class OtpPush extends Notification {
send() { return `[push to ${this.to}] Your code / ${this.detail} is valid for 5 minutes`; }
}
class PromoEmail extends Notification {
send() { return `[email to ${this.to}] Weekend offer: 20 percent off wraps till Sunday`; }
}
class PromoSms extends Notification {
send() { return `[sms to ${this.to}] ` + cut("Weekend offer: 20 percent off wraps till Sunday"); }
}
class PromoPush extends Notification {
send() { return `[push to ${this.to}] Weekend offer / 20 percent off wraps till Sunday`; }
}
for (const message of [new OrderShippedEmail("asha", 77), new OrderShippedSms("asha", 77), new OtpSms("asha", 482913), new PromoSms("asha"), new PromoPush("asha")]) {
console.log(message.send());
}[email to asha] Order shipped: Order 77 is on its way
[sms to asha] Order shipped: Order 77 is on its way
[sms to asha] Your code: 482913 is valid for 5 minutes
[sms to asha] Weekend offer: 20 percent off wraps till
[push to asha] Weekend offer / 20 percent off wraps till SundayCount the classes. Three kinds and three channels give 3 × 3 = 9 concrete classes, and the base makes 10. Each class repeats two things: the words of its kind (Order shipped is written three times) and the format of its channel (the 40-character cut is written three times, which is why the promotion ends at till).
- A new channel is a class per kind. A fourth channel, chat, adds 3 classes: 12 concrete classes. A fourth kind adds one per channel.
- The count is a product. 4 kinds and 5 channels make 20 classes.
- Every rule is written several times. The 40-character limit is in three classes, so changing it is three edits, and one gets missed.
- The two lines can't change separately. New wording for the promotion opens three classes, and a new email provider opens three kinds.
Before the refactor: what are the two dimensions?
Nine classes come from two things that each vary. Name them, and say how many classes you would write if each dimension had its own hierarchy.
One strong answer
The kind of message (what is said) and the channel (how it goes out). With a hierarchy for each, you write 3 kinds plus 3 channels, 6 concrete classes, and any kind can use any channel. A product (3 × 3) becomes a sum (3 + 3), and adding a channel is one class.
Bridge: the pattern#
The book that named it (Design Patterns, 1994, by Gamma, Helm, Johnson and Vlissides) states the intent of Bridge as: "Decouple an abstraction from its implementation so that the two can vary independently."
"Abstraction" and "implementation" are two roles here, not an abstract class and a concrete one. The abstraction is the side the client uses. The implementation is the lower-level side that does the physical work.
| Role | In the notification code | Its one job |
|---|---|---|
| Abstraction | Notification |
The high-level side: knows what is said (title, body) and holds a channel |
| Refined abstractions | OrderShipped, Otp, Promo |
One per kind of message |
| Implementor | Channel |
The low-level interface: deliver(to, title, body). It need not match the abstraction's methods |
| Concrete implementors | Email, Sms, Push, Chat |
One per way of sending |
%% title: Bridge: kinds on one side, channels on the other, joined by one reference
classDiagram
class Notification {
<<abstract>>
-to str
+title() str
+body() str
+send() str
}
class OrderShipped
class Otp
class Promo
class Channel {
<<interface>>
+deliver(to, title, body) str
}
class Email
class Sms
class Push
class Chat
Notification <|-- OrderShipped : extends
Notification <|-- Otp : extends
Notification <|-- Promo : extends
Notification --> Channel : bridge, delivers through
Channel <|.. Email : implements
Channel <|.. Sms : implements
Channel <|.. Push : implements
Channel <|.. Chat : implementsRead the arrows. There are two hierarchies, and the one arrow between them, from Notification to Channel, is the bridge. A kind never names a channel class and a channel never names a kind, so each side grows without touching the other. send() is written once, in the base: it asks the kind for its words and hands them to whichever channel it holds.
Kinds of message are one hierarchy and channels are another. One arrow joins them: a notification holds a channel and delivers through it.
Bridge: the code#
Here is the refactor in all four languages. It adds the fourth channel, Chat, as one new class, and sends every kind through every channel:
# bridge.py: what is said (the kind) and how it goes out (the channel) are two hierarchies joined by one reference
from abc import ABC, abstractmethod
class Channel(ABC): # the implementation side: how a message physically goes out
@abstractmethod
def deliver(self, to: str, title: str, body: str) -> str: ...
class Email(Channel):
def deliver(self, to: str, title: str, body: str) -> str:
return f"[email to {to}] {title}: {body}"
class Sms(Channel):
def deliver(self, to: str, title: str, body: str) -> str:
return f"[sms to {to}] " + f"{title}: {body}"[:40] # a text message has a limit
class Push(Channel):
def deliver(self, to: str, title: str, body: str) -> str:
return f"[push to {to}] {title} / {body}"
class Chat(Channel): # added later: one new class, and no kind is edited
def deliver(self, to: str, title: str, body: str) -> str:
return f"[chat to {to}] {title} | {body}"
class Notification(ABC): # the abstraction: what is being said, holding a channel
def __init__(self, to: str, channel: Channel) -> None:
self.to, self.channel = to, channel
@abstractmethod
def title(self) -> str: ...
@abstractmethod
def body(self) -> str: ...
def send(self) -> str:
return self.channel.deliver(self.to, self.title(), self.body())
class OrderShipped(Notification):
def __init__(self, to: str, channel: Channel, order_id: int) -> None:
super().__init__(to, channel)
self.order_id = order_id
def title(self) -> str:
return "Order shipped"
def body(self) -> str:
return f"Order {self.order_id} is on its way"
class Otp(Notification):
def __init__(self, to: str, channel: Channel, code: int) -> None:
super().__init__(to, channel)
self.code = code
def title(self) -> str:
return "Your code"
def body(self) -> str:
return f"{self.code} is valid for 5 minutes"
class Promo(Notification):
def title(self) -> str:
return "Weekend offer"
def body(self) -> str:
return "20 percent off wraps till Sunday"
def every_kind(channel: Channel) -> list:
return [OrderShipped("asha", channel, 77), Otp("asha", channel, 482913), Promo("asha", channel)]
channels = [Email(), Sms(), Push(), Chat()]
sent = [n.send() for channel in channels for n in every_kind(channel)]
for i in (0, 3, 6, 5, 11): # order shipped by email, sms and push; the promo by sms and chat
print(sent[i])
kinds = len(every_kind(channels[0]))
print(f"{len(sent)} messages: {kinds} kinds x {len(channels)} channels, {kinds + len(channels)} classes written")// Bridge.java: what is said (the kind) and how it goes out (the channel) are two hierarchies joined by one reference
import java.util.ArrayList;
import java.util.List;
class Bridge {
interface Channel { // the implementation side: how a message physically goes out
String deliver(String to, String title, String body);
}
static final class Email implements Channel {
public String deliver(String to, String title, String body) { return "[email to " + to + "] " + title + ": " + body; }
}
static final class Sms implements Channel {
public String deliver(String to, String title, String body) {
String text = title + ": " + body;
return "[sms to " + to + "] " + (text.length() > 40 ? text.substring(0, 40) : text); // a text message has a limit
}
}
static final class Push implements Channel {
public String deliver(String to, String title, String body) { return "[push to " + to + "] " + title + " / " + body; }
}
static final class Chat implements Channel { // added later: one new class, and no kind is edited
public String deliver(String to, String title, String body) { return "[chat to " + to + "] " + title + " | " + body; }
}
static abstract class Notification { // the abstraction: what is being said, holding a channel
private final String to;
private final Channel channel;
Notification(String to, Channel channel) {
this.to = to;
this.channel = channel;
}
abstract String title();
abstract String body();
String send() { return channel.deliver(to, title(), body()); }
}
static final class OrderShipped extends Notification {
private final int orderId;
OrderShipped(String to, Channel channel, int orderId) {
super(to, channel);
this.orderId = orderId;
}
String title() { return "Order shipped"; }
String body() { return "Order " + orderId + " is on its way"; }
}
static final class Otp extends Notification {
private final int code;
Otp(String to, Channel channel, int code) {
super(to, channel);
this.code = code;
}
String title() { return "Your code"; }
String body() { return code + " is valid for 5 minutes"; }
}
static final class Promo extends Notification {
Promo(String to, Channel channel) { super(to, channel); }
String title() { return "Weekend offer"; }
String body() { return "20 percent off wraps till Sunday"; }
}
static List<Notification> everyKind(Channel channel) {
return List.of(new OrderShipped("asha", channel, 77), new Otp("asha", channel, 482913), new Promo("asha", channel));
}
public static void main(String[] args) {
List<Channel> channels = List.of(new Email(), new Sms(), new Push(), new Chat());
List<String> sent = new ArrayList<>();
for (Channel channel : channels) for (Notification n : everyKind(channel)) sent.add(n.send());
for (int i : new int[] {0, 3, 6, 5, 11}) System.out.println(sent.get(i)); // order shipped by email, sms and push; the promo by sms and chat
int kinds = everyKind(channels.get(0)).size();
System.out.println(sent.size() + " messages: " + kinds + " kinds x " + channels.size() + " channels, " + (kinds + channels.size()) + " classes written");
}
}// bridge.cpp: what is said (the kind) and how it goes out (the channel) are two hierarchies joined by one reference
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <vector>
class Channel { // the implementation side: how a message physically goes out
public:
virtual ~Channel() = default;
virtual std::string deliver(const std::string& to, const std::string& title, const std::string& body) const = 0;
};
class Email : public Channel {
public:
std::string deliver(const std::string& to, const std::string& title, const std::string& body) const override { return "[email to " + to + "] " + title + ": " + body; }
};
class Sms : public Channel {
public:
std::string deliver(const std::string& to, const std::string& title, const std::string& body) const override {
return "[sms to " + to + "] " + (title + ": " + body).substr(0, 40); // a text message has a limit
}
};
class Push : public Channel {
public:
std::string deliver(const std::string& to, const std::string& title, const std::string& body) const override { return "[push to " + to + "] " + title + " / " + body; }
};
class Chat : public Channel { // added later: one new class, and no kind is edited
public:
std::string deliver(const std::string& to, const std::string& title, const std::string& body) const override { return "[chat to " + to + "] " + title + " | " + body; }
};
class Notification { // the abstraction: what is being said, holding a channel
public:
Notification(std::string to, const Channel& channel) : to_(std::move(to)), channel_(channel) {}
virtual ~Notification() = default;
virtual std::string title() const = 0;
virtual std::string body() const = 0;
std::string send() const { return channel_.deliver(to_, title(), body()); }
private:
std::string to_;
const Channel& channel_;
};
class OrderShipped : public Notification {
public:
OrderShipped(std::string to, const Channel& channel, int order_id) : Notification(std::move(to), channel), order_id_(order_id) {}
std::string title() const override { return "Order shipped"; }
std::string body() const override { return "Order " + std::to_string(order_id_) + " is on its way"; }
private:
int order_id_;
};
class Otp : public Notification {
public:
Otp(std::string to, const Channel& channel, int code) : Notification(std::move(to), channel), code_(code) {}
std::string title() const override { return "Your code"; }
std::string body() const override { return std::to_string(code_) + " is valid for 5 minutes"; }
private:
int code_;
};
class Promo : public Notification {
public:
Promo(std::string to, const Channel& channel) : Notification(std::move(to), channel) {}
std::string title() const override { return "Weekend offer"; }
std::string body() const override { return "20 percent off wraps till Sunday"; }
};
std::vector<std::unique_ptr<Notification>> every_kind(const Channel& channel) {
std::vector<std::unique_ptr<Notification>> kinds;
kinds.push_back(std::make_unique<OrderShipped>("asha", channel, 77));
kinds.push_back(std::make_unique<Otp>("asha", channel, 482913));
kinds.push_back(std::make_unique<Promo>("asha", channel));
return kinds;
}
int main() {
Email email;
Sms sms;
Push push;
Chat chat;
const std::vector<const Channel*> channels = {&email, &sms, &push, &chat};
std::vector<std::string> sent;
for (const Channel* channel : channels)
for (const auto& n : every_kind(*channel)) sent.push_back(n->send());
for (int i : {0, 3, 6, 5, 11}) std::cout << sent[i] << "\n"; // order shipped by email, sms and push; the promo by sms and chat
size_t kinds = every_kind(email).size();
std::cout << sent.size() << " messages: " << kinds << " kinds x " << channels.size() << " channels, " << kinds + channels.size() << " classes written\n";
}// bridge.js: what is said (the kind) and how it goes out (the channel) are two hierarchies joined by one reference
class Email { // the implementation side: how a message physically goes out
deliver(to, title, body) { return `[email to ${to}] ${title}: ${body}`; }
}
class Sms {
deliver(to, title, body) { return `[sms to ${to}] ` + `${title}: ${body}`.slice(0, 40); } // a text message has a limit
}
class Push {
deliver(to, title, body) { return `[push to ${to}] ${title} / ${body}`; }
}
class Chat { // added later: one new class, and no kind is edited
deliver(to, title, body) { return `[chat to ${to}] ${title} | ${body}`; }
}
class Notification { // the abstraction: what is being said, holding a channel
constructor(to, channel) {
this.to = to;
this.channel = channel;
}
send() { return this.channel.deliver(this.to, this.title(), this.body()); }
}
class OrderShipped extends Notification {
constructor(to, channel, orderId) {
super(to, channel);
this.orderId = orderId;
}
title() { return "Order shipped"; }
body() { return `Order ${this.orderId} is on its way`; }
}
class Otp extends Notification {
constructor(to, channel, code) {
super(to, channel);
this.code = code;
}
title() { return "Your code"; }
body() { return `${this.code} is valid for 5 minutes`; }
}
class Promo extends Notification {
title() { return "Weekend offer"; }
body() { return "20 percent off wraps till Sunday"; }
}
const everyKind = (channel) => [new OrderShipped("asha", channel, 77), new Otp("asha", channel, 482913), new Promo("asha", channel)];
const channels = [new Email(), new Sms(), new Push(), new Chat()];
const sent = channels.flatMap((channel) => everyKind(channel).map((n) => n.send()));
for (const i of [0, 3, 6, 5, 11]) console.log(sent[i]); // order shipped by email, sms and push; the promo by sms and chat
const kinds = everyKind(channels[0]).length;
console.log(`${sent.length} messages: ${kinds} kinds x ${channels.length} channels, ${kinds + channels.length} classes written`);[email to asha] Order shipped: Order 77 is on its way
[sms to asha] Order shipped: Order 77 is on its way
[push to asha] Order shipped / Order 77 is on its way
[sms to asha] Weekend offer: 20 percent off wraps till
[chat to asha] Weekend offer | 20 percent off wraps till Sunday
12 messages: 3 kinds x 4 channels, 7 classes writtenCheck the counts by hand. There are 3 refined abstractions and 4 concrete implementors: 7 concrete classes, plus the two base types (Notification and Channel). The 12 messages are 3 × 4, every kind through every channel, including Chat, which was added without opening any kind. The first version would need 12 concrete classes (13 with its base). At 3 × 3 the saving is modest (6 concrete classes against 9, or 8 types against 10) and grows with each addition: 5 kinds and 5 channels make 10 classes against 25.
Each language shapes the code a little differently:
- Python could pass a channel as a plain function when it has one method. Keep the class when channels have state (an API key, a rate limit).
- Java makes
Channelan interface andNotificationan abstract class, so the compiler checks both sides. - C++ holds the channel by
const Channel&, which is cheap and non-owning: the channels must outlive the notifications. Usestd::shared_ptr<const Channel>if they might not. - JavaScript needs no interface: a channel is any object with
deliver, and a test can pass a fake that records what it was given.
Adding chat to the class-per-pair design needs three new classes. With a bridge it is one new class, and no kind is edited.
Bridge, Strategy and Adapter#
Bridge is confused with two others, because the code looks the same: a class holds an interface and delegates to it. The difference is the intent and the shape.
| Bridge | Strategy (lesson 16) | Adapter (lesson 10) | |
|---|---|---|---|
| Intent | Let an abstraction and its implementation both vary, as two hierarchies | Choose one algorithm for a job, from outside | Make an existing class fit an interface |
| Shape | Two hierarchies joined by a reference. Both grow | One context, with a family of interchangeable algorithms behind one interface | One wrapper around one incompatible class |
| Here | Notification and Channel, each with several classes |
A FeePolicy held by a delivery-fee calculator |
AlphaAdapter around a vendor's SDK |
The book's line with Adapter: Adapter makes unrelated classes work together and is usually applied after the design, while Bridge is used up front so the two sides can vary independently. Against Strategy, ask one question: are there two sets of classes, each growing? One context with one swappable rule is Strategy, even if the code looks like a bridge.
Flyweight: the first version, a copy for every seat#
The second problem is a seat map. A big hall has 40 rows of 50 seats: 2,000 seats. Rows 0 to 4 are recliners, rows 5 to 14 are premium, and the rest are regular. In the first version every seat builds its own details when it is created:
# seats_first_version.py: every seat builds its own copy of what its kind means
TABLE = {"regular": (200, 80), "premium": (350, 95), "recliner": (600, 120)} # price in rupees, legroom in cm
def kind_name(row: int) -> str: # rows 0-4 are recliners, rows 5-14 premium, the rest regular
return "recliner" if row < 5 else "premium" if row < 15 else "regular"
class Details: # what a kind of seat means: identical for every seat of that kind
created = 0
def __init__(self, name: str, price: int, legroom: int) -> None:
self.name, self.price, self.legroom = name, price, legroom
Details.created += 1
class Seat:
def __init__(self, row: int, number: int) -> None:
price, legroom = TABLE[kind_name(row)]
self.row, self.number = row, number
self.details = Details(kind_name(row), price, legroom) # a new copy for every seat
seats = [Seat(row, number) for row in range(40) for number in range(50)]
print(f"seats: {len(seats)}")
print(f"details objects: {Details.created}")
print(f"two regular seats share their details: {'yes' if seats[-1].details is seats[-2].details else 'no'}")
first = seats[0]
print(f"row {first.row} seat {first.number}: {first.details.name}, {first.details.price} rupees, {first.details.legroom} cm")// SeatsFirstVersion.java: every seat builds its own copy of what its kind means
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
class SeatsFirstVersion {
static final Map<String, int[]> TABLE = Map.of("regular", new int[] {200, 80}, "premium", new int[] {350, 95}, "recliner", new int[] {600, 120}); // price in rupees, legroom in cm
static String kindName(int row) { return row < 5 ? "recliner" : row < 15 ? "premium" : "regular"; } // rows 0-4 are recliners, rows 5-14 premium, the rest regular
static final class Details { // what a kind of seat means: identical for every seat of that kind
static int created = 0;
final String name;
final int price, legroom;
Details(String name, int price, int legroom) {
this.name = name;
this.price = price;
this.legroom = legroom;
created++;
}
}
static final class Seat {
final int row, number;
final Details details;
Seat(int row, int number) {
int[] spec = TABLE.get(kindName(row));
this.row = row;
this.number = number;
this.details = new Details(kindName(row), spec[0], spec[1]); // a new copy for every seat
}
}
public static void main(String[] args) {
List<Seat> seats = new ArrayList<>();
for (int row = 0; row < 40; row++) for (int number = 0; number < 50; number++) seats.add(new Seat(row, number));
System.out.println("seats: " + seats.size());
System.out.println("details objects: " + Details.created);
System.out.println("two regular seats share their details: " + (seats.get(seats.size() - 1).details == seats.get(seats.size() - 2).details ? "yes" : "no"));
Seat first = seats.get(0);
System.out.println("row " + first.row + " seat " + first.number + ": " + first.details.name + ", " + first.details.price + " rupees, " + first.details.legroom + " cm");
}
}// seats_first_version.cpp: every seat builds its own copy of what its kind means
#include <iostream>
#include <map>
#include <string>
#include <utility>
#include <vector>
const std::map<std::string, std::pair<int, int>> TABLE = {{"regular", {200, 80}}, {"premium", {350, 95}}, {"recliner", {600, 120}}}; // price in rupees, legroom in cm
std::string kind_name(int row) { return row < 5 ? "recliner" : row < 15 ? "premium" : "regular"; } // rows 0-4 are recliners, rows 5-14 premium, the rest regular
struct Details { // what a kind of seat means: identical for every seat of that kind
static int created;
std::string name;
int price, legroom;
Details(std::string n, int p, int l) : name(std::move(n)), price(p), legroom(l) { ++created; }
};
int Details::created = 0;
struct Seat {
int row, number;
Details details; // a new copy for every seat
Seat(int r, int n) : row(r), number(n), details(kind_name(r), TABLE.at(kind_name(r)).first, TABLE.at(kind_name(r)).second) {}
};
int main() {
std::vector<Seat> seats;
seats.reserve(2000);
for (int row = 0; row < 40; ++row)
for (int number = 0; number < 50; ++number) seats.emplace_back(row, number);
std::cout << "seats: " << seats.size() << "\n";
std::cout << "details objects: " << Details::created << "\n";
std::cout << "two regular seats share their details: " << (&seats[seats.size() - 1].details == &seats[seats.size() - 2].details ? "yes" : "no") << "\n";
const Seat& first = seats[0];
std::cout << "row " << first.row << " seat " << first.number << ": " << first.details.name << ", " << first.details.price << " rupees, " << first.details.legroom << " cm\n";
}// seatsFirstVersion.js: every seat builds its own copy of what its kind means
const TABLE = { regular: [200, 80], premium: [350, 95], recliner: [600, 120] }; // price in rupees, legroom in cm
const kindName = (row) => (row < 5 ? "recliner" : row < 15 ? "premium" : "regular"); // rows 0-4 are recliners, rows 5-14 premium, the rest regular
let created = 0;
class Details { // what a kind of seat means: identical for every seat of that kind
constructor(name, price, legroom) {
Object.assign(this, { name, price, legroom });
created++;
}
}
class Seat {
constructor(row, number) {
const [price, legroom] = TABLE[kindName(row)];
this.row = row;
this.number = number;
this.details = new Details(kindName(row), price, legroom); // a new copy for every seat
}
}
const seats = [];
for (let row = 0; row < 40; row++) for (let number = 0; number < 50; number++) seats.push(new Seat(row, number));
console.log(`seats: ${seats.length}`);
console.log(`details objects: ${created}`);
console.log(`two regular seats share their details: ${seats[seats.length - 1].details === seats[seats.length - 2].details ? "yes" : "no"}`);
const first = seats[0];
console.log(`row ${first.row} seat ${first.number}: ${first.details.name}, ${first.details.price} rupees, ${first.details.legroom} cm`);seats: 2000
details objects: 2000
two regular seats share their details: no
row 0 seat 0: recliner, 600 rupees, 120 cmThere are 2,000 seats and 2,000 details objects, though only 3 kinds of seat. Every regular seat has its own copy of regular, 200 rupees, 80 cm, and the last two regular seats don't share theirs. The data is the same for all 1,250 regular seats, and it is stored 1,250 times.
Flyweight: the pattern#
The book states the intent of Flyweight as: "Use sharing to support large numbers of fine-grained objects efficiently." The key is to split each object's state in two:
- Intrinsic state is stored in the shared object and doesn't depend on where it is used: the kind's name, price and legroom. It is the same wherever a recliner appears.
- Extrinsic state depends on the context and can't be shared: which row and number this seat is, and whether it is sold. The client stores it or passes it in.
| Role | In the seat map | Its one job |
|---|---|---|
| Flyweight | SeatKind |
Holds the intrinsic state. Immutable, so it is safe to share |
| Flyweight factory | SeatKinds.get(name) |
Creates a SeatKind the first time a name is asked for and hands the same one out afterwards. Clients don't construct flyweights directly |
| Context | Seat, SeatMap |
Hold the extrinsic state (row, number, the sold set) and a reference to the shared kind |
| Client | The seat-map screen | Gets kinds from the factory and never builds one |
%% title: Flyweight: many seats point at a few shared kinds, and the factory makes sure there are only a few
classDiagram
class SeatKind {
+name str
+price int
+legroom int
}
class SeatKinds {
+get(name) SeatKind
}
class Seat {
+row int
+number int
}
class SeatMap {
-sold set
+sell(row, number)
+revenue() int
}
SeatKinds o-- "3" SeatKind : one per name
Seat --> SeatKind : shares
SeatMap *-- "2000" Seat : holds
SeatMap --> SeatKinds : asks for kindsRead the arrows. 2,000 seats point at 3 kinds (the number on the factory's arrow is its cache), and a seat holds only a reference to its kind. What differs per seat sits in the seat or, for the sold flag, in the map's own set. Nothing in SeatKind knows where it is used, which is what makes sharing safe.
Intrinsic state is the same for every seat of a kind and lives in one shared, immutable object. Extrinsic state, like the sold flag, stays outside.
Flyweight: the code#
Here is the refactor in all four languages. The kind is immutable (a frozen dataclass, a record, Object.freeze, const members), the factory is the only way to get one, and the sold flag stays outside it:
# flyweight.py: the shared part of a seat is one immutable SeatKind; where the seat is stays with the seat
from dataclasses import dataclass
TABLE = {"regular": (200, 80), "premium": (350, 95), "recliner": (600, 120)} # price in rupees, legroom in cm
def kind_name(row: int) -> str: # rows 0-4 are recliners, rows 5-14 premium, the rest regular
return "recliner" if row < 5 else "premium" if row < 15 else "regular"
@dataclass(frozen=True)
class SeatKind: # the flyweight: intrinsic state only, and it can't be changed
name: str
price: int
legroom: int
class SeatKinds: # the flyweight factory: one SeatKind per name, handed out again and again
def __init__(self, table) -> None:
self.table, self.made = table, {}
def get(self, name: str) -> SeatKind:
if name not in self.made:
price, legroom = self.table[name]
self.made[name] = SeatKind(name, price, legroom)
return self.made[name]
class Seat: # extrinsic state (where it is) lives here; the kind is only a reference
def __init__(self, row: int, number: int, kind: SeatKind) -> None:
self.row, self.number, self.kind = row, number, kind
class SeatMap:
def __init__(self, rows: int, per_row: int, kinds: SeatKinds) -> None:
self.seats = [Seat(r, n, kinds.get(kind_name(r))) for r in range(rows) for n in range(per_row)]
self.sold = set() # sold or not is extrinsic too: it differs per seat, so it stays outside the shared kind
def sell(self, row: int, number: int) -> None:
self.sold.add((row, number))
def revenue(self) -> int:
return sum(s.kind.price for s in self.seats if (s.row, s.number) in self.sold)
kinds = SeatKinds(TABLE)
seat_map = SeatMap(40, 50, kinds)
seats = seat_map.seats
print(f"seats: {len(seats)}")
print(f"kind objects: {len(kinds.made)}")
print(f"two regular seats share their kind: {'yes' if seats[-1].kind is seats[-2].kind else 'no'}")
first = seats[0]
print(f"row {first.row} seat {first.number}: {first.kind.name}, {first.kind.price} rupees, {first.kind.legroom} cm")
for row, number in ((0, 0), (0, 1), (10, 3), (20, 5)):
seat_map.sell(row, number)
print(f"sold {len(seat_map.sold)} seats, revenue {seat_map.revenue()}")// Flyweight.java: the shared part of a seat is one immutable SeatKind; where the seat is stays with the seat
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
class Flyweight {
static final Map<String, int[]> TABLE = Map.of("regular", new int[] {200, 80}, "premium", new int[] {350, 95}, "recliner", new int[] {600, 120}); // price in rupees, legroom in cm
static String kindName(int row) { return row < 5 ? "recliner" : row < 15 ? "premium" : "regular"; } // rows 0-4 are recliners, rows 5-14 premium, the rest regular
record SeatKind(String name, int price, int legroom) {} // the flyweight: intrinsic state only, and a record can't be changed
static final class SeatKinds { // the flyweight factory: one SeatKind per name, handed out again and again
private final Map<String, int[]> table;
private final Map<String, SeatKind> made = new HashMap<>();
SeatKinds(Map<String, int[]> table) { this.table = table; }
SeatKind get(String name) { return made.computeIfAbsent(name, n -> new SeatKind(n, table.get(n)[0], table.get(n)[1])); }
int made() { return made.size(); }
}
record Seat(int row, int number, SeatKind kind) {} // extrinsic state (where it is) lives here; the kind is only a reference
static final class SeatMap {
final List<Seat> seats = new ArrayList<>();
final Set<String> sold = new HashSet<>(); // sold or not is extrinsic too: it differs per seat, so it stays outside the shared kind
SeatMap(int rows, int perRow, SeatKinds kinds) {
for (int r = 0; r < rows; r++) for (int n = 0; n < perRow; n++) seats.add(new Seat(r, n, kinds.get(kindName(r))));
}
void sell(int row, int number) { sold.add(row + ":" + number); }
int revenue() {
int total = 0;
for (Seat s : seats) if (sold.contains(s.row() + ":" + s.number())) total += s.kind().price();
return total;
}
}
public static void main(String[] args) {
SeatKinds kinds = new SeatKinds(TABLE);
SeatMap seatMap = new SeatMap(40, 50, kinds);
List<Seat> seats = seatMap.seats;
System.out.println("seats: " + seats.size());
System.out.println("kind objects: " + kinds.made());
System.out.println("two regular seats share their kind: " + (seats.get(seats.size() - 1).kind() == seats.get(seats.size() - 2).kind() ? "yes" : "no"));
Seat first = seats.get(0);
System.out.println("row " + first.row() + " seat " + first.number() + ": " + first.kind().name() + ", " + first.kind().price() + " rupees, " + first.kind().legroom() + " cm");
int[][] sales = {{0, 0}, {0, 1}, {10, 3}, {20, 5}};
for (int[] s : sales) seatMap.sell(s[0], s[1]);
System.out.println("sold " + seatMap.sold.size() + " seats, revenue " + seatMap.revenue());
}
}// flyweight.cpp: the shared part of a seat is one immutable SeatKind; where the seat is stays with the seat
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <utility>
#include <vector>
const std::map<std::string, std::pair<int, int>> TABLE = {{"regular", {200, 80}}, {"premium", {350, 95}}, {"recliner", {600, 120}}}; // price in rupees, legroom in cm
std::string kind_name(int row) { return row < 5 ? "recliner" : row < 15 ? "premium" : "regular"; } // rows 0-4 are recliners, rows 5-14 premium, the rest regular
struct SeatKind { // the flyweight: intrinsic state only, and every member is const
const std::string name;
const int price;
const int legroom;
};
class SeatKinds { // the flyweight factory: one SeatKind per name, handed out again and again
std::map<std::string, std::pair<int, int>> table_;
std::map<std::string, SeatKind> made_; // map nodes never move, so a pointer to one stays valid
public:
explicit SeatKinds(std::map<std::string, std::pair<int, int>> table) : table_(std::move(table)) {}
const SeatKind* get(const std::string& name) {
auto it = made_.find(name);
if (it == made_.end()) it = made_.emplace(name, SeatKind{name, table_.at(name).first, table_.at(name).second}).first;
return &it->second;
}
size_t made() const { return made_.size(); }
};
struct Seat { // extrinsic state (where it is) lives here; the kind is only a pointer
int row, number;
const SeatKind* kind;
};
class SeatMap {
public:
std::vector<Seat> seats;
std::set<std::pair<int, int>> sold; // sold or not is extrinsic too: it differs per seat, so it stays outside the shared kind
SeatMap(int rows, int per_row, SeatKinds& kinds) {
for (int r = 0; r < rows; ++r)
for (int n = 0; n < per_row; ++n) seats.push_back({r, n, kinds.get(kind_name(r))});
}
void sell(int row, int number) { sold.insert({row, number}); }
int revenue() const {
int total = 0;
for (const Seat& s : seats)
if (sold.count({s.row, s.number})) total += s.kind->price;
return total;
}
};
int main() {
SeatKinds kinds(TABLE);
SeatMap seat_map(40, 50, kinds);
const std::vector<Seat>& seats = seat_map.seats;
std::cout << "seats: " << seats.size() << "\n";
std::cout << "kind objects: " << kinds.made() << "\n";
std::cout << "two regular seats share their kind: " << (seats[seats.size() - 1].kind == seats[seats.size() - 2].kind ? "yes" : "no") << "\n";
const Seat& first = seats[0];
std::cout << "row " << first.row << " seat " << first.number << ": " << first.kind->name << ", " << first.kind->price << " rupees, " << first.kind->legroom << " cm\n";
for (auto [row, number] : {std::pair<int, int>{0, 0}, {0, 1}, {10, 3}, {20, 5}}) seat_map.sell(row, number);
std::cout << "sold " << seat_map.sold.size() << " seats, revenue " << seat_map.revenue() << "\n";
}// flyweight.js: the shared part of a seat is one immutable SeatKind; where the seat is stays with the seat
const TABLE = { regular: [200, 80], premium: [350, 95], recliner: [600, 120] }; // price in rupees, legroom in cm
const kindName = (row) => (row < 5 ? "recliner" : row < 15 ? "premium" : "regular"); // rows 0-4 are recliners, rows 5-14 premium, the rest regular
class SeatKinds { // the flyweight factory: one frozen SeatKind per name, handed out again and again
constructor(table) {
this.table = table;
this.made = new Map();
}
get(name) {
if (!this.made.has(name)) {
const [price, legroom] = this.table[name];
this.made.set(name, Object.freeze({ name, price, legroom })); // the flyweight: intrinsic state only, and frozen
}
return this.made.get(name);
}
}
class Seat { // extrinsic state (where it is) lives here; the kind is only a reference
constructor(row, number, kind) {
Object.assign(this, { row, number, kind });
}
}
class SeatMap {
constructor(rows, perRow, kinds) {
this.seats = [];
for (let r = 0; r < rows; r++) for (let n = 0; n < perRow; n++) this.seats.push(new Seat(r, n, kinds.get(kindName(r))));
this.sold = new Set(); // sold or not is extrinsic too: it differs per seat, so it stays outside the shared kind
}
sell(row, number) { this.sold.add(`${row}:${number}`); }
revenue() { return this.seats.filter((s) => this.sold.has(`${s.row}:${s.number}`)).reduce((sum, s) => sum + s.kind.price, 0); }
}
const kinds = new SeatKinds(TABLE);
const seatMap = new SeatMap(40, 50, kinds);
const seats = seatMap.seats;
console.log(`seats: ${seats.length}`);
console.log(`kind objects: ${kinds.made.size}`);
console.log(`two regular seats share their kind: ${seats[seats.length - 1].kind === seats[seats.length - 2].kind ? "yes" : "no"}`);
const first = seats[0];
console.log(`row ${first.row} seat ${first.number}: ${first.kind.name}, ${first.kind.price} rupees, ${first.kind.legroom} cm`);
for (const [row, number] of [[0, 0], [0, 1], [10, 3], [20, 5]]) seatMap.sell(row, number);
console.log(`sold ${seatMap.sold.size} seats, revenue ${seatMap.revenue()}`);seats: 2000
kind objects: 3
two regular seats share their kind: yes
row 0 seat 0: recliner, 600 rupees, 120 cm
sold 4 seats, revenue 1750Check it by hand. There are still 2,000 seats but 3 kind objects, and the last two regular seats point at the same one. Four seats are sold: two recliners at 600, a premium at 350 and a regular at 200, so 600 + 600 + 350 + 200 = 1,750. Each price came from the shared kind, and which seats were sold came from the set outside it. That split is the pattern: one object answers "what is a recliner?" for every seat, and the seat map separately answers "which seats are taken?".
How much did it save? Measured with tracemalloc on CPython 3.9.6 (macOS, arm64), building each design and counting the seats, what they own and the list that holds them: about 313 bytes per seat against about 160, which is 610 KiB against 311 KiB for 2,000 seats and about 60 MiB against 31 MiB for 200,000. The same code on CPython 3.12 takes about 187 bytes against 98, because newer versions store an object's attributes more compactly. So the bytes move with the version, and the ratio, about half, holds. Half and no more, because each seat is still an object with two numbers and a reference. At 2,000 seats the saving is 300 KiB, which nobody would notice. The pattern pays at hundreds of thousands or millions of objects, when the shared part is large against the per-object part. These are CPython figures only: Java, C++ and JavaScript lay objects out differently, so measure there before you claim a number.
What each language brings:
- Python gets immutability from
@dataclass(frozen=True): assigning to a field raisesFrozenInstanceError. - Java uses a
record, whose fields can't be reassigned, andcomputeIfAbsentfor the factory. Threads sharing a factory need a concurrent map. An immutable flyweight is safe to share with no lock. - C++ makes every member
constand holds a plainconst SeatKind*. The kinds must outlive the seats, andstd::mapnodes never move, so a pointer into one stays valid. - JavaScript uses
Object.freeze. In strict mode (modules, class bodies) assigning to a frozen field throwsTypeError, and in sloppy mode it is silently ignored.
Flyweight: what can go wrong#
The first trap is the one that bites. This program shares a kind that is not immutable and applies "an offer for this one seat" by writing to it. Then it does it properly:
# shared_state.py: a mutable flyweight is shared by 1,250 seats, so one change reaches all of them
class Kind: # not frozen: that is the bug
def __init__(self, name: str, price: int) -> None:
self.name, self.price = name, price
regular = Kind("regular", 200)
seats = [(row, number, regular) for row in range(25) for number in range(50)] # 1,250 regular seats share one kind
seats[0][2].price = 100 # "an offer for this one seat"
print(f"mutable kind: seats at 100 after one offer: {sum(1 for s in seats if s[2].price == 100)}")
kind = Kind("regular", 200) # fresh again, and this time it is never changed
offers = {(0, 0): 100} # a per-seat exception is extrinsic: it is kept outside the shared kind
def price_of(row: int, number: int) -> int:
return offers.get((row, number), kind.price)
at_100 = sum(1 for row in range(25) for number in range(50) if price_of(row, number) == 100)
print(f"extrinsic offer: seats at 100: {at_100}, the kind still costs {kind.price}")// SharedState.java: a mutable flyweight is shared by 1,250 seats, so one change reaches all of them
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class SharedState {
static final class Kind { // price isn't final: that is the bug
final String name;
int price;
Kind(String name, int price) {
this.name = name;
this.price = price;
}
}
record Seat(int row, int number, Kind kind) {}
public static void main(String[] args) {
Kind regular = new Kind("regular", 200);
List<Seat> seats = new ArrayList<>();
for (int row = 0; row < 25; row++) for (int number = 0; number < 50; number++) seats.add(new Seat(row, number, regular)); // 1,250 regular seats share one kind
seats.get(0).kind().price = 100; // "an offer for this one seat"
System.out.println("mutable kind: seats at 100 after one offer: " + seats.stream().filter(s -> s.kind().price == 100).count());
Kind kind = new Kind("regular", 200); // fresh again, and this time it is never changed
Map<String, Integer> offers = new HashMap<>();
offers.put("0:0", 100); // a per-seat exception is extrinsic: it is kept outside the shared kind
int at100 = 0;
for (int row = 0; row < 25; row++) for (int number = 0; number < 50; number++) if (offers.getOrDefault(row + ":" + number, kind.price) == 100) at100++;
System.out.println("extrinsic offer: seats at 100: " + at100 + ", the kind still costs " + kind.price);
}
}// shared_state.cpp: a mutable flyweight is shared by 1,250 seats, so one change reaches all of them
#include <iostream>
#include <map>
#include <string>
#include <utility>
#include <vector>
struct Kind { // not const: that is the bug
std::string name;
int price;
};
struct Seat {
int row, number;
Kind* kind; // a non-const pointer lets any seat change the shared kind
};
int main() {
Kind regular{"regular", 200};
std::vector<Seat> seats;
for (int row = 0; row < 25; ++row)
for (int number = 0; number < 50; ++number) seats.push_back({row, number, ®ular}); // 1,250 regular seats share one kind
seats[0].kind->price = 100; // "an offer for this one seat"
int shared = 0;
for (const Seat& s : seats) shared += s.kind->price == 100;
std::cout << "mutable kind: seats at 100 after one offer: " << shared << "\n";
Kind kind{"regular", 200}; // fresh again, and this time it is never changed
std::map<std::pair<int, int>, int> offers = {{{0, 0}, 100}}; // a per-seat exception is extrinsic: it is kept outside the shared kind
int at_100 = 0;
for (int row = 0; row < 25; ++row)
for (int number = 0; number < 50; ++number) {
auto it = offers.find({row, number});
at_100 += (it != offers.end() ? it->second : kind.price) == 100;
}
std::cout << "extrinsic offer: seats at 100: " << at_100 << ", the kind still costs " << kind.price << "\n";
}// sharedState.js: a mutable flyweight is shared by 1,250 seats, so one change reaches all of them
class Kind { // not frozen: that is the bug
constructor(name, price) {
this.name = name;
this.price = price;
}
}
const regular = new Kind("regular", 200);
const seats = [];
for (let row = 0; row < 25; row++) for (let number = 0; number < 50; number++) seats.push({ row, number, kind: regular }); // 1,250 regular seats share one kind
seats[0].kind.price = 100; // "an offer for this one seat"
console.log(`mutable kind: seats at 100 after one offer: ${seats.filter((s) => s.kind.price === 100).length}`);
const kind = new Kind("regular", 200); // fresh again, and this time it is never changed
const offers = new Map([["0:0", 100]]); // a per-seat exception is extrinsic: it is kept outside the shared kind
let at100 = 0;
for (let row = 0; row < 25; row++) for (let number = 0; number < 50; number++) if ((offers.get(`${row}:${number}`) ?? kind.price) === 100) at100++;
console.log(`extrinsic offer: seats at 100: ${at100}, the kind still costs ${kind.price}`);mutable kind: seats at 100 after one offer: 1250
extrinsic offer: seats at 100: 1, the kind still costs 200Setting the price through one seat changed the one object that all 1,250 regular seats share, so every regular seat in the hall cost 100. The fix has two parts. Make the kind immutable, so the write can't happen. And keep the exception outside it: a per-seat offer is extrinsic state, in a map keyed by row and number, and a seat's price is the offer if there is one and the kind's price if not. After the fix, one seat is at 100 and the kind still costs 200.
Three more traps are quieter:
- Identity. Two seats' kinds are the same object, so code that uses identity as meaning (a lock on a kind, a map keyed by a kind's reference) treats all 1,250 seats as one.
- The factory is a cache and can grow. Three kinds are fine. A glyph factory with an object for every style a user types grows without bound unless an entry can go when no one uses it:
weakref.WeakValueDictionaryin Python, and in Java a map whose values areWeakReferences (a plainWeakHashMapholds its keys weakly, which is not the same thing). - Threads. Two threads asking for a new kind at once must not make two: use a concurrent map's
computeIfAbsent, a lock, or build every kind at start-up.
Setting a price through one seat changes the one kind that all 1,250 regular seats share. Freeze the kind, and keep a per-seat offer outside it.
Interning: a flyweight you already use#
String interning is the same idea built into the language: equal strings share one object. What is guaranteed and what is an implementation detail differs by language, which makes a good interview answer. Each claim below was run (Java 23, CPython 3.9, Node 22), except C++'s, which is the standard's wording.
- Java pools string literals:
"hello" == "hello"is true,new String("hello") == "hello"is false, and.intern()returns the pooled copy.Integer.valueOfcaches −128 to 127 by default, so==is true for 127 and false for 128. Compare withequals, since==tests identity. - Python has
sys.intern(s). CPython also interns some strings and small integers (−5 to 256) on its own, soa is bcan be true for equal literals, but that is an implementation detail, not a promise. Use==, andisonly forNone. - JavaScript compares strings by value, so interning is not observable. A symbol differs:
Symbol.for("a") === Symbol.for("a")is true, because the global registry is a flyweight factory, andSymbol("a") === Symbol("a")is false. - C++ guarantees nothing: whether two identical string literals are one object is unspecified, and
std::stringnever interns. Build the factory yourself, as the seat map does.
An enum is the smallest flyweight: one object per value. The movie-booking lesson's SeatType is one.
Flyweight and its look-alikes#
| Pattern | What it does | How to tell it apart |
|---|---|---|
| Singleton | Exactly one instance of a class | A factory makes one per distinct intrinsic value: three kinds, not one |
| Prototype | Makes a copy to start from | The opposite move: Flyweight shares and never copies |
| Composite | A tree of parts, treated alike | The leaves of a composite are often flyweights, but a flyweight can't hold a parent pointer: where it sits is extrinsic |
| Object pool | Reuses expensive mutable objects, one user at a time | A flyweight is immutable and used by many at once, with nothing checked out or returned |
When not to use them#
- Bridge, when only one side varies. One channel and many kinds is a base class with subclasses. A bridge for a dimension that never grows is a second hierarchy to maintain for nothing.
- Bridge, when it is really Strategy. One context with one swappable rule needs the smaller pattern.
- Bridge, when the interface between the sides keeps changing. If every new kind needs a new
deliverparameter, the split is in the wrong place. - Flyweight, when there are few objects. At 2,000 seats the saving is 300 KiB. Measure first: sharing adds a factory, an immutability rule and extrinsic state to track.
- Flyweight, when the shared part is small or the state can't be split. A reference costs 4 or 8 bytes, so replacing two small numbers with one saves next to nothing, and mostly unique state has nothing to share.
- Flyweight, when identity matters. If code locks on an object or tells two objects apart by reference, sharing breaks it.
Where they show up in the case studies#
Neither pattern is used by name in the 22 rounds. The two places the plan names are where Flyweight would fit, and what they have today is worth saying precisely.
| Case study | What is there | What to say |
|---|---|---|
| Movie booking | Seat is a frozen dataclass holding a SeatType enum, and prices sit in a table on the Show keyed by SeatType |
The shared part is already shared: an enum is one object per value, and the price is looked up by kind and not copied into each seat. That is a flyweight in spirit, at the size of 8 seats, where it saves nothing. For a 2,000-seat hall with richer per-kind data it would be this lesson's SeatKind |
| Text editor | Document.text is one str |
There are no per-character objects, so nothing to share. The book's own example is an editor that does make an object per character: one shared flyweight per letter, holding only the character code, with its position and font passed in as extrinsic state |
In the interview#
Say each with its catch. Bridge: "Bridge splits an abstraction from its implementation so both vary independently: M kinds and N channels need M + N classes, not M × N. Use it up front, and only when both sides keep growing." Flyweight: "Flyweight shares the intrinsic, immutable state of many small objects through a factory, and keeps the extrinsic state outside. It pays only at large counts, and the shared state must never change."
Then expect the follow-ups:
- "Bridge or Strategy?" Same code shape, different intent. Bridge has two hierarchies that both grow. Strategy has one context with a swappable rule.
- "Bridge or Adapter?" Adapter fits an existing class to an interface, after the fact. Bridge is designed in from the start.
- "How many classes?" Bridge: M + N concrete classes plus two base types. One class per pair: M × N plus a base.
- "Intrinsic or extrinsic?" Intrinsic is shared and independent of context. Extrinsic depends on context, and the client holds it.
- "Why must a flyweight be immutable?" A write through one user changes every user, as the 1,250 seats show.
- "Where is Flyweight in a language?" String interning,
Integer.valueOf's cache,Symbol.for, and enums.
Common mistakes#
- Calling any delegation a Bridge. One class holding one swappable interface is Strategy. Bridge needs two hierarchies.
- Saying "abstraction" means an abstract class. In Bridge it is a role: the side the client uses.
- Counting classes wrongly. M × N against M + N, and say whether the base types are included.
- A mutable flyweight, or extrinsic state inside one (the sold flag, the row). One write reaches every user.
- Claiming savings without measuring. Half on CPython is not a promise for other languages.
- Relying on
==and interning for strings or boxed numbers. It works by accident for literals and small values, and not elsewhere.
Bridge and Flyweight
Q1/4 A system has 4 kinds of notification and 5 channels. Counting concrete classes only, how many does one class per pair need, and how many does Bridge need?
Answer & explanation
Answer: C
One class per pair is a product, 4 × 5 = 20. Bridge keeps the two dimensions in two hierarchies, so the count is a sum, 4 + 5 = 9, and any kind can use any channel. The two base types are extra in both designs.
Q2/4 A context holds an interface and delegates to it. It has a single rule that is swapped at run time, and no hierarchy of its own. Which pattern is it?
Answer & explanation
Answer: B
The code shape is the same as a bridge, so the intent decides. Bridge has two hierarchies that both keep growing. One swappable rule on one context is Strategy.
Q3/4 In the seat map, which state belongs in the shared
SeatKind?Answer & explanation
Answer: C
Shared state must be the same for every user and independent of context. Name, price and legroom are. The row, the sold flag and a one-seat offer differ per seat, so they are extrinsic and stay outside the kind.
Q4/4 A teammate gives one seat an offer with
seat.kind.price = 100, and all 1,250 regular seats now cost 100. What is the lesson?Answer & explanation
Answer: C
The write went to the one object that every regular seat points at. Make the kind immutable so the write can't happen, and keep the exception in a map keyed by seat. A new kind per seat would undo the sharing.
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.


