Bridge Design Pattern

Classic GOF Structural Design Patterns

 
  

Intent

The Bridge Pattern is used to decouple an abstraction from its implementation so that the two can vary independently.

It is used when:

  • Abstractions and implementations should not be bound at compile time.
  • Abstractions and implementations should be independently extensible.
  • Changes in the implementation of an abstraction should have no impact on clients.
  • Implementation details should be hidden from the client.
bridge patterns
  

Problem

The Bridge pattern should be used when both the class as well as what it does vary often. The bridge pattern can also be thought of as two layers of abstraction. It is commonly applied when the abstractions and implementations should not be bound at compile time, and they both should be independently extensible.

  

Discussion

Let's reiterate what the GoF say about the Bridge Pattern:

Decouples an abstraction from its implementation so that the two can vary independently.

An abstraction, in this definition, means a generalized way of looking at something without requiring you to understand how it actually is implemented. For example, a steering wheel is an abstraction on a car. Let's face it, for many folks, turning the steering wheel makes the car change direction; how it actually accomplishes this is completely abstracted away from the users. The abstraction of a steering wheel actually protects users from having to know all the mechanical details of how the change in direction is accomplished. If the methods were different from one car brand to another, the user doesn't care.

Given this definition of an abstraction, it is easy to see what an implementation is - it is merely the realization of an abstraction. It makes the abstraction real in a specific case. As you can guess, abstractions and implementations are normally rather tightly coupled. For a specific class of problems, this coupling is beneficial and works to our advantage.

But, let's expand the scope of our problem slightly. What happens if we want to use the steering wheel in other types of vehicles - boats, airplanes, or tanks. We can put a car steering wheel in each of these vehicles; however, steering these types of vehicles is dramatically different than how steering a car works. Since the steering wheel (abstraction) is tightly coupled to the implementation, it will not work for us in these cases. What we need to do is to decouple the abstraction of the steering wheel from its specific implementation of steering a car. We need an abstraction of an abstraction - we need to make a vehicle steering wheel rather that a car steering wheel. This is the purpose of the bridge pattern - to create an abstraction of an abstraction.


bridge pattern discussion example

With this approach, we can create different steering wheels for each type of vehicle that aren’t coupled to a specific steering system. The bridge pattern makes it so that the abstraction of a steering wheel is decoupled from the implementation of the specific steering systems, like using turning an axle or raising or lowering a wing-flap.

  

Structure

The Client doesn't want to deal with platform-dependent details. The Bridge pattern encapsulates this complexity behind an abstraction "wrapper". The Bridge emphasizes identifying and decoupling "interface" abstraction from "implementation" abstraction.


bridge pattern structure

As you can see in the diagram above, the the structure is fairly basic (note that the actual "bridge" in the pattern is outlined in red in the drawing above):

  • Abstraction - defines the abstraction's interface and maintains a reference to an object of type Implementor. The abstraction mainly contains a control logic like a user interface. Abstraction's code relies on the Implementation object to do the real job.
  • Refined Abstraction - extends the interface defined by Abstraction. 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.
  • Implementor - defines the interface for implementation classes. This interface doesn't have to correspond exactly to Abstraction's interface; in fact the two interfaces can be quite different. Typically the Implementation interface provides only primitive operations, and Abstraction defines higher-level operations based on these primitives.
  • ConcreteImplementor - implements the Implementor interface and defines its concrete implementation. Essentially, Concrete Implementations contain platform-specific code.
  

Practical Example

For a practical example of the Bridge Pattern in a real world usage scenario, lets imagine that we have a simple web application framework that allows you to create Blogs, host a Store, and a News site. Functionally, this is pretty basic. The situation gets more complicated though when we want to start introducing the concept of themes. Instead of a simple hierarchy, we can see that this can quickly lead to an explosion of classes. It gets even worse when we want to add additional themes in the future.


gof adapter example problem

Much like the generic diagram in STRUCTURE above, by splitting the problem into two separate hierarchies and allowing for a bridge approach, we can see that I would be able to vary either side of the bridge without effecting the other side. In other words, I could add additional web application types and take advantage of themes or I could add additional themes and still allow for my web application instances to take advantage of them. Another way of stating this is the definition of the bridge pattern, we have decoupled an abstraction from its implementation so that the two can vary independently


gof adapter example solution

Let's extend this sample with some practical code below.

  

Code Samples

Below, you will find examples of the Bridge implemented in four different commonly used languages. Each one of the examples implements the exact same scenario as described in the PRACTICAL EXAMPLE section above.

Each sample will use a Bridge to allow for adding either new web application types or new themes..


C# Bridge Code Sample

In this C# example, the Bridge pattern is used


Java Bridge Code Sample

In this Java example, the Bridge pattern is used.


Typescript Bridge Code Sample

In this TypeScript example, the Bridge pattern is used


JavaScript Bridge Code Sample

In this JavaScript example, the Bridge pattern is used.


  

Considerations

  1. 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.
  2. Design the separation of concerns: what does the client want, and what do the platforms provide.
  3. Design a platform-oriented interface that is minimal, necessary, and sufficient. Its goal is to decouple the abstraction from the platform.
  4. Define a derived class of that interface for each platform.
  5. Create the abstraction base class that "has a" platform object and delegates the platform-oriented functionality to it.
  6. Define specializations of the abstraction class if desired.
  

Additional Notes

  • Adapter makes things work after they're designed; Bridge makes them work before they are.
  • Bridge is designed up-front to let the abstraction and the implementation vary independently. Adapter is retrofitted to make unrelated classes work together.
  • 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.
  • The structure of State and Bridge are identical (except that Bridge admits hierarchies of envelope classes, whereas State allows only one). The two patterns use the same structure to solve different problems: State allows an object's behavior to change along with its state, while Bridge's intent is to decouple an abstraction from its implementation so that the two can vary independently.
  • If interface classes delegate the creation of their implementation classes (instead of creating/coupling themselves directly), then the design usually uses the Abstract Factory pattern to create the implementation objects.
  • One of the major drawbacks of this pattern is that, in providing flexibility, it increases complexity.
  • There's also possible performance issues with the indirection of messages - the abstraction needs to pass messages along to the implementator for the operation to get executed.