The Bridge Pattern is a structural design pattern that helps decouple abstraction from implementation, allowing them to evolve independently.
This is particularly useful when dealing with multiple dimensions of variability in our system. Instead of having a complex hierarchy of subclasses, we use composition to separate concerns, making our code more maintainable and scalable.
In this post, we'll explore the Bridge Pattern using an easy-to-understand example with different devices and remote controls. ๐บ๐ฎ
Why Use the Bridge Pattern?
โ
Reduces Class Explosion โ Avoids deep inheritance trees when dealing with multiple variations.
โ
Promotes Composition Over Inheritance โ Favors flexibility by allowing independent changes to abstraction and implementation.
โ
Improves Maintainability โ Enhances readability and separation of concerns.
Problem Without the Bridge Pattern
Imagine we have different types of devices (like TV and Radio) and different remote controls (like Basic Remote and Advanced Remote).
A bad approach would be using inheritance to create a subclass for each combination, like:
-
BasicTVRemote
-
AdvancedTVRemote
-
BasicRadioRemote
-
AdvancedRadioRemote
As we add more device types or remote types, our class hierarchy grows exponentially! ๐
Example Without the Bridge Pattern (Using Inheritance)
// Parent class for all remotes
public abstract class Remote {
public abstract void turnOn();
public abstract void turnOff();
}
// Concrete class for Basic TV Remote
public class BasicTVRemote extends Remote {
@Override
public void turnOn() {
System.out.println("Turning on the TV.");
}
@Override
public void turnOff() {
System.out.println("Turning off the TV.");
}
}
// Concrete class for Advanced TV Remote
public class AdvancedTVRemote extends BasicTVRemote {
public void mute() {
System.out.println("Muting the TV.");
}
}
// Concrete class for Basic Radio Remote
public class BasicRadioRemote extends Remote {
@Override
public void turnOn() {
System.out.println("Turning on the Radio.");
}
@Override
public void turnOff() {
System.out.println("Turning off the Radio.");
}
}
// Concrete class for Advanced Radio Remote
public class AdvancedRadioRemote extends BasicRadioRemote {
public void mute() {
System.out.println("Muting the Radio.");
}
}
// Main class
public class WithoutBridgeExample {
public static void main(String[] args) {
BasicTVRemote basicTV = new BasicTVRemote();
AdvancedTVRemote advancedTV = new AdvancedTVRemote();
BasicRadioRemote basicRadio = new BasicRadioRemote();
AdvancedRadioRemote advancedRadio = new AdvancedRadioRemote();
basicTV.turnOn();
advancedTV.mute();
basicRadio.turnOff();
advancedRadio.mute();
}
}
Problems with This Approach
๐จ Class Explosion โ Every new remote type requires new subclasses.
๐จ Hard to Maintain โ Changes in the Remote logic require modifications in multiple classes.
๐จ Tight Coupling โ Device behavior is tightly linked to Remote behavior.
Solution: Using the Bridge Pattern
Instead of relying on inheritance, we use composition:
- We separate Devices (TV, Radio, etc.) from Remotes (Basic, Advanced, etc.).
- Remotes hold a reference to a Device, allowing independent variations.
Step 1: Create the Device Interface
Each device must implement basic functionalities like turning on/off and changing volume.
public interface Device {
void turnOn();
void turnOff();
void setVolume(int volume);
boolean isOn();
}
Step 2: Implement Different Devices (TV and Radio)
public class TV implements Device {
private boolean on = false;
private int volume = 50;
@Override
public void turnOn() {
on = true;
System.out.println("TV is now ON");
}
@Override
public void turnOff() {
on = false;
System.out.println("TV is now OFF");
}
@Override
public void setVolume(int volume) {
this.volume = volume;
System.out.println("TV volume set to " + volume);
}
@Override
public boolean isOn() {
return on;
}
}
public class Radio implements Device {
private boolean on = false;
private int volume = 30;
@Override
public void turnOn() {
on = true;
System.out.println("Radio is now ON");
}
@Override
public void turnOff() {
on = false;
System.out.println("Radio is now OFF");
}
@Override
public void setVolume(int volume) {
this.volume = volume;
System.out.println("Radio volume set to " + volume);
}
@Override
public boolean isOn() {
return on;
}
}
Step 3: Create the Remote Control Abstraction
Now, we define an abstract class RemoteControl
, which uses composition to interact with a Device
.
public class RemoteControl {
protected Device device;
public RemoteControl(Device device) {
this.device = device;
}
public void togglePower() {
if (device.isOn()) {
device.turnOff();
} else {
device.turnOn();
}
}
public void volumeUp() {
device.setVolume(50);
}
public void volumeDown() {
device.setVolume(10);
}
}
Step 4: Extend the Remote Control with More Functionality
Let's create an Advanced Remote that can also mute the device.
public class AdvancedRemoteControl extends RemoteControl {
public AdvancedRemoteControl(Device device) {
super(device);
}
public void mute() {
System.out.println("Muting the device");
device.setVolume(0);
}
}
Step 5: Using the Bridge Pattern in the Main Program
public class BridgePatternExample {
public static void main(String[] args) {
Device tv = new TV();
Device radio = new Radio();
RemoteControl basicRemote = new RemoteControl(tv);
AdvancedRemoteControl advancedRemote = new AdvancedRemoteControl(radio);
System.out.println("Using Basic Remote with TV:");
basicRemote.togglePower();
basicRemote.volumeUp();
System.out.println("\nUsing Advanced Remote with Radio:");
advancedRemote.togglePower();
advancedRemote.mute();
}
}
Pros of the Bridge Pattern
โ
Decouples abstraction from implementation โ Devices and Remotes can evolve separately.
โ
Improves Maintainability โ Changes in one part of the code donโt affect the other.
โ
Promotes Code Reusability โ You can mix and match different implementations.
Conclusion
The Bridge Pattern is a great solution when you have multiple variations of both abstractions and implementations. By using composition over inheritance, we gain flexibility, scalability, and better code organization.
Top comments (3)
this a great article to address design pattern, i have this case and confuse how to use which pattern to solve it
case:
"Imagine there is a corridor where this corridor processes requests, and these requests have different characteristics. However, the corridor is singular, so all requests must pass through it to be processed into the desired outcomes. Some requests use the same processes within the corridor, but the order of those processes varies."
In your opinion, what design pattern should I implement for the corridor and the requests so that my application adheres to the SOLID principles?
Hi @try_code, glad that you enjoy the article.
Ok, I think that a good approach for this scenario would be to combine the Chain of Responsibility and Strategy design patterns.
The Chain of Responsibility allows you to model the "corridor" as a chain of processing steps, where each step can handle (or not) the request and then pass it along to the next.
The Strategy pattern can be used to define different strategies for ordering and combining these steps, since each request may require a different sequence of processes.
This combination keeps the system flexible and modular, making it easier to maintain and extend, while also adhering well to the SOLID principles.
I have an article that explains the Strategy pattern here.
Unfortunately, I havenโt written any examples on the blog using the Chain of Responsibility yet, but I highly recommend checking out Refactoring Guru, it's a great resource for learning design patterns.
Hope this helps you solve your problem.
Thanks again for your comment!
Thank you for your response; I really appreciate it. I will review the pattern you suggested.