Hi everyone, in the last article we learned about the Adapter Design pattern, in this one we will go through another Structural Design Pattern which is the Bridge Pattern.
What is a Bridge Pattern?
A bridge Pattern is a structural design pattern that lets you split a large class or a set of closely related classes into two separate hierarchies—abstraction and implementation—which can be developed independently.
Does the definition Sounds Scary? Let us understand more with an example
Suppose we have a Device Class with a couple of subclasses: TV and DVDPlayer.
Now we want to add a hierarchy to it by introducing remotes: Basic and Advanced remotes. This will lead to the creation of 4 different Classes: BasicRemoteTv
, AdvancedRemoteTv
, BasicRemoteDvd
, and AdvancedRemoteDvd
.
Adding a new device or remote type, the number of classes will grow exponentially. For example, adding a new device will need to create 2 new subclasses for each remote type and then adding a remote will need 3 more subclasses
This is a classic inheritance issue because we are trying to extend the Device class into 2 independent directions: type of device and remote
Why should we use Bridge Pattern?
Bridge Pattern solves the problem by switching from inheritance to composition.
With the Bridge Pattern, we create separate hierarchies for devices and remotes. The speciality lies in the Bridge
—a connector that brings them together. The Device
class and the Remote
class become friends, working together through composition rather than a rigid parent-child relationship.
How it Works:
We start by defining the
Device
interface, which represents the common operations for turning on and off.public interface Device { void turnOn(); void turnOff(); void setChannel(int channel); }
We implement device classes like
TV
andDVDPlayer
that implements this interface, each with its unique behaviour.public class TV implements Device { @Override public void turnOn() { System.out.println("TV is ON"); } @Override public void turnOff() { System.out.println("TV is OFF"); } @Override public void setChannel(int channel) { System.out.println("Setting TV channel to " + channel); } }
public class DVDPlayer implements Device { @Override public void turnOn() { System.out.println("DVD Player is ON"); } @Override public void turnOff() { System.out.println("DVD Player is OFF"); } @Override public void setChannel(int channel) { // DVD player does not have channels System.out.println("DVD Player does not have channels"); } public void play() { System.out.println("DVD Player is playing"); } public void pause() { System.out.println("DVDPlayer Player is Paused"); } }
Next, we create the
Remote
class, our abstraction, which contains a reference to aDevice
(the Bridge).public abstract class RemoteControl { protected Device device; protected RemoteControl(Device device) { this.device = device; } public abstract void powerOn(); public abstract void powerOff(); public abstract void setChannel(int channel); }
We then have concrete implementations of the
Remote
class –BasicRemote
andAdvancedRemote
public class BasicRemoteControl extends RemoteControl{ public BasicRemoteControl(Device device) { super(device); } @Override public void powerOn() { device.turnOn(); } @Override public void powerOff() { device.turnOff(); } @Override public void setChannel(int channel) { device.setChannel(channel); } }
public class AdvancedRemoteControl extends RemoteControl { public AdvancedRemoteControl(Device device) { super(device); } @Override public void powerOn() { device.turnOn(); } @Override public void powerOff() { device.turnOff(); } @Override public void setChannel(int channel) { device.setChannel(channel); } public void play() { if (device instanceof DVDPlayer) { ((DVDPlayer) device).play(); } else { System.out.println("This functionality is only available for DVDPlayer"); } } public void pause() { if (device instanceof DVDPlayer) { ((DVDPlayer) device).pause(); } else { System.out.println("This functionality is only available for DVDPlayer"); } } }
Now let us understand
What is Abstraction and Implementor in the context of Bridge Design Pattern?
Abstraction (
Remote
class):- The abstraction is the high-level part of the system. It defines the interface that clients interact with. Here, the
Remote
class provides methods likepowerOn()
andpowerOff()
. This class is abstract because it doesn't provide the concrete implementation of these methods. Instead, it delegates the actual work to its bridge, which is the implementor.
- The abstraction is the high-level part of the system. It defines the interface that clients interact with. Here, the
Implementor (
Device
):- The implementor is the low-level part of the system. It provides the concrete implementation of the methods declared in the abstraction. In this example, the
Device
interface (or its implementations likeTV
andDVDPlayer
) defines the actual behavior for turning on and off.
- The implementor is the low-level part of the system. It provides the concrete implementation of the methods declared in the abstraction. In this example, the
Bridge:
The
Device
instance held by theRemote
class acts as the bridge, connecting the high-level abstraction (remote) with its low-level implementation (device).This indicates that the
Device
interface or class is responsible for carrying out the actual work defined in theRemote
class. When a method likepowerOn()
is called on aRemote
object, it delegates the task to the connectedDevice
object, allowing the concrete implementation inTV
orDVDPlayer
to execute.Device
is the one doing the real work, and theRemote
is orchestrating and providing a simplified interface for clients to interact with. The bridge (Device) connects these two parts, allowing them to work together while remaining independent and flexible for future changes.
public class BridgePatternDemo { public static void main(String[] args) { DVDPlayer dvdPlayer = new DVDPlayer(); AdvancedRemoteControl advancedRemoteControl = new AdvancedRemoteControl(dvdPlayer); advancedRemoteControl.powerOn(); advancedRemoteControl.play(); advancedRemoteControl.powerOff(); TV tv = new TV(); advancedRemoteControl = new AdvancedRemoteControl(tv); advancedRemoteControl.powerOn(); advancedRemoteControl.play(); } }
An instance of
DVDPlayer
is created, and anAdvancedRemoteControl
is instantiated with the DVDPlayer.The
powerOn()
method of theAdvancedRemoteControl
is called, which, in turn, calls theturnOn()
method of the associatedDVDPlayer
.The
play()
method of theAdvancedRemoteControl
is called, which, in turn, calls theplay()
method of the associatedDVDPlayer
. Note thatplay()
is an additional method specific to theDVDPlayer
class.The
powerOff()
method of theAdvancedRemoteControl
is called, which calls theturnOff()
method of the associatedDVDPlayer
.An instance of
TV
is created, and theAdvancedRemoteControl
is reused with the TV.The
powerOn()
method of theAdvancedRemoteControl
is called, which calls theturnOn()
method of the associatedTV
.The
play()
method of theAdvancedRemoteControl
is called, which, in this case, does nothing since theTV
class doesn't have aplay()
method. The method is called for illustration purposes.OUTPUT DVD Player is ON DVD Player is playing DVD Player is OFF TV is ON This functionality is only available for DVDPlayer
This code demonstrates the flexibility of the Bridge Pattern. The
AdvancedRemoteControl
class can be associated with different devices (DVDPlayer, TV) without modifying its code, providing a way to control devices independently of their specific implementations.
If you find this helpful, do give it a like and share it with your peers.
Till then, happy coding!
Complete implementations can be found over Github