Some people consider the Bridge as one of the more confusing patterns in the GoF pattern catalog. Not because it has such a complicated structure, but rather because its official description raises more questions than answering them:
[The Bridge] decouples an abstraction from its implementation so that the two can vary independently.
This explanation sounds a bit odd: What is meant by abstraction and implementation? Are these the same terms we use to describe the standard OOP concepts? Or are they used in a different context here, and case of the latter, what do they actually mean then?
Okay, but we won't let the description mislead us. To better understand this pattern, we instead look at what concrete problems it tries to solve. There are actually two use cases where it really shines:
- Reducing inheritance hierarchies through separation
- Composing more complex functionality from lower-level implementations
We will look at both of them.
Reducing Inheritance Hierarchies through Separation
There is a well-known problem when using inheritance to model different characteristics or features of a domain called Combinatorial Explosion.
To demonstrate this issue, we will pretend we implement a fantasy-based role-playing video game (RPG). In our RPG, players could choose between professions for their characters, like warriors or mages. But that's not all: A character is also bound to a specific weapon, be it a sword or a staff (note that in our game, even mages can wield a sword - some might consider this imbalanced, but we will overlook this for now...).
Since we have two professions and two weapons, using static inheritance to implement our game would result in four implementation classes - one for each possible combination.
Maybe not the best approach you think, but we've all seen worse, so let's stick with it for now. Later on, the game designer suggests that there should be another profession to choose from, a Ranger. A ranger can also use swords and staffs, so we need two more implementation classes.
Slowly you're getting nervous as you have a bad feeling where this is going... Two days later, your game designer approaches you and asks whether we could introduce a new kind of magic weapon, something like a firesword maybe? Since you now have three different types of profession, each of them needs a new implementation for the firesword.
Oh, and did I mention that now the game should also include Barbarians, Paladins, and Necromants as professions?
And of course, all of them should later also be able to use range weapons like bows or crossbows...
The problem we're facing here is that we have two different features, professions and weapons. Since both can be combined, we need a new class for every possible pairing, if the number of features grow, this cleary does not scale very well.
This is what we call a combinatorial explosion, and it is quite problematic, as a design like this would sooner or later become unmanageable.
The cause of this issue is that we tried to fit both features into a single inheritance tree. A better approach would be to define one separate class hierarchy for each feature. For our example, this would result in two decoupled class trees, one for professions and one for weapons. Both trees are separated from each other and can evolve independently.
However, since we isolated our features from each other, we somehow have to connect them again. We can achieve this by adding a reference pointing to a weapon in our profession. This connection is also known as a bridge, and now we see where the pattern got its name.
That's basically the whole structure of our pattern: We separate our hierarchy into two distinct trees and use composition to connect them again. Instead of implementing the logic in one class, we put the code into a separate implementation class and use a reference to call it.
If you now consider our Profession and Weapon example, the terms Abstraction and Implementation make also sense now: The professions in the left tree have access to a weapon but don't care about its concrete type. They're only interested in the behavior the weapon provides (which is adding damage to the target they hit). They use only an abstraction of a weapon without knowing its exact kind. In contrast, the right tree consists of concrete implementations of these weapons - like swords or staffs. From this perspective, the naming conventions of this pattern - while perhaps not the best choice - still sound plausible.
Reducing combinatorial complexity may be the Bridge's primary purpose, but the pattern can also become handy when writing code for different platforms.
Composing complex functionality from lower-level implementations
One problem you sometimes face when developing for different platforms is the need to rely on relatively low-level System API. Take, for example, the graphical APIs provided by operating systems. They provide several functions to draw primitives like lines, rectangles, or polygons (or 3D shapes if you look at APIs like OpenGL or DirectX). Since these APIs are pretty basic, working with them to create more complex applications can be very cumbersome. And despite that, they're often incompatible across different operating systems, requiring you to reimplement your logic for each platform.
In these cases, using a Bridge can help reduce the implementation effort: The low-level, OS-dependent code is placed in the Implementation part. We define an interface that covers the available functionality and provide an implementation for each platform. The Abstraction side can now use this interface to compose more advanced, application-specific logic.
Talking about a real-world scenario, we once had to implement a building/facility management system for a video game. A bit like Sim City but much smaller in functionality, as it was only a part of another game.
Since the game engine was quite heavy to fire up and run, we prototyped the game logic for the building part separately with a small DirectX-based renderer. This renderer had just enough functionality to draw most of the required primitives like tiles, rectangles, textures, etc. This was the Implementation part of the Bridge. The other part, the Abstraction, used the renderer interface to draw more sophisticated game objects, like complete buildings, area tiles, or other UI elements. It simply did this by combining different methods from the renderer as needed.
When we moved from prototyping to production, we switched our custom renderer with the rendering engine integrated into our game system. Thanks to the Bridge, this part was relatively straightforward: Since the renderer implemented only low-level drawing routines, these were easily reimplemented. The more complex logic for drawing complex game objects was already available and could stay untouched.
Compared to the example from the beginning, this is a simplified bridge as there is only a single class on the Abstraction side. However, here the goal was not to decouple a complex hierarchy but to simplify replacing the rendering engine, which did work surprisingly well.
Interesting, but is there anything I should consider when using the pattern?
The pattern replaces static class hierarchies by using composition. Albeit static inheritance is inflexible, it's also sometimes easier to understand since it cannot change during runtime. Composition, on the other side, makes a system much more dynamic. Replacing different parts during runtime is now easily possible. While this can sometimes even be desired, it also makes your design harder to understand.
The Bridge also reduces the cohesiveness of your classes. The logic previously located in a single class is now split between the Abstraction and the Implementation. If you recognize that both parts are too strongly related and often need to be changed in conjunction, then using a Bridge is not the best solution, as it makes your system harder to maintain.
Another important aspect is that all your implementations must share a common interface, which is not always possible. Take, for example, the different desktop UIs used by macOS, Windows, or Linux: Each of their windows behaves a bit differently. Some UI controls even exist only on one platform or look and function differently on each system. Implementing this via a shared interface would mean ignoring all platform-specific advantages and limiting yourself to the lowest common denominator of all systems - often not a good solution. Therefore, it's always necessary to check upfront whether such a shared interface exists for your domain objects.
Are there alternatives to the bridge pattern?
There exist different concepts similar to the bridge, all with their own pros and cons. Let's check out some of them.
Using Multiple Inheritance. The main problem of combinatorial explosion in OOP languages that support only single inheritance is that you have to reimplement large parts of the logic for each of your subclasses. In a language with multiple inheritance, like C++, you could solve this problem by using more than one base class. You still end up with the same amount of classes but with fewer implementations - since you can reuse most of your base classes' code.
With multiple inheritance, we could restructure our example from above in the following way:
Still, this is barely a good solution as, in most cases, multiple inheritance creates more problems than it tries to solve. Consider, for instance, how to handle the case that two or more base classes have the same attribute or method; how do you ensure that the correct one is always used? Also, when you rely on static inheritance, you deprive yourself of the opportunity to replace parts of your implementation at runtime.
Entity Component System. An Entity-Component-System (ECS) is a more sophisticated alternative to the Bridge pattern that has become quite popular in video game development. It has the same purpose - avoiding the combinatorial explosion of sub-classes - but takes a different approach: In an ECS, there are two types of objects: Components that provide specific data and sometimes also behavior, and Entities that represent the actual game objects. To provide an Entity with additional features or characteristics, you simply add a matching Component to it. These systems are very flexible, as Components can be added or removed dynamically, making adding new features almost effortless. Many of today's 3D engines, for example, Unity, use variations of an Entity-Component-System. However, implementing a full-blown ECS is a complex task and probably oversized for most day-to-day problems. A Bridge is often far easier to implement.