The Bridge pattern seeks to decouple an abstraction from its implementation such that both can vary independently. In effect, the Bridge maintains a reference to both abstraction and implementation but doesn't implement either, thereby allowing the details of both to remain in their distinct classes.
In object-oriented programming, the concept of inheritance is crucial to developing objects. This binds the implementation to its abstraction, and often that's exactly what we want. However, there can be scenarios in which one class inheriting from another might not be the best solution, particularly when multiple inheritances can be used. Into this void steps the Bridge pattern.
This pattern is especially useful for scenarios in which changes to the implementation of an object should have no bearing on how their clients use said implementations. Bridge also differs from the Adapter pattern in that Bridge is used when designing new systems while Adapter is used to adapt old systems to new ones.
NOTE: This post is part of a series demonstrating software design patterns using C# and .NET. The patterns are taken from the book Design Patterns by the Gang of Four. Check out the other posts in this series!
The Rundown
- Type: Structural
- Useful? 3/5 (Sometimes)
- Good For: Allowing lots of variation between implementations of interfaces.
- Example Code: On GitHub
The Participants
- The Abstraction defines an interface and maintains a reference to an Implementor.
- The RefinedAbstraction extends the interface defined by the Abstraction.
- The Implementor defines the interface for the ConcreteImplementor objects. This interface does not need to correspond to the Abstraction's interface.
- The ConcreteImplementor objects implement the Implementor interface.
A Delicious Example
In real life, my brother has Celiac disease, and this means that his body cannot properly process gluten. He cannot eat wheat, rye, barley, oats, or anything made from any of those ingredients; if he does, he's probably going to be unwillingly asleep for the next six hours or so and could cause permanent damage to his digestive system.
Consequently it can be difficult for him to order a meal from restaurants, since often they don't provide the proper special-needs meal he 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). In his honor, let's model a system by which we can order various special-needs meals from many different restaurants.
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 are (e.g. a dairy-free meal from a diner or a gluten-free meal from a fancy restaurant).
In a traditional inheritance model, we might have the following classes:
interface IOrder { }
class DairyFreeOrder : IOrder { }
class GlutenFreeOrder : IOrder { }
But what if we also need to keep track of what kind of restaurant the order came from? This has no real relation to what the meal consists of, but might still be part of the model. In this case, we might end up with a crazy inheritance tree:
interface IOrder { }
class DairyFreeOrder : IOrder { }
class GlutenFreeOrder : IOrder { }
interface IDinerOrder : IOrder { }
class DinerDairyFreeOrder : DairyFreeOrder, IDinerOrder { }
class DinerGlutenFreeOrder : GlutenFreeOrder, IDinerOrder { }
interface IFancyRestaurantOrder : IOrder { }
class FancyRestaurantDairyFreeOrder : DairyFreeOrder, IFancyRestaurantOrder { }
class FancyRestaurantGlutenFreeOrder : GlutenFreeOrder, IFancyRestaurantOrder { }
So we're only modeling two orthogonal properties (dairy-free vs gluten-free and diner vs fancy restaurant) but we need three interfaces and six classes? This seems like overkill, and it is. 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. What we want is to end up with something like this:
interface IOrder { }
class DairyFreeOrder : IOrder { } //This class keeps a
//private reference to an IRestaurantOrder
class GlutenFreeOrder : IOrder { } //This class also keeps
//a private reference to an IRestaurantOrder
interface IRestaurantOrder : IOrder { }
class DinerOrder : IRestaurantOrder { }
class FancyRestaurantOrder : IRestaurantOrder { }
Let's expand this to fully implement the Bridge pattern. First need to write our Implementor participant which will define a method for placing an order:
/// <summary>
/// Implementor which defines an interface for placing an order
/// </summary>
public interface IOrderingSystem
{
void Place(string order);
}
We also need the Abstraction participant, which for this demo is an abstract class and will define a method for sending an order and keep a reference to the Implementor:
/// <summary>
/// Abstraction which represents the sent order
/// and maintains a reference to the restaurant where the order is going.
/// </summary>
public abstract class SendOrder
{
//Reference to the Implementor
public IOrderingSystem _restaurant;
public abstract void Send();
}
Now we can start defining our RefinedAbstraction classes. For this demo, let's take those two kinds of special-needs meals from earlier (dairy-free and gluten-free) and implement RefinedAbstraction objects for them.
/// <summary>
/// RefinedAbstraction for a dairy-free order
/// </summary>
public class SendDairyFreeOrder : SendOrder
{
public override void Send()
{
_restaurant.Place("Dairy-Free Order");
}
}
/// <summary>
/// RefinedAbstraction for a gluten free order
/// </summary>
public class SendGlutenFreeOrder : SendOrder
{
public override void Send()
{
_restaurant.Place("Gluten-Free Order");
}
}
Finally, we need to define the restaurants themselves (which are our ConcreteImplementor participants).
/// <summary>
/// ConcreteImplementor 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.");
}
}
/// <summary>
/// ConcreteImplementor for an ordering system at a fancy restaurant.
/// </summary>
public class FancyRestaurantOrders : IOrderingSystem
{
public void Place(string order)
{
Console.WriteLine("Placing order for "
+ order + " at the Fancy Restaurant.");
}
}
Finally, we create a Main()
which uses the Bridge to create different orders and send them to different restaurants.
static void Main(string[] args)
{
SendOrder _sendOrder = new SendDairyFreeOrder();
_sendOrder._restaurant = new DinerOrders();
_sendOrder.Send();
_sendOrder._restaurant = new FancyRestaurantOrders();
_sendOrder.Send();
_sendOrder = new SendGlutenFreeOrder();
_sendOrder._restaurant = new DinerOrders();
_sendOrder.Send();
_sendOrder._restaurant = new FancyRestaurantOrders();
_sendOrder.Send();
Console.ReadKey();
}
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 actually 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:
Will I Ever Use This Pattern?
Probably. As mentioned above, this pattern is very useful when designing systems where multiple different kinds of inheritance are possible; Bridge allows you to implement these inheritances without tightly binding to their abstractions.
That said, this is one of those patterns where the complexity needed to implement it may well cancel out its benefits. Don't forget to think critically about your situation and determine if you really need a pattern before you start refactoring toward it!
Summary
The Bridge pattern seeks to allow abstractions and implementation to vary independently. This becomes useful when dealing with situations where regular inheritance would make us end up with too many classes.
Now if only ordering special meals like this was a little easier to do in the real world...
Happy Coding!