Bridge is a structural design pattern that let you split a giant class or a set of closely related classes into two separate hierarchies, abstraction and implementation, which can be developed independently of each other.
Abstraction? Implementation? Sound scary? Let's consider the simple example for starters.
Say, you have a geometric
Shape class with a pair of subclasses:
Triangle. You'd also want to extend this hierarchy to have colors. In other words, you'd like to have
But since you already have subclasses, you need to create 4 class combinations like
Adding more shape types and colors will make this hierarchy even bigger. For example, to add
Circles you have to create two subclasses, one for each color. And after that, adding new color will require creating three subclasses, one for each shape type. Further we go, worse it becomes.
This problem happens each time we extend the class hierarchy into several independent dimensions.
The Bridge has the solution for this problem which based on the old principle of replacing inheritance with delegations. You have to extract one of these "dimensions" into its own class hierarchy. Original classes will contain a reference to an object of the new hierarchy, instead of storing all of its state and behaviors inside of one class.
This way we extract
Color into its own class with two child classes,
Shape class will get a reference field to one of the color classes. Using that reference, it will be able to delegate work to color objects when needed. This reference will serve as a bridge between
Color. From now on, adding new colors won't require changing shape classes and vice versa.
Abstraction and Implementation
The GoF book introduces these terms in a Bridge pattern definition. In my opinion, they sound too academic and make the pattern look harder than it is. But having read the simple example from above, let's see what the GoF really meant.
The Abstraction, also called the Interface, is a control layer of some entity. It does not do any real job on its own, but rather delegates most of the work to the Implementation layer, sometimes called the Platform. Just don't mix it up with interfaces and abstract classes from your programming language, they are not the same things.
When talking about real applications, the Abstraction could be a graphical user interface (GUI), and Implementation could be underlying operating system API, that GUI calls in response to user interactions.
There are two directions you can extend such app to:
- Having several different GUIs (for customer and admins).
- Supporting several different APIs (work under Windows, Linux, and MacOS).
The code of this program may look like a giant spaghetti bowl, where tons of conditional operators connect different GUI and API behaviors.
The code can be improved by subclassing all interface-platform variants. But eventually, this will lead to the problem we've already seen in the shapes example. Class hierarchy will grow exponentially, each new GUI or API type will require adding several class combinations.
The Bridge pattern suggests to divide these classes into two hierarchies:
- Abstraction: the GUI layer of the app.
- Implementation: the operational system APIs.
Abstraction object will have a reference to one of the Implementation objects. Different implementations will be interchangeable as long as they follow a common interface, enabling the same GUI to work under Windows and Linux.
But most importantly, you could start working on the GUI classes without touching the operational system code and vice versa. For example, adding support for a new operational system would only require creating a subclass in implementation hierarchy.
Abstraction mainly contains a control logic like a user interface. Abstraction's code relies on the Implementation object to do the real job.
Implementation defines a common interface for the different implementations. Abstraction could work with any Concrete implementation that follows this interface.
Abstraction's and Implementation's interfaces sometimes may be equal in some programs. But in most cases, Implementation contains basic primitive operations used by Abstraction to perform some complex behavior.
Concrete Implementations contain platform-specific code.
Refined Abstractions may be created to provide several variants of the control logic. These classes, just as their parent, should use the Implementation interface to work with different implementations.
Client works only with the Abstraction classes with the one exception. It's Client's job to pass a Concrete Implementation object to the Abstraction during its construction. However, implementation can also be changed dynamically if needed.
In this example Bridge breaks the monolithic code of the devices and remote controls into several parts:
- Devices (act as implementation)
- Remotes (act as abstraction)
The remote's base class has a field that contains a reference to a device object that it controls. Remotes work with devices through a common interface. It allows one remote to work with several device types.
You can change remote control classes independently from the device classes. For instance, you could create a simple remote with only two buttons or sophisticated remote with a touch screen.
Thus, the Bridge pattern allows you to break an entity into several different ones and then evolve them independently from each other. Client code also remains simple. It only needs to pick an abstraction and configure it with one of the implementations.
// All remote classes contain reference to the device they controls. Remote's // methods delegate most of the work to the device methods. class Remote is field device: Device constructor BasicRemote(device: Device) is this.device = device method togglePower() is if device.isEnabled() then device.disable() else device.enable() method volumeDown() is device.setVolume(device.getVolume() - 10) method volumeUp() is device.setVolume(device.getVolume() + 10) method channelDown() is device.setChannel(device.getChannel() - 1) method channelUp() is device.setChannel(device.getChannel() + 1) // You can extend remote hierarchy independently from device classes. class AdvancedRemote extends BasicRemote is method mute() is device.setVolume(0) // All devices have the common interface. This makes them compatible with // all remotes. interface Device is method isEnabled() method enable() method disable() method getVolume() method setVolume(percent) method getChannel() method setChannel(channel) // But each concrete device may have its own implementation. class Tv implements Device is // ... class Radio implements Device is // ... // Somewhere in client code. tv = new Tv(); remote = new Remote(tv) remote.pover() radio = new Radio(); remote = new AdvancedRemote(radio)
When you have a huge class that has several variants of some functionality (for example, works with different database servers).
The class becomes hard to support because anyone who touches it has to spend a lot of time trying to comprehend it fully. Changes to one of the functionality variant result in editing the whole class, which may introduce nasty overlooked bugs.
The Bridge pattern splits the monolithic class into several class hierarchies, one of which references the other ones. Classes in these hierarchies can be edited independently from the other ones. It simplifies the support and minimizes a risk of breaking existing code.
When you need to extend a class in several orthogonal (independent) dimensions.
Instead of growing a single hierarchy, Bridge pattern suggests to create a separate class hierarchy for each dimension and link these hierarchies with a reference field.
When you need to be able to change implementation in runtime.
Although it's optional, Bridge pattern allows changing the implementation object inside the abstraction. That's as easy as assigning a new value to a field.
By the way, this item is a reason why so many people confuse Bridge with the Strategy pattern. Remember that pattern is not just a class structure, but also the intent. And he purpose of the Bridge pattern is to structure the code.
How to Implement
Identify the orthogonal dimensions in your classes. These independent concepts could be: abstraction/platform, or domain/infrastructure, or front-end/back-end, or interface/implementation.
Think what operations does the Client want and describe them inside the base Abstraction class.
Determine what all platform are capable of and what does the Abstraction need. Then describe it inside the Implementation interface.
Create Concrete implementation classes for all platforms in your domain, but make sure they all follow the Implementation interface.
Inside the Abstraction class, add a field of the Implementation type. Then implement all abstraction method, while delegating most of the work to the implementation object referenced in that field.
The Client code should pass the implementation object to the abstraction's constructor. Then it may use the abstraction object as it needs to.
Pros and Cons
- Allows building platform independent code.
- Follows the Open/Closed Principle.
- Hides implementation details from client.
- Increases overall code complexity by creating multiple additional classes.
Relations with Other Patterns
Bridge is designed up-front to let the abstraction and the implementation vary independently. Adapter is retrofitted to make unrelated classes work together. Adapter makes things work after they're designed; Bridge makes them work before they are.
State, Strategy, Bridge (and to some degree Adapter) have similar solution structures. They all share elements of the "handle/body" idiom. They differ in intent - that is, they solve different problems.
Abstract Factory can be used along with a Bridge pattern. It's useful when the "interface" part of the Bridge can work only with a particular "implementation". In this case, factory can encapsulate these relations and hide the complexity from a client.