Bridge
Bridge separates two things that vary independently, so each can grow without multiplying classes. This page splits Pedal's operations alerts (what to say) from delivery channels (how to send it), and counts the classes the split saves.
1. The pain
Pedal's operations team gets alerts: a bike's battery is low, a bike moved without a ride (probably stolen), a dock's lock jammed. Each can go out by SMS, phone push or email, depending on who's on shift. The first version made a class for every combination: TheftSms, TheftPush, LowBatteryEmail…
Three alerts and three channels made nine classes. Changing how SMS marks urgency meant editing three of them. Adding a pager channel meant three more, and the next alert kind would mean four. Two independent things were glued into one class hierarchy, so it grew as a product instead of a sum.
2. The idea in one picture
Bridge splits one hierarchy into two that vary independently, joined by composition. The abstraction (what an alert says, and whether it's urgent) holds a reference to an implementation (how a message travels). Theft decides its text and that it's urgent; Push decides that urgent means "priority high". Either side can grow without touching the other.
Bridge is usually designed up front, when you can see two dimensions of variation coming. Adapter, which it resembles, is usually added later to make existing pieces fit.
3. The roles
| Role | Its job | In Pedal |
|---|---|---|
| Abstraction | The high-level side; holds an implementor and delegates the low-level work to it. | Alert with send() |
| Refined abstraction | Variants of the high-level side. | LowBattery, Theft, DockFault |
| Implementor | The interface for the low-level side. | Channel.deliver(to, text, urgent) |
| Concrete implementor | One way of doing the low-level work. | Sms, Push, Email |
4. Before and after
The alert classes both ways (Python sketch; the full program is in section 5).
Without the pattern
class TheftSms(Alert): ...
class TheftPush(Alert): ...
class TheftEmail(Alert): ...
class LowBatterySms(Alert): ...
class LowBatteryPush(Alert): ...
class LowBatteryEmail(Alert): ...
class DockFaultSms(Alert): ...
class DockFaultPush(Alert): ...
class DockFaultEmail(Alert): ...
# a pager channel: three more classesWith it
class Alert(ABC):
def __init__(self, channel, to):
self.channel, self.to = channel, to
def send(self):
return self.channel.deliver(
self.to, self.text(), self.urgent())
Theft(Push(), "ops-app", "B-17").send()
Theft(Sms(), phone, "B-17").send()| The next change | Without the pattern | With it |
|---|---|---|
| Add a pager channel | One class per alert kind (3) | One Pager class |
| Change how SMS marks urgency | Edit every …Sms class (3) | Edit Sms.deliver |
| Route an alert by shift (SMS at night) | Pick a different class at runtime | Pass a different channel |
Two dimensions in two hierarchies grow as a sum (alerts + channels), not a product (alerts × channels).
5. The code
Three alert kinds hold one of three channels. The same theft alert goes out by push and by SMS, and each channel shows urgency in its own way.
from __future__ import annotations
from abc import ABC, abstractmethod
# ---- One side of the bridge: how a message travels. ----
class Channel(ABC):
@abstractmethod
def deliver(self, to: str, text: str, urgent: bool) -> str: ...
class Sms(Channel):
def deliver(self, to: str, text: str, urgent: bool) -> str:
return f"sms {to}: {'URGENT ' if urgent else ''}{text}"
class Push(Channel):
def deliver(self, to: str, text: str, urgent: bool) -> str:
return f"push {to} [priority {'high' if urgent else 'normal'}]: {text}"
class Email(Channel):
def deliver(self, to: str, text: str, urgent: bool) -> str:
return f"email {to}: {'[URGENT] ' if urgent else ''}{text}"
# ---- The other side: what is being said. Each alert holds a channel instead of being one. ----
class Alert(ABC):
def __init__(self, channel: Channel, to: str) -> None:
self.channel, self.to = channel, to
def send(self) -> str:
return f"{self.title()} -> " + self.channel.deliver(self.to, self.text(), self.urgent())
@abstractmethod
def title(self) -> str: ...
@abstractmethod
def text(self) -> str: ...
@abstractmethod
def urgent(self) -> bool: ...
class LowBattery(Alert):
def __init__(self, channel: Channel, to: str, bike: str, percent: int) -> None:
super().__init__(channel, to)
self.bike, self.percent = bike, percent
def title(self) -> str: return f"low battery {self.bike}"
def text(self) -> str: return f"{self.bike} battery at {self.percent}%"
def urgent(self) -> bool: return self.percent < 15
class Theft(Alert):
def __init__(self, channel: Channel, to: str, bike: str) -> None:
super().__init__(channel, to)
self.bike = bike
def title(self) -> str: return f"theft {self.bike}"
def text(self) -> str: return f"{self.bike} moved without a ride"
def urgent(self) -> bool: return True
class DockFault(Alert):
def __init__(self, channel: Channel, to: str, dock: str, problem: str) -> None:
super().__init__(channel, to)
self.dock, self.problem = dock, problem
def title(self) -> str: return f"dock fault {self.dock}"
def text(self) -> str: return f"{self.dock} {self.problem}"
def urgent(self) -> bool: return False
if __name__ == "__main__":
phone, app, inbox = "+44 7700 900123", "ops-app", "ops@pedal.example"
alerts: list[Alert] = [
LowBattery(Sms(), phone, "E-205", 31),
Theft(Push(), app, "B-17"),
DockFault(Email(), inbox, "D9", "lock jammed"),
Theft(Sms(), phone, "B-17"), # same alert, another channel
LowBattery(Push(), app, "E-311", 9), # same channel, urgency decided by the alert
]
for a in alerts:
print(a.send())
print("classes: 3 alerts + 3 channels = 6 (one class per pair would need 9)")
// ---- One side of the bridge: how a message travels. ----
interface Channel { deliver(to: string, text: string, urgent: boolean): string; }
class Sms implements Channel {
deliver(to: string, text: string, urgent: boolean) { return `sms ${to}: ${urgent ? "URGENT " : ""}${text}`; }
}
class Push implements Channel {
deliver(to: string, text: string, urgent: boolean) { return `push ${to} [priority ${urgent ? "high" : "normal"}]: ${text}`; }
}
class Email implements Channel {
deliver(to: string, text: string, urgent: boolean) { return `email ${to}: ${urgent ? "[URGENT] " : ""}${text}`; }
}
// ---- The other side: what is being said. Each alert holds a channel instead of being one. ----
abstract class Alert {
constructor(private readonly channel: Channel, private readonly to: string) {}
send(): string { return `${this.title()} -> ` + this.channel.deliver(this.to, this.text(), this.urgent()); }
abstract title(): string;
abstract text(): string;
abstract urgent(): boolean;
}
class LowBattery extends Alert {
constructor(channel: Channel, to: string, private readonly bike: string, private readonly percent: number) { super(channel, to); }
title() { return `low battery ${this.bike}`; }
text() { return `${this.bike} battery at ${this.percent}%`; }
urgent() { return this.percent < 15; }
}
class Theft extends Alert {
constructor(channel: Channel, to: string, private readonly bike: string) { super(channel, to); }
title() { return `theft ${this.bike}`; }
text() { return `${this.bike} moved without a ride`; }
urgent() { return true; }
}
class DockFault extends Alert {
constructor(channel: Channel, to: string, private readonly dock: string, private readonly problem: string) { super(channel, to); }
title() { return `dock fault ${this.dock}`; }
text() { return `${this.dock} ${this.problem}`; }
urgent() { return false; }
}
const phone = "+44 7700 900123", app = "ops-app", inbox = "ops@pedal.example";
const alerts: Alert[] = [
new LowBattery(new Sms(), phone, "E-205", 31),
new Theft(new Push(), app, "B-17"),
new DockFault(new Email(), inbox, "D9", "lock jammed"),
new Theft(new Sms(), phone, "B-17"), // same alert, another channel
new LowBattery(new Push(), app, "E-311", 9), // same channel, urgency decided by the alert
];
for (const a of alerts) console.log(a.send());
console.log("classes: 3 alerts + 3 channels = 6 (one class per pair would need 9)");import java.util.List;
public class Main {
// ---- One side of the bridge: how a message travels. ----
interface Channel { String deliver(String to, String text, boolean urgent); }
static class Sms implements Channel {
public String deliver(String to, String text, boolean urgent) { return "sms " + to + ": " + (urgent ? "URGENT " : "") + text; }
}
static class Push implements Channel {
public String deliver(String to, String text, boolean urgent) { return "push " + to + " [priority " + (urgent ? "high" : "normal") + "]: " + text; }
}
static class Email implements Channel {
public String deliver(String to, String text, boolean urgent) { return "email " + to + ": " + (urgent ? "[URGENT] " : "") + text; }
}
// ---- The other side: what is being said. Each alert holds a channel instead of being one. ----
abstract static class Alert {
private final Channel channel;
private final String to;
Alert(Channel channel, String to) { this.channel = channel; this.to = to; }
String send() { return title() + " -> " + channel.deliver(to, text(), urgent()); }
abstract String title();
abstract String text();
abstract boolean urgent();
}
static class LowBattery extends Alert {
private final String bike; private final int percent;
LowBattery(Channel c, String to, String bike, int percent) { super(c, to); this.bike = bike; this.percent = percent; }
String title() { return "low battery " + bike; }
String text() { return bike + " battery at " + percent + "%"; }
boolean urgent() { return percent < 15; }
}
static class Theft extends Alert {
private final String bike;
Theft(Channel c, String to, String bike) { super(c, to); this.bike = bike; }
String title() { return "theft " + bike; }
String text() { return bike + " moved without a ride"; }
boolean urgent() { return true; }
}
static class DockFault extends Alert {
private final String dock, problem;
DockFault(Channel c, String to, String dock, String problem) { super(c, to); this.dock = dock; this.problem = problem; }
String title() { return "dock fault " + dock; }
String text() { return dock + " " + problem; }
boolean urgent() { return false; }
}
public static void main(String[] args) {
String phone = "+44 7700 900123", app = "ops-app", inbox = "ops@pedal.example";
List<Alert> alerts = List.of(
new LowBattery(new Sms(), phone, "E-205", 31),
new Theft(new Push(), app, "B-17"),
new DockFault(new Email(), inbox, "D9", "lock jammed"),
new Theft(new Sms(), phone, "B-17"), // same alert, another channel
new LowBattery(new Push(), app, "E-311", 9)); // same channel, urgency decided by the alert
for (Alert a : alerts) System.out.println(a.send());
System.out.println("classes: 3 alerts + 3 channels = 6 (one class per pair would need 9)");
}
}Output (the same in all three languages)
low battery E-205 -> sms +44 7700 900123: E-205 battery at 31% theft B-17 -> push ops-app [priority high]: B-17 moved without a ride dock fault D9 -> email ops@pedal.example: D9 lock jammed theft B-17 -> sms +44 7700 900123: URGENT B-17 moved without a ride low battery E-311 -> push ops-app [priority high]: E-311 battery at 9% classes: 3 alerts + 3 channels = 6 (one class per pair would need 9)
6. Try it
Grow both sides
Add alert kinds and channels and compare how many classes each design needs.
Each cell is one combination people can use. Without a bridge each cell is a class; with one, only the row and column headers are.
7. When not to use it
What you get
- Linear growth: alerts + channels classes, not alerts × channels.
- Independent change: edit one channel without touching any alert.
- Runtime choice: the same alert can travel by whichever channel suits the shift.
- Hides the platform: alerts don't know SMS gateways exist.
What you pay
- Up-front design: you must spot the two dimensions early.
- An interface to agree:
deliver(to, text, urgent)must suit every alert and channel. - Indirection: the behaviour of one alert is split across two classes.
- Overkill for one dimension: if channels never vary, it's just extra code.
| If… | Use instead |
|---|---|
| Only one thing varies | 13 Strategy or plain subclasses |
| You're fitting an existing class with the wrong interface | 06 Adapter |
| Several channels must receive the same alert | Send to a list, or use 18 Observer |
| The pairs genuinely differ in logic | Separate classes may be honest |
8. How it goes wrong
deliver() takes a battery_percent, only battery alerts fit. Keep it about delivery.if isinstance(alert, Theft) inside Sms glues the two sides back together.9. Remember it and say it
How this hook works: what “Any remote, any TV” means
What the slogan means, phrase by phrase
- “Any remote”
- Any alert kind (the abstraction).
- “any TV”
- Any channel (the implementation).
- “one agreed interface”
Channel.deliver().
Why this picture fits: Remotes and TVs are made by different people on different schedules and still work together.
The story → the concept
| The living room | The code |
|---|---|
| The remote | Theft, LowBattery |
| The TV | Sms, Push, Email |
| The infra-red standard | deliver(to, text, urgent) |
| A new TV brand | A Pager channel |
Watch it on an example
Theftdecides the text and that it's urgent.- It hands both to whichever channel it holds.
Pushshows urgency as "priority high";Smsas an URGENT prefix.
The trap it saves you from
Building one class per combination, so the hierarchy grows as a product.
It still works when…
Yes: JDBC (application code over any database driver) and graphics toolkits (shapes over any rendering back end).
In an interview or code review
- Name the two dimensions: "Alert kind and delivery channel vary independently, so alerts hold a
Channel." - Do the arithmetic: 3 + 3 classes instead of 3 × 3, and the gap widens with each addition.
- Say who decides what: the alert decides content and urgency; the channel decides how urgency looks.
- Distinguish it: adapter is retrofitted to make things fit; bridge is designed in to keep two families apart.
10. Check yourself
What problem does a bridge solve?
4 alert kinds and 5 channels: classes without and with a bridge?
Where is "how urgency looks on SMS" decided?
What joins the two hierarchies?
Bridge vs adapter?
11. Go deeper
| Resource | Why |
|---|---|
| 06 Adapter | The look-alike pattern, used after the fact. |
| 13 Strategy | One varying algorithm, rather than two families. |
Java DriverManager (JDBC) | Application code on one side, any database driver on the other. |
| Wikipedia: Bridge pattern | Structure and examples. |