Today, I want to discuss another structural design pattern: the Bridge Pattern. We'll implement this pattern using .NET and explore its strengths and weaknesses. Let’s dive in!
What is the Bridge Pattern?
The Bridge Pattern is a structural design pattern that decouples an abstraction from its implementation, allowing both to evolve independently. This separation enables you to extend functionality without modifying existing business logic. Essentially, the Bridge Pattern introduces an interface (the "bridge") that encapsulates the core logic, which can then be extended or redefined without affecting the abstraction.
Implementing the Bridge Pattern
Let’s start by defining the interface that will act as the bridge. This interface will unify similar logic and be used in the abstraction layer. For this example, we’ll use electronic devices.
public interface IDevice
{
void TurnOn();
void TurnOff();
void SetVolume(int volume);
}
Next, we’ll create two concrete implementations of this interface: one for a TV and another for a radio. Both devices share similar functionality.
public class BridgeTelevision : IDevice
{
public void TurnOn() => Console.WriteLine("TV is turned ON");
public void TurnOff() => Console.WriteLine("TV is turned OFF");
public void SetVolume(int volume) => Console.WriteLine($"TV volume set to {volume}");
}
public class BridgeRadio : IDevice
{
public void TurnOn() => Console.WriteLine("Radio is turned ON");
public void TurnOff() => Console.WriteLine("Radio is turned OFF");
public void SetVolume(int volume) => Console.WriteLine($"Radio volume set to {volume}");
}
Implementing the Abstraction
Now, let’s implement the abstraction. You can choose between an abstract class or an interface for this purpose. While both options are valid, I prefer using an abstract class in this case because it offers slightly better performance, especially when methods don’t need to be overridden. However, an interface would work just as well if flexibility is a priority.
public abstract class Remote(IDevice device)
{
protected readonly IDevice Device = device;
public virtual void PowerButton()
{
Console.WriteLine("Basic Remote: Power Button Pressed.");
Device.TurnOn();
}
public virtual void VolumeUp()
{
Console.WriteLine("Basic Remote: Increasing Volume.");
Device.SetVolume(10);
}
public virtual void VolumeDown()
{
Console.WriteLine("Basic Remote: Decreasing Volume.");
Device.SetVolume(5);
}
}
To demonstrate flexibility, I’ve overridden the methods in a derived class:
public class BridgeRemoteControl(IDevice device) : Remote(device)
{
public override void PowerButton()
{
Console.WriteLine("Override Remote: Power Button Pressed.");
Device.TurnOn();
}
public override void VolumeUp()
{
Console.WriteLine("Override Remote: Increasing Volume.");
Device.SetVolume(10);
}
public override void VolumeDown()
{
Console.WriteLine("Override Remote: Decreasing Volume.");
Device.SetVolume(5);
}
}
Running the Example
Let’s see the Bridge Pattern in action. Notice how we can pass different devices to a single class without the abstraction layer knowing the specifics of the concrete implementations.
IDevice tv = new BridgeTelevision();
Remote basicRemote1 = new BridgeRemoteControl(tv);
basicRemote1.PowerButton();
basicRemote1.VolumeUp();
Console.WriteLine();
IDevice radio = new BridgeRadio();
Remote basicRemote2 = new BridgeRemoteControl(radio);
basicRemote2.PowerButton();
basicRemote2.VolumeUp();
The output will be consistent since the methods are similar to those in the base class.
Extending Functionality
One key benefit of the Bridge Pattern is its extensibility. For instance, if we want to add a "mute" feature to the radio, we can do so without affecting other devices.
public class ExtendedBridgeRemoteControl(IDevice device) : Remote(device)
{
public void Mute()
{
Console.WriteLine("Advanced Remote: Muting Device.");
Device.SetVolume(0);
}
}
To use this new functionality, we simply update the calling code:
IDevice tv = new BridgeTelevision();
Remote basicRemote = new BridgeRemoteControl(tv);
basicRemote.PowerButton();
basicRemote.VolumeUp();
Console.WriteLine();
IDevice radio = new BridgeRadio();
ExtendedBridgeRemoteControl extendedBridgeRemoteControl = new ExtendedBridgeRemoteControl(radio);
extendedBridgeRemoteControl.PowerButton();
extendedBridgeRemoteControl.Mute();
The updated output will reflect the new functionality:
As you can see, we only made two changes to extend the radio’s functionality, and the TV remained unaffected.
A Non-Bridge Approach: Why It’s Problematic
What if you weren’t aware of the Bridge Pattern? You might end up creating separate classes for each device, tightly coupling the abstraction and implementation.
public class NonBridgeTelevision
{
public void TurnOn() => Console.WriteLine("TV is turned ON");
public void TurnOff() => Console.WriteLine("TV is turned OFF");
public void SetVolume(int volume) => Console.WriteLine($"TV volume set to {volume}");
}
public class NonBridgeRadio
{
public void TurnOn() => Console.WriteLine("Radio is turned ON");
public void TurnOff() => Console.WriteLine("Radio is turned OFF");
public void SetVolume(int volume) => Console.WriteLine($"Radio volume set to {volume}");
}
You’d then need to create a separate remote control class for each device:
public class NonBridgeRemoteControlForTelevision(NonBridgeTelevision tv)
{
public void PowerButton()
{
Console.WriteLine("Basic Remote: Power Button Pressed.");
tv.TurnOn();
}
public void VolumeUp()
{
Console.WriteLine("Basic Remote: Increasing Volume.");
tv.SetVolume(10);
}
public void VolumeDown()
{
Console.WriteLine("Basic Remote: Decreasing Volume.");
tv.SetVolume(5);
}
}
public class NonBridgeRemoteControlForRadio(NonBridgeRadio radio)
{
public void PowerButton()
{
Console.WriteLine("Advanced Remote: Power Button Pressed.");
radio.TurnOn();
}
public void VolumeUp()
{
Console.WriteLine("Basic Remote: Increasing Volume.");
radio.SetVolume(10);
}
public void VolumeDown()
{
Console.WriteLine("Basic Remote: Decreasing Volume.");
radio.SetVolume(5);
}
}
If you later decide to extend the radio’s functionality, you’d need to modify the existing class:
public void Mute()
{
Console.WriteLine("Advanced Remote: Muting Device.");
radio.SetVolume(0);
}
And update the calling code:
NonBridgeTelevision tv = new NonBridgeTelevision();
NonBridgeRemoteControlForTelevision tvRemote = new NonBridgeRemoteControlForTelevision(tv);
tvRemote.PowerButton();
tvRemote.VolumeUp();
Console.WriteLine();
NonBridgeRadio radio = new NonBridgeRadio();
NonBridgeRemoteControlForRadio radioRemote = new NonBridgeRemoteControlForRadio(radio);
radioRemote.PowerButton();
radioRemote.Mute();
While this approach might seem simpler at first, it violates several SOLID principles, including the Open-Closed Principle (OCP), Single Responsibility Principle (SRP), and Dependency Inversion Principle (DIP). It also becomes harder to maintain and scale.
Performance Considerations
Although the non-bridge approach involves less code initially, it performs worse in terms of scalability and maintainability. While the performance difference might be negligible in small applications, it becomes significant as the system grows.
Conclusions
The Bridge Pattern is a powerful tool for decoupling abstractions from their implementations. It promotes extensibility, maintainability, and adherence to SOLID principles. While it may require a bit more upfront effort, the long-term benefits far outweigh the initial investment.
You can find the source code for this example here.
I hope this article has been helpful! Until next time, happy coding!
Top comments (0)