Understanding and Implementing the Bridge Pattern in C#

Understanding and Implementing the Bridge Pattern in C#

In this article, we will discuss the Bridge Pattern in software design. At first glance, the term "Bridge Pattern" might suggest that it's about creating a literal bridge between two different implementations. However, this misconception is rooted in the Adapter Pattern, which is quite different. The Bridge Pattern, in contrast, is designed to allow both abstraction and implementation to evolve independently.

Background of the Bridge Pattern

The Gang of Four (GoF) defines the Bridge Pattern as: “Decouple an abstraction from its implementation so that the two can vary independently.”

A classic use case of the Bridge Pattern is in applications where different implementations (like plugins or drivers) need to be swapped in and out without affecting the abstraction. Consider a scenario where a game, like Half-Life, provides a configuration screen to select a video mode—OpenGL, Direct3D, or software mode. In this case, the game’s abstraction (the configuration screen) remains unchanged, while the underlying implementation (the video rendering method) varies depending on the user’s selection.

Key Components of the Bridge Pattern

Let’s break down the components of the Bridge Pattern, which can be represented in a class diagram:


Article content

  1. Implementor: This interface defines the basic operations that all implementations must provide.
  2. ConcreteImplementor: These are the actual classes that implement the Implementor interface. These classes provide the specific behavior for each type of plugin or driver.
  3. Abstraction: This class defines the high-level interface that clients use. It delegates calls to the Implementor, but the client remains unaware of the actual implementation.
  4. RefinedAbstraction: This class extends Abstraction and adds functionality. It is responsible for selecting the appropriate ConcreteImplementor.

Example: Smart TV with Bridge Pattern

Let’s consider a Smart TV application, where a user can choose from various video sources like local cable TV, satellite TV, or IPTV. The abstraction for getting a TV guide and playing video remains the same, but the implementation varies depending on the chosen source.

We begin by defining the Implementor:

interface IVideoSource
{
    string GetTvGuide();
    string PlayVideo();
}        

Next, we define the ConcreteImplementors for each video source:

class LocalCableTv : IVideoSource
{
    const string SOURCE_NAME = "Local Cable TV";
    public string GetTvGuide() => $"Getting TV guide from - {SOURCE_NAME}";
    public string PlayVideo() => $"Playing - {SOURCE_NAME}";
}

class LocalDishTv : IVideoSource
{
    const string SOURCE_NAME = "Local DISH TV";
    public string GetTvGuide() => $"Getting TV guide from - {SOURCE_NAME}";
    public string PlayVideo() => $"Playing - {SOURCE_NAME}";
}

class IPTvService : IVideoSource
{
    const string SOURCE_NAME = "IP TV";
    public string GetTvGuide() => $"Getting TV guide from - {SOURCE_NAME}";
    public string PlayVideo() => $"Playing - {SOURCE_NAME}";
}        

Now, we define the Abstraction and RefinedAbstraction as a Smart TV class:

class MySuperSmartTV
{
    private IVideoSource currentVideoSource;

    public IVideoSource VideoSource
    {
        get => currentVideoSource;
        set => currentVideoSource = value;
    }

    public void ShowTvGuide()
    {
        Console.WriteLine(currentVideoSource?.GetTvGuide() ?? "Please select a Video Source to get TV guide.");
    }

    public void PlayTV()
    {
        Console.WriteLine(currentVideoSource?.PlayVideo() ?? "Please select a Video Source to play.");
    }
}        

Finally, we implement a controller that allows the user to select a TV source:

class SuperSmartTvController
{
    static void Main(string[] args)
    {
        MySuperSmartTV myTv = new MySuperSmartTV();
        Console.WriteLine("Select a source to get TV Guide and Play");
        Console.WriteLine("1. Local Cable TV\n2. Local Dish TV\n3. IP TV");

        var input = Console.ReadKey();

        switch (input.KeyChar)
        {
            case '1': myTv.VideoSource = new LocalCableTv(); break;
            case '2': myTv.VideoSource = new LocalDishTv(); break;
            case '3': myTv.VideoSource = new IPTvService(); break;
        }

        Console.WriteLine();
        myTv.ShowTvGuide();
        myTv.PlayTV();
    }
}        

With this setup, the user can select a TV source, and the selected source will display its own implementation for getting the TV guide and playing the video. The abstraction (the Smart TV) remains unchanged, allowing for flexibility and easy expansion.


Article content

Class Diagram

The implementation follows the class diagram of the Bridge Pattern, where:

Article content

  • The Abstraction class (MySuperSmartTV) uses the Implementor interface (IVideoSource).
  • The ConcreteImplementors (LocalCableTv, LocalDishTv, IPTvService) provide different implementations.

Conclusion

By separating the abstraction from its implementation, the Bridge Pattern provides a way to vary and extend both independently. It’s particularly useful when you have multiple implementations of a feature, such as plugins or drivers, that need to be interchangeable without affecting the client code.

In summary, the Bridge Pattern decouples interfaces from implementations, allowing you to easily switch or extend them without modifying the high-level abstraction. It’s an excellent choice for situations where flexibility and maintainability are key.

#BridgePattern #CSharp #DesignPatterns #SoftwareArchitecture

To view or add a comment, sign in

More articles by Rahul Rajat Singh

Others also viewed

Explore content categories