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:
- Abstraction: Defines the high-level control logic and maintains a reference to an object of the Implementor type.
- Refined Abstraction: Extends the Abstraction to provide more specialized variants of high-level control.
- Implementor (Bridge Interface): Defines the interface for all implementation classes. It typically declares primitive operations used by the Abstraction.
- 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
Nabstractions andMimplementations, standard inheritance leads toN * Mclasses. The Bridge pattern reduces this toN + Mclasses. - 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).