The Bridge is a structural design pattern that lets us split a large class or a set of closely related classes into two separate hierarchies -abstraction and implementation- which can be developed independently of each other.
The Bridge design pattern decouples an abstraction from its implementation. As a result, the abstraction and implementation can be developed and changed independently. This pattern's primary use is to enable the separation of concerns.
In effect, the Bridge maintains a reference to both abstraction and implementation but doesn't implement either, thus allowing the details of both to remain in their distinct classes.
You can find the example code of this post, on GitHub
Conceptualizing the Problem
Imagine that we are working on a CAD application. Currently, the application has an IShape
class that provides functions for the various geometrical shapes, and some subclasses: Circle
, Triangle
and Rectangle
.
We now want to extend this class hierarchy to incorporate colours, so we need to create a Red
, Green
and Blue
shape subclasses. However, we already have three subclasses, so we need to create nine class combinations like BlueCircle
and GreenTriangle
.
Adding new shape types and colours to this hierarchy will make it grow exponentially. If, for example, we want to add the Pentagon
shape, we will need to add three additional subclasses, one of each colour. If we want to add the Magenta
colour, we need to update all three existing shapes. And finally, if we want to add both, we need seven new subclasses to account for all shapes and colours. As you can imagine, the further we go, the worse it becomes.
The problem occurs because we're trying to extend the shape classes in two independent dimensions, form-wise and colour-wise.
The Bridge pattern attempts to solve this problem by switching from inheritance to object composition. What this means is that we extract one of the dimensions into a separate class hierarchy, so that the original classes will reference an object of the new hierarchy, instead of having all of its state and behaviours within one class.
Following this approach, we can extract the colour-related code into its class with three subclasses: Red
, Green
and Blue
. The IShape
interface then gets a reference field pointing to one of the colour objects. Now the shape can delegate any colour-related work to the linked colour object. That reference will act as a bridge between the IShape
and IColor
classes. From now on, adding new colours won't require changing the shape hierarchy, and vice versa.
Now that we have established a simple example with shapes and colours, let's decipher the meaning of the terms Abstraction and Implementation.
Abstraction, which is also called an interface, is a high-level control layer for some entities. This layer isn't supposed to do any real work on its own. It should only delegate the work to the implementation layer, which is also called the platform.
Note that we are talking about interfaces and abstract classes of C#. These aren't the same things.
When talking about real applications, abstraction and implementation can be implemented by any type of system. For example, the abstraction can be represented by a graphical user interface, and the implementation could be the underlying backend system (API) which the GUI layer calls in response to user interactions.
Generally speaking, we can extend such an application in two independent directions:
- Have different interfaces, for instance, tailored for regular users and administrators.
- Have different APIs, for instance, having the app launching on Windows, macOS or Linux operating systems.
In the worst-case scenario, the application might look like a big ball of mud, where hundreds of conditionals connect different types of interfaces and implementations all over the code.
We can bring order to this chaos by extracting the code related to specific interface-platform combinations into separate classes. However, soon we'll discover that there are lots of these classes. The class hierarchy will grow exponentially because adding a new GUI or supporting a different API would require creating more and more classes.
Let's try to solve this issue with the Bridge pattern. It suggests that we divide the classes into two hierarchies, Abstractions, which will be the GUI layer of the application and Implementations, which will be the operating systems' APIs.
The abstraction object will control the appearance of the application, delegating the actual work to the linked implementation object. Different implementations are interchangeable as long as they follow a common interface, enabling the same GUI to work under Windows and Linux.
As a result, we can change the GUI classes without touching the API-related classes. Moreover, adding support for another operating system only requires creating a subclass in the implementation hierarchy.
Structuring the Bridge Pattern
In this basic implementation, the Bridge pattern has 5 participants:
- Abstraction: The Abstraction provides high-level control logic. It relies on the implementation object to do the actual low-level work.
- Implementation: The Implementation declares the interface that's common for all concrete implementations. An abstraction can only communicate with an implementation object via methods that are declared here.
The abstraction may list the same methods as the implementation, but usually, the abstraction declares some complex behaviours that rely on a wide variety of primitive operations declared by the implementation.
- Concrete Implementation: The Concrete Implementations contain platform-specific code.
- Refined Abstraction: The Refined Abstractions provide variants of control logic. Like their parent, they work with different implementations via the general implementation interface.
- Client: Usually, the Client is only interested in working with the abstraction. However, it's the client's job to link the abstraction object with one of the implementation objects.
To demonstrate how the Bridge pattern works, we are going to create an ordering system for people who have special dietary needs. Many people have diseases like Celiac diseaseor AGS that prohibit them from consuming certain foodstuff. Consequently, it can be difficult for a person with such issues to order a meal from restaurants, since often they don't provide the proper special-needs meal with the person's needs (and even if they do, the environment in which the food is prepped is often not properly ventilated or sterilized, making cross-contamination likely).
The idea goes like this: I should be able to pick a type of special meal and pick a restaurant, without needing to know exactly what either of those things is (e.g. a meat-free meal from a diner or a gluten-free meal from a fancy restaurant).
If we used a traditional inheritance model, we might have the following classes:
public interface IOrder { }
public class MeatFreeOrder : IOrder { }
public class GlutenFreeOrder : IOrder { }
But what if we also need to keep track of what kind of restaurant the order came from? There is no real relation between the meal and the restaurant, but it might still be part of the model.
In this case, we might end up with a convoluted inheritance tree:
public interface IOrder { }
public class MeatFreeOrder : IOrder { }
public class GlutenFreeOrder : IOrder { }
public interface IDinerOrder : IOrder { }
public class DinerMeatFreeOrder : MeatFreeOrder, IDinerOrder { }
public class DinerGlutenFreeOrder : GlutenFreeOrder, IDinerOrder { }
public interface IFineDiningOrder : IOrder { }
public class FineDiningMeatFreeOrder : MeatFreeOrder, IFineDiningOrder { }
public class FineDiningGlutenFreeOrder : GlutenFreeOrder, IFineDiningOrder { }
Here we're modelling two orthogonal properties (meat-free/gluten-free and diner/fine dining) but we need three interfaces and six classes. We can simplify this using the Bridge pattern.
The Bridge pattern seeks to divide the responsibility of these interfaces such that they're much more reusable. With that we will end up with something like the following;
public interface IOrder { }
public class MeatFreeOrder : IOrder { }
public class GlutenFreeOrder : IOrder { }
public interface IRestaurantOrder : IOrder { }
public class DinerOrder : IRestaurantOrder { }
public class FineDiningOrder : IRestaurantOrder { }
Let's expand this to full implementation of the Bridge pattern. First, we need our Implementation participant who will define a method for placing an order:
namespace Bridge
{
/// <summary>
/// Implementation participant which defines an interface for placing an order
/// </summary>
public interface IOrderingSystem
{
public void Place(string order);
}
}
We also need the Abstraction participant, which will be an abstract class and will define a method for sending an order and keep a reference to the Implementation:
namespace Bridge
{
/// <summary>
/// Abstraction which represents the sent order
/// and maintains a reference to the restaurant where the order is going.
/// </summary>
public abstract class OrderHandler
{
public IOrderingSystem _restaurant;
public abstract void SendOrder();
}
}
Now we can start defining our RefinedAbstraction classes. Let's take those two kinds of special-needs meals from earlier (meat-free and gluten-free) and implement RefinedAbstraction objects for them.
namespace Bridge
{
/// <summary>
/// RefinedAbstraction for a meat-free order
/// </summary>
public class MeatFreeOrder : OrderHandler
{
public override void SendOrder()
{
_restaurant.Place("Meat-Free Order");
}
}
/// <summary>
/// RefinedAbstraction for a gluten-free order
/// </summary>
public class GlutenFreeOrder : OrderHandler
{
public override void SendOrder()
{
_restaurant.Place("Gluten-Free Order");
}
}
}
Finally, we need to implement our ConcreteImplementation participants, which are the restaurants themselves:
namespace Bridge
{
/// <summary>
/// ConcreteImplementation for an ordering system at a diner.
/// </summary>
public class DinerOrders : IOrderingSystem
{
public void Place(string order)
{
Console.WriteLine($"Placing order for {order} at the Diner");
if (order.Equals("Meat-Free Order"))
Console.WriteLine("\tDish: Bean & Halloumi Stew");
if (order.Equals("Gluten-Free Order"))
Console.WriteLine("\tDish: Stuffed Peppers");
}
}
/// <summary>
/// ConcreteImplementation for an ordering system at a fancy restaurant
/// </summary>
public class FineDiningRestaurantOrders : IOrderingSystem
{
public void Place(string order)
{
Console.WriteLine($"Placing order for {order} at the Fine Dining Restaurant");
if (order.Equals("Meat-Free Order"))
Console.WriteLine("\tDish: Vegetarian Mushroom Risotto With Truffle Oil");
if (order.Equals("Gluten-Free Order"))
Console.WriteLine("\tDish: Arroz de Bacalhau");
}
}
}
Finally, we need a Main()
which uses the Bridge to create different orders and send them to different restaurants:
using Bridge;
OrderHandler meatFreeOrderHandler = new MeatFreeOrder
{
_restaurant = new DinerOrders()
};
meatFreeOrderHandler.SendOrder();
meatFreeOrderHandler._restaurant = new FineDiningRestaurantOrders();
meatFreeOrderHandler.SendOrder();
Console.WriteLine();
OrderHandler glutenFreeOrderHandler = new GlutenFreeOrder
{
_restaurant = new DinerOrders()
};
glutenFreeOrderHandler.SendOrder();
glutenFreeOrderHandler._restaurant = new FineDiningRestaurantOrders();
glutenFreeOrderHandler.SendOrder();
If we run the app, we find that we can send any order to any restaurant. Further, if any of the abstractions (the orders) change their definition, the implementors don't care; and vice-versa, if the implementors change their implementation, the abstractions don't need to change as well.
Here's a screenshot of the demo app in action:
Applicability of the Bridge Pattern
The Bridge pattern is quite helpful in the following scenarios:
- Avoid coupling between abstraction and implementation.
- Having extensible abstraction and implementation.
- Changes in implementation can affect the client.
- Organizing and splitting a monolithic class with numerous functionality alternatives.
Avoiding coupling between abstraction and implementation
Although it's optional, the Bridge pattern lets us replace the implementation object inside the abstraction. It's as easy as assigning a new value to a field.
By the way, this is the main reason why people confuse the Bridge pattern with the Strategy pattern. Remember that a pattern is more than just a certain way to structure your classes. It can also communicate intent and a problem being addressed.
Having extensible Abstraction and Implementation
The Bridge suggests that we extract a separate class hierarchy for each of the dimensions. The original class delegates the related work to the objects belonging to those hierarchies instead of doing everything on its own.
Changes in Implementation can affect the client
The Bridge pattern can help you avoid any changes done in the implementation that would affect your client, or there wouldn’t be a need to recompile the code on the client’s side after some changes in implementation.
Organizing and splitting a monolithic class with numerous functionality alternatives
The bigger a class becomes, the harder it is to figure out how it works, and the longer it takes to make a change. The changes made to one of the variations of functionality may require making changes across the whole class, which often results in making errors or not addressing some critical side effects.
The Bridge pattern lets you split the monolithic class into several class hierarchies. After this, you can change the classes in each hierarchy independently of the classes in the others. This approach simplifies code maintenance and minimizes the risk of breaking existing code.
Pros and Cons of the Bridge Pattern
✔We can create platform-independent classes and apps. | ❌We might make the code more complicated by applying the pattern to a highly cohesive class. |
✔ The client code works with high-level abstractions. It isn’t exposed to the platform details. | |
✔ We can introduce new abstractions and implementations independently from each other, thus satisfying the Open/Closed Principle. | |
✔ We can focus on high-level logic in the abstraction and on platform details in the implementation, thus satisfying the Simple Responsibility Principle |
Relations with Other Patterns
- The Bridge is usually designed up-front letting us develop parts of an application independently of each other. On the other hand, the Adapter is commonly used with an existing app to make some otherwise-incompatible classes work together nicely.
- Bridge, State, Strategy and Adapter have very similar structures. Indeed, these patterns are based on composition, which is delegating work to other objects. However, they all solve different problems. A pattern isn’t just a recipe for structuring our code in a specific way. It can also communicate to other developers the problem the pattern solves.
- We can use the Abstract Factory along with the Bridge pattern. This pairing is valuable when some abstractions defined by the Bridge pattern can only work with specific implementations. In this case, the Abstract Factory can encapsulate these relations and hide the complexity from the client code.
- We can combine the Builder pattern with the Bridge pattern: the director class plays the role of abstraction, while different builders act as implementations.
Final Thoughts
In this article, we have discussed what is the Bridge pattern, when to use it and what are the pros and cons of using this design pattern. We then examined some use cases for this pattern and how the Bridge relates to other classic design patterns.
It's worth noting that the Bridge pattern, along with the rest of the design patterns presented by the Gang of Four, is not a panacea or a be-all-end-all solution when designing an application. Once again it's up to the engineers to consider when to use a specific pattern. After all these patterns are useful when used as a precision tool, not a sledgehammer.
Top comments (0)