Design Patterns · Structural Patterns
Bridge
Separate two dimensions of variation. Connect high-level behavior to a replaceable implementation through composition.
This lesson builds on Adapter and composition over inheritance.
Bridge splits a class family into two independently extensible parts. One part owns high-level behavior. The other supplies implementation operations. A reference connects them. The cost is designing a boundary that both sides can honor.
Count the combinations
Notifications vary by urgency and delivery channel. Standard and urgent messages both support email, SMS, and push. Inheritance for every pair creates six combination classes. A fourth channel adds two more. The count is urgency variants multiplied by channels.
Try it
Add a channel to the class family
These counts compare variant classes, excluding shared bases and interfaces. Bridge saves duplicated combinations when both dimensions really vary.
These counts compare variant classes, excluding shared bases and interfaces. Bridge saves duplicated combinations when both dimensions really vary.
Connect behavior to implementation
interface Channel { deliver(text: string): void }
class EmailChannel implements Channel {
deliver(text: string) { console.log("email:", text); }
}
class SmsChannel implements Channel {
deliver(text: string) { console.log("sms:", text); }
}
class Notification {
constructor(protected channel: Channel) {}
send(message: string) { this.channel.deliver(message); }
}
class UrgentNotification extends Notification {
send(message: string) {
this.channel.deliver("URGENT: " + message);
}
}
const alert = new UrgentNotification(new SmsChannel());
alert.send("Deployment failed");
The pattern calls Notification the abstraction and Channel the implementation. These are roles. Notification does not need to be a language-level abstract class. Its public behavior can combine several lower-level operations from Channel.
Try it
Choose the two dimensions independently
behavior class: Notification
implementation object: email
no class named UrgentSmsNotification
behavior class: Notification
implementation object: sms
no class named UrgentSmsNotification
behavior class: UrgentNotification
implementation object: email
no class named UrgentSmsNotification
behavior class: UrgentNotification
implementation object: sms
no class named UrgentSmsNotification
Choose a stable boundary
The channel contract must support the operations all notification variants need. If email supports rich HTML and SMS supports only text, do not hide that difference behind a promise to deliver arbitrary HTML. Use a common text capability or make unsupported features explicit.
Bridge communicates two independent dimensions
Strategy swaps an algorithm inside a context. Bridge organizes two families that grow independently. Their diagrams can look alike. Name the intended variation before naming the pattern.
Check yourself
One channel supports only plain text. Should Channel promise arbitrary HTML delivery for every implementation?
Correct. The bridge contract must be honest across supported implementations.
Not quite. Silent loss breaks the abstraction's promise.
Map the roles
The GoF names, abstraction and implementation, sound academic. In Bridge they name layers, not language keywords: the abstraction is the high-level control layer, and the implementation is the platform layer it delegates to. A GUI that calls an operating system API is the textbook case.
| Role | In this example | Job |
|---|---|---|
| Abstraction | Notification | High-level control logic. Delegates the low-level work to an implementation object. |
| Implementation | Channel | Declares the operations common to all concrete implementations. The abstraction talks to the implementation only through these. |
| Concrete implementations | EmailChannel, SmsChannel | Contain platform-specific code |
| Refined abstractions | UrgentNotification | Variants of the control logic. They also work through the implementation interface. |
| Client | the code that writes new UrgentNotification(new SmsChannel()) | Links an abstraction to an implementation, then works only with the abstraction |
Check yourself
In a cross-platform app, which part is the abstraction and which is the implementation?
Not quite. In Bridge the word names a layer. It has nothing to do with the language keyword.
Correct. The GUI holds high-level control and delegates real work to the platform API.
Reach for it when a class grows along two axes
- you want to split a monolithic class that has several variants of one capability, such as a class that works with several database servers. Each change in a large class risks side effects. Separate hierarchies can change on their own.
- you need to extend a class in independent dimensions. Extract each dimension into its own hierarchy, and let the original class delegate to it instead of doing everything itself.
- you need to switch implementations at runtime. The abstraction holds a field, so replacing the implementation is one assignment. This is also why people confuse Bridge with Strategy. Patterns communicate intent, not only class layout.
Check yourself
A ReportStore class has grown branches for Postgres, MySQL, and SQLite, mixed with its business logic. Which Bridge use case is this?
Correct. Moving the database variants into their own hierarchy lets each side change without the other.
Not quite. Nothing here needs a runtime switch. The pain is one class doing everything.
Implement it in seven steps
Refactor existing code toward the pattern in this order:
- Identify the independent dimensions in your classes: abstraction and platform, domain and infrastructure, front end and back end, or interface and implementation.
- List the operations the client needs and define them in the base abstraction class.
- Find the operations available on all platforms. Declare the ones the abstraction needs in the general implementation interface.
- Create a concrete implementation class for every platform in your domain, each following that interface.
- Add a field of the implementation type to the abstraction. The abstraction delegates most of its work to that object.
- If the high-level logic has several variants, create a refined abstraction for each by extending the base abstraction.
- Have client code pass an implementation object to the abstraction's constructor. After that, the client works only with the abstraction.
Check yourself
Step 3 says to put only operations every platform supports into the implementation interface. Why?
Not quite. Compile speed is not the concern. Honest contracts across platforms are.
Correct. An operation one platform cannot perform turns into a silent failure or an exception there.
Name what it costs
| You gain | You pay |
|---|---|
| Platform-independent classes and apps | An extra interface and delegation layer |
| Clients work with high-level abstractions and never see platform details | The implementation contract must stay meaningful across every platform |
| New abstractions and implementations independently of each other (open/closed) | Applying it to a highly cohesive class makes the code more complicated |
| High-level logic and platform details live in separate classes (single responsibility) | Invalid pairings still need validation or a constrained factory |
Check yourself
A class has one backend and one behavior, and neither is expected to change. Should you split it into a Bridge?
Correct. Bridge pays off when both dimensions really vary.
Not quite. Splitting a cohesive class only adds indirection.
Do not confuse it with its neighbors
| Pattern | How it relates |
|---|---|
| Adapter | Bridge is designed up front so parts can be developed independently. Adapter retrofits existing classes so incompatible ones work together. |
| State, Strategy | Bridge, State, Strategy, and to some degree Adapter share a delegation structure based on composition. Each solves a different problem. |
| Abstract Factory | When some abstractions work only with specific implementations, an abstract factory can encapsulate the valid pairings and hide them from the client. |
| Builder | They combine well: the director plays the abstraction, and the builders play the implementations. |
Check yourself
Urgent notifications may use only SMS and push, never email. How do you stop clients from pairing them wrongly?
Not quite. That would undo the bridge and bring the combination classes back.
Correct. The factory encapsulates the allowed combinations, so clients cannot assemble invalid ones.
Retrieve and apply
Check yourself
An existing library already has the wrong method names and data format. Only that mismatch needs fixing. Which pattern is the direct fit?
Correct. The immediate problem is connecting an existing incompatible interface. Bridge is useful when two designed dimensions should evolve independently.
Not quite. A wrapper field alone does not prove there are two independent families to separate.
Draw two axes for a feature with many combination classes. State the operations crossing between them. Continue to Composite for a different shape: a tree of interchangeable leaves and containers.
Source: Alexander Shvets, Dive Into Design Patterns (深入设计模式), Chinese edition v2021-1.25. Bridge, printed pages 155–168. Explanations, examples, and exercises are adapted for this course.