DEV Community

Cover image for Design Patterns #4: Unlocking Flexibility with the Bridge Pattern.
Serhii Korol
Serhii Korol

Posted on

Design Patterns #4: Unlocking Flexibility with the Bridge Pattern.

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.

bridge

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);
}
Enter fullscreen mode Exit fullscreen mode

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}");
}

Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

The output will be consistent since the methods are similar to those in the base class.

bridge result

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

The updated output will reflect the new functionality:

new bridge result

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}");
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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.

benchmark

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!

Buy Me A Beer

Top comments (0)