Bridge Design Pattern in C# Implementation Guide

The Bridge design pattern is a structural pattern used to decouple an abstraction from its implementation so that both can evolve independently. Instead of relying on deep inheritance hierarchies that multiply exponentially with each new variant, this pattern uses composition by placing the implementation details behind a dedicated interface. This guide demonstrates how the Bridge pattern works in C#, breaks down its core components, and walks through a clear, practical code implementation.

Key Components of the Bridge Pattern

The pattern splits a system into two separate layers:

  1. Abstraction: Defines the high-level control logic and maintains a reference to an object of the Implementor type.
  2. Refined Abstraction: Extends the Abstraction to provide more specialized variants of high-level control.
  3. Implementor (Bridge Interface): Defines the interface for all implementation classes. It typically declares primitive operations used by the Abstraction.
  4. Concrete Implementors: Provide the concrete platform- or detail-specific code implementing the Implementor interface.

Step-by-Step C# Implementation

Consider a scenario where you control different electronic devices (such as a TV or a Radio) using different types of remote controls (a basic remote and an advanced remote).

1. Define the Implementor Interface

The implementor interface outlines the low-level operations that every concrete platform or device must support.

public interface IDevice
{
    bool IsEnabled { get; }
    void Enable();
    void Disable();
    int Volume { get; set; }
}

2. Create Concrete Implementors

These classes provide the actual implementation details for specific devices.

public class Tv : IDevice
{
    public bool IsEnabled { get; private set; }
    public int Volume { get; set; } = 10;

    public void Enable()
    {
        IsEnabled = true;
        Console.WriteLine("TV is now turned ON.");
    }

    public void Disable()
    {
        IsEnabled = false;
        Console.WriteLine("TV is now turned OFF.");
    }
}

public class Radio : IDevice
{
    public bool IsEnabled { get; private set; }
    public int Volume { get; set; } = 5;

    public void Enable()
    {
        IsEnabled = true;
        Console.WriteLine("Radio is now turned ON.");
    }

    public void Disable()
    {
        IsEnabled = false;
        Console.WriteLine("Radio is now turned OFF.");
    }
}

3. Define the Abstraction

The abstraction maintains a reference to the implementor and provides high-level operations.

public class RemoteControl
{
    protected readonly IDevice _device;

    public RemoteControl(IDevice device)
    {
        _device = device ?? throw new ArgumentNullException(nameof(device));
    }

    public void TogglePower()
    {
        if (_device.IsEnabled)
        {
            _device.Disable();
        }
        else
        {
            _device.Enable();
        }
    }

    public virtual void VolumeUp()
    {
        _device.Volume++;
        Console.WriteLine($"Volume increased to: {_device.Volume}");
    }

    public virtual void VolumeDown()
    {
        _device.Volume--;
        Console.WriteLine($"Volume decreased to: {_device.Volume}");
    }
}

4. Create Refined Abstractions

Refined abstractions extend the basic control logic without changing the device implementations.

public class AdvancedRemoteControl : RemoteControl
{
    public AdvancedRemoteControl(IDevice device) : base(device)
    {
    }

    public void Mute()
    {
        _device.Volume = 0;
        Console.WriteLine("Device has been muted.");
    }
}

5. Client Usage

The client code assembles the abstraction and implementation dynamically at runtime.

class Program
{
    static void Main()
    {
        // Control a TV using a basic remote
        IDevice tv = new Tv();
        RemoteControl basicRemote = new RemoteControl(tv);

        basicRemote.TogglePower();
        basicRemote.VolumeUp();
        basicRemote.TogglePower();

        Console.WriteLine();

        // Control a Radio using an advanced remote
        IDevice radio = new Radio();
        AdvancedRemoteControl advancedRemote = new AdvancedRemoteControl(radio);

        advancedRemote.TogglePower();
        advancedRemote.Mute();
        advancedRemote.VolumeUp();
        advancedRemote.TogglePower();
    }
}

When to Use the Bridge Pattern

  • Avoid Combinatorial Explosions: If you have N abstractions and M implementations, standard inheritance leads to N * M classes. The Bridge pattern reduces this to N + M classes.
  • Runtime Binding: When you need to switch an implementation dynamically at runtime.
  • Cross-Platform Architecture: When abstract logic must run across different platforms (e.g., Windows vs. Linux file operations, or SQL vs. NoSQL data access layers).