One-line Definition
Separates an object's abstraction from its implementation, allowing both to evolve independently without exponential subclassing.
Problem
Suppose you have a Shape class with two subclasses: Circle and Square. Now you want to add colors, so you create RedCircle, BlueCircle, RedSquare, and BlueSquare. When you add a new shape (Triangle) and a new color (Green), the number of classes explodes to 3 shapes × 3 colors = 9 classes. The Bridge Pattern solves this by moving from inheritance to composition, placing the color into a separate hierarchy.
Real-world Analogy
Think of a TV remote and a TV. Samsung and Sony make different TVs. Basic and Advanced remotes exist. Without a bridge you'd need BasicSamsungRemote, AdvancedSamsungRemote, BasicSonyRemote, AdvancedSonyRemote. The bridge lets you combine any remote with any TV at runtime by composing them, so you just need 2 remote classes and 2 TV classes.
Structure
- Client
- Works with the Abstraction and doesn't care about the underlying Implementation.
- Abstraction
- High-level control layer that delegates work to the Implementation.
- RefinedAbstraction
- Extends the Abstraction with additional features (e.g., an Advanced Remote).
- Implementation
- Interface defining low-level operations.
- ConcreteImplementation
- Classes that implement the low-level interface (e.g., specific TVs).
Diagram
classDiagram
class Client {
}
class Abstraction {
-Implementation impl
+operation()
}
class RefinedAbstraction {
+extraOperation()
}
class Implementation {
<<interface>>
+operationImpl()
}
class ConcreteImplA {
+operationImpl()
}
class ConcreteImplB {
+operationImpl()
}
Client --> Abstraction
Abstraction o-- Implementation : bridge
RefinedAbstraction --|> Abstraction
ConcreteImplA ..|> Implementation
ConcreteImplB ..|> Implementation
Code Walkthrough
Notice how the Remote (Abstraction) contains a reference to a Device (Implementation). You can add new Remotes or new Devices entirely independently.
interface Device {
void turnOn();
void turnOff();
void setVolume(int volume);
int getVolume();
void setChannel(int channel);
}
class TV implements Device {
private int volume = 30;
private int channel = 1;
@Override
public void turnOn() { System.out.println("TV is ON"); }
@Override
public void turnOff() { System.out.println("TV is OFF"); }
@Override
public void setVolume(int volume) { this.volume = volume; }
@Override
public int getVolume() { return volume; }
@Override
public void setChannel(int channel) {
this.channel = channel;
System.out.println("TV channel set to " + channel);
}
}
class Remote {
protected Device device;
public Remote(Device device) {
this.device = device;
}
public void togglePower() {
device.turnOn();
}
public void volumeUp() {
device.setVolume(device.getVolume() + 10);
System.out.println("Volume: " + device.getVolume());
}
}
class AdvancedRemote extends Remote {
public AdvancedRemote(Device device) {
super(device);
}
public void mute() {
device.setVolume(0);
System.out.println("Device muted");
}
}
class Main {
public static void main(String[] args) {
Device tv = new TV();
// Basic remote bridges to TV
Remote basicRemote = new Remote(tv);
basicRemote.togglePower();
basicRemote.volumeUp();
// Advanced remote bridges to TV
AdvancedRemote advancedRemote = new AdvancedRemote(tv);
advancedRemote.mute();
}
}
Bad vs Good
Bad Approach
Problems
- Class explosion grows multiplicatively (N shapes × M colors = N*M classes).
- Hard to maintain and update.
class BasicTVRemote {
public void turnOn() { System.out.println("TV ON"); }
}
class AdvancedTVRemote extends BasicTVRemote {
public void mute() { System.out.println("TV muted"); }
}
class BasicRadioRemote {
public void turnOn() { System.out.println("Radio ON"); }
}
class AdvancedRadioRemote extends BasicRadioRemote {
public void mute() { System.out.println("Radio muted"); }
}
Better Approach
Improvements
- Remote and Device evolve independently. Total classes grow additively (N + M).
- New combinations are composed dynamically at runtime instead of hard-coded as subclasses.
interface Device {
void turnOn();
void setVolume(int volume);
int getVolume();
}
class Remote {
protected Device device;
public Remote(Device device) { this.device = device; }
public void togglePower() { device.turnOn(); }
}
class AdvancedRemote extends Remote {
public AdvancedRemote(Device device) { super(device); }
public void mute() { device.setVolume(0); }
}
// Any remote + any device
Remote r = new AdvancedRemote(new TV());
Pros vs Cons
| Pros | Cons |
|---|---|
| Prevents exponential class explosion | Increases initial code complexity |
| Enforces the Open/Closed Principle | Requires careful upfront architecture design |
| Abstraction and implementation evolve independently | Can be overkill for a single dimension of variation |
| Runtime flexibility to swap implementations | Indirection may confuse readers used to inheritance |
| Hides implementation details from the client |
When to Use
- You want to divide and organize a monolithic class that has multiple variants of some functionality.
- You need to extend a class in several orthogonal (independent) dimensions.
- You want to be able to switch implementations at runtime.
When Not to Use
- The class only varies in one dimension (e.g., just Shape, no Color).
- The hierarchy is simple and unlikely to grow.
- The added abstraction layer makes the code harder to read without tangible benefits.
Real-world Examples
java.sql.DriverManager(Abstraction) andjava.sql.Driver(Implementation)- SLF4J (Abstraction) bridging to Logback/Log4j (Implementation)
- GUI frameworks separating window management (Abstraction) from OS-specific rendering (Implementation)
Key Takeaway
The Bridge Pattern replaces inheritance with composition to prevent an exponential explosion of subclasses. Use it when an entity has two or more independent axes of variation (like Shape and Color, or Remote and Device). Avoid it if you're only dealing with a single dimension of variation, where simple inheritance suffices.