We're working on a substantial update for the whole design patterns section that should be ready by the end of September. Until then, please sorry for all embarrassing typos and errors you might encounter here and there.
Bridge is a structural design pattern that lets 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 a simple example for starters.
Say, you have a geometric
Shape class with a pair of subclasses:
Square. You want to extend this hierarchy to incorporate colors by creating
Blue shapes subclasses. But since you already have subclasses, you need to create 4 class combinations such as
Adding more shape types and colors will make this hierarchy even bigger. For example, to add
Triangles 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 pattern attempts to solve it by replacing inheritance with delegations. You have to extract one of these "dimensions" into separate 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 will 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 will not require changing shape classes and vice versa.
Abstraction and Implementation
The GoF book introduces the terms Abstraction and Implementation as part of the definition of Bridge pattern. In my opinion, they sound too academic and make the pattern look harder than it really is. But having read the simple example from above, let's see what GoF really meant.
Abstraction, also called 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 an Implementation layer, sometimes also called Platform. Just do not mix it up with interfaces and abstract classes from your programming language, these are not the same things.
For example, when talking about real applications, Abstraction can represent a graphical user interface (GUI), and Implementation could be a underlying operating system's API, which GUI layer calls in response to user interactions.
There are two directions in which 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 have 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 will 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 declares the common interface for all Concrete implementations. Abstraction can 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 is 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.
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 at runtime.
Although it is optional, Bridge pattern allows changing the implementation object inside the abstraction. That is 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 the 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.