This week we’ll continue our exploration of design patterns and look at a structural pattern, the Bridge pattern. This pattern is fairly simple, but is not used very often.
The Gang of Four book defines the Bridge pattern as follows:
“Decouple an abstraction from its implementation so that the two can vary independently.”
The Bridge Pattern
The Bridge pattern separates the abstraction from the implementation into two independent but connected hierarchies so that each can vary independently of the other. This allows us to change the classes in each hierarchy independently of the other classes. This minimizes the risk of breaking any existing code, and makes code maintenance easier.
The GoF terminology is a little confusing, but they aren’t talking about interfaces or abstract classes from a programming language perspective here. Their Abstraction is a high-level control layer that isn’t supposed to do any real work on its own. The Abstraction should delegate the work to the Implementation layer . The Abstraction is also called an interface, while the Implementation layer is also called the platform.
A simple example of the Bridge pattern is a household switch which controls lights, air-conditioners, ceiling fans, etc. The purpose of the switch is to turn a device on or off.
The switch is wired to a specific device, but switches don’t know anything about the devices themselves. We don’t buy a specific LightSwitch
, FanSwitch
, AirConSwitch
. We buy the same type of switch no matter what device we’re controlling.
The actual switch can be implemented as a simple two-position lever switch, a wall-mounted switch, a dimmer switch, a pull-chain switch, etc. The orthogonal dimensions are the Device
and the Switch
. The Device
is the Abstraction while the Switch
class is the Implementation. The DimmerSwitch
, LeverSwitch
and PullSwitch
are the Concrete Implementations.
The driving force behind the Bridge pattern is the often-repeated design principle of *Prefer containment over inheritance*.
Bridge Pattern: Design Example
Back in our first person shooter game, we’ve already created mechanised fighting units, like self-driving armoured vehicles and autonomous fighting robots. We modelled these robots and vehicles as MechanisedFightingVehicle
s and MechanisedFightingRobot
s. We can command them to fight and direct them towards specific targets.
We had previously created a FightingUnit
interface with a fight()
method. Let’s add a few more methods for movement:
public interface FightingUnit { public void fight(); public void turn(int degrees); public void advance(); public void retreat(); }
Each FightingUnit
would implement these methods differently. For example, a MechanisedFightingVehicle
would turn by turning its front wheels, or if it was running on tank tracks, by braking one track. A MechanisedFightingRobot
would turn by taking longer strides with one leg, or if it was running on tracks, by braking one track.
How will we actually control our FightingUnit
s? We’d probably like to use a remote control in the same way we control drones and model cars in real life.
public interface RemoteControl { public void engage(); public void turn(int degrees); public void forward(); public void back(); }
Initially we’d be happy with a simple RemoteControl
with buttons and levers to control movement, speed and direction. But in future we could want more advanced remote controls that can sense our movements, or even be operated by voice commands.
We could model the various remote controls as a single class hierarchy implementing the RemoteControl
interface. We will also have a number of different FightingUnit
subclasses: MechanisedFightingRobot
, MechanisedFightingVehicle
, MechanisedFightingShip
, etc.
If we use inheritance to extend the RemoteControl
class, we would have to think of all the possible combinations of the various RemoteControl
types and the FightingUnit
types, and code them as separate classes.
This will lead to an ever-increasing set of different combinations, e.g: BasicRobotRemoteControl
, AdvancedRobotRemoteControl
, VoiceActivatedRobotRemoteControl
, BasicVehicleRemoteControl
, AdvancedVehicleRemoteControl
, VoiceActivatedVehicleRemoteControl
, etc. If we added other RemoteControl
and FightingUnit
types later, our class hierarchy would expand accordingly. This is a clumsy design and a nightmare for later modification and maintenance.
Bridge Pattern: Solution Code
Let’s rather use composition instead of inheritance to configure a specific FightingUnit
within a RemoteControl
.
The Bridge pattern separates the abstraction from the implementation by creating two separate class hierarchies. Each hierarchy can change independently of the other. In our example, the abstraction is the RemoteControl
and the implementation is the FightingUnit
.
When we use a RemoteControl
, it delegates the actual instructions to the contained FightingUnit
object. This adds flexibility to our design. Both the RemoteControl
and FightingUnit
can change independently of the other. It means that:
- New remote controls can be designed and used in the game without worrying about what they control.
- New fighting units (robots, vehicles, ships, planes, etc.) can be designed and used in a remote control without changing the remote control code.
- It simplifies the design, as well as makes later code maintenance easier.
We will create an AbstractRemoteControl
class that contains an FightingUnit
object which will be supplied at object creation time.
public abstract class AbstractRemoteControl implements RemoteControl { // composition/containment private FightingUnit unit; // can be used for dependency injection public AbstractRemoteControl(FightingUnit unit) { this.unit = unit; } // common implementations for all remote controls @Override public void engage() { unit.fight(); } @Override public void turn(int degrees) { unit.turn(degrees); } @Override public void forward() { unit.advance(); } @Override public void back() { unit.retreat(); } @Override public String toString() { return getClass().getName() + " is controlling a " + unit; } }
It is obvious from the preceding code that the RemoteControl
and the FightingUnit
interfaces have different methods. This is more common than both interfaces having the same methods. The RemoteControl
(the Abstraction) declares more complex operations that rely on the simpler operations defined by the FightingUnit
(the Implementation).
We can create concrete implementations of the AbstractRemoteControl
as follows:
public class BasicRemoteControl extends AbstractRemoteControl { public BasicRemoteControl(FightingUnit unit) { super(unit); } // override the common implementations as necessary } public class VoiceActivatedRemoteControl extends AbstractRemoteControl { public VoiceActivatedRemoteControl(FightingUnit unit) { super(unit); } // override the common implementations as necessary }
The client code creates the various RemoteControl
s and passes the desired FightUnit
s to them. A code snippet follows:
// in client code FightingUnit robot = new MechanisedFightingRobot(); FightingUnit vehicle = new MechanisedFightingVehicle(); AbstractRemoteControl remote1 = new BasicRemoteControl(robot); AbstractRemoteControl remote2 = new VoiceActivatedRemoteControl(vehicle); System.out.println(remote1); System.out.println(remote2); remote1.forward(); remote1.turn(90); remote1.engage(); remote1.back(); remote2.forward(); remote2.turn(90); remote2.engage(); remote2.back(); // more code...
Comparison to Other Patterns
The Bridge pattern has a similar class structure to the Adapter, State and Strategy patterns. All of these patterns are based on composition, which delegates the actual work to other objects. Each pattern has a different intent, however:
- The Bridge pattern decouples the abstraction and implementation by creating two separate class hierarchies, so they can be changed independently.
- The Adapter pattern changes the interface of an existing object, and lets two incompatible interfaces work together.
- As stated by GOF, “Adapter makes things work after they’re designed; Bridge makes them work before”.
- The State pattern enables an object to change its behaviour when its internal state changes. The state-related work is delegated to a state object.
- The Strategy pattern allows us to define a family of algorithms and make them interchangeable.
What’s Next?
In the weeks ahead, we’ll continue examining some of the more useful design patterns. Stay tuned!
I’m always interested in your opinion, so please leave a comment. Your feedback helps me write tips that help you.