Skip to content Skip to footer

Bridge Design Pattern: Decoupling Abstraction from Implementation

You’re building a drawing application that needs to support multiple shapes (circles, rectangles) and multiple rendering engines (vector, raster). Without careful design, you’ll end up with a combinatorial explosion: CircleVector, CircleRaster, RectangleVector, RectangleRaster, and so on. The Bridge pattern prevents this nightmare by separating abstraction from implementation.

Problem

Consider a notification system that sends messages via different channels:

public abstract class Notification
{
    public abstract void Send(string message);
}

public class EmailNotification : Notification
{
    public override void Send(string message)
    {
        Console.WriteLine($"Sending email: {message}");
    }
}

public class SmsNotification : Notification
{
    public override void Send(string message)
    {
        Console.WriteLine($"Sending SMS: {message}");
    }
}

Now you need to add priority levels (urgent, normal). Without the Bridge pattern, you’d create:

  • UrgentEmailNotification
  • NormalEmailNotification
  • UrgentSmsNotification
  • NormalSmsNotification

Adding another channel (Push) and another priority (Low) creates 9 classes. This exponential growth makes the codebase unmaintainable.

Solution

The Bridge pattern decouples an abstraction from its implementation so that both can vary independently. It splits a large class or closely related classes into two separate hierarchies: abstraction and implementation.

Definition: The Bridge pattern lets you split a large class or a set of closely related classes into two separate hierarchies—abstraction and implementation—which can be developed independently of each other.

Instead of creating classes for every combination, you create:

  • Abstraction hierarchy: Defines the high-level control logic
  • Implementation hierarchy: Defines the platform-specific details

Pseudocode

Abstraction (Notification)
    ↓ uses
Implementation Interface (IMessageSender)
    ↓ implemented by
Concrete Implementations (EmailSender, SmsSender, PushSender)

Examples

Here’s a complete, runnable C# implementation:

using System;

// Implementation interface
public interface IMessageSender
{
    void SendMessage(string message);
}

// Concrete implementations
public class EmailSender : IMessageSender
{
    public void SendMessage(string message)
    {
        Console.WriteLine($"Email: {message}");
    }
}

public class SmsSender : IMessageSender
{
    public void SendMessage(string message)
    {
        Console.WriteLine($"SMS: {message}");
    }
}

public class PushSender : IMessageSender
{
    public void SendMessage(string message)
    {
        Console.WriteLine($"Push Notification: {message}");
    }
}

// Abstraction
public abstract class Notification
{
    protected IMessageSender _sender;

    protected Notification(IMessageSender sender)
    {
        _sender = sender;
    }

    public abstract void Send(string message);
}

// Refined abstractions
public class UrgentNotification : Notification
{
    public UrgentNotification(IMessageSender sender) : base(sender) { }

    public override void Send(string message)
    {
        var urgentMessage = $"[URGENT] {message}";
        _sender.SendMessage(urgentMessage);
    }
}

public class NormalNotification : Notification
{
    public NormalNotification(IMessageSender sender) : base(sender) { }

    public override void Send(string message)
    {
        _sender.SendMessage(message);
    }
}

// Usage
class Program
{
    static void Main()
    {
        // Combine abstractions with implementations at runtime
        var urgentEmail = new UrgentNotification(new EmailSender());
        urgentEmail.Send("Server is down!");

        var normalSms = new NormalNotification(new SmsSender());
        normalSms.Send("Your order has shipped");

        var urgentPush = new UrgentNotification(new PushSender());
        urgentPush.Send("Payment failed");
    }
}

Output:

Email: [URGENT] Server is down!
SMS: Your order has shipped
Push Notification: [URGENT] Payment failed

Real-World Example: Remote Control and Devices

// Implementation interface
public interface IDevice
{
    void TurnOn();
    void TurnOff();
    void SetVolume(int volume);
    int GetVolume();
}

// Concrete devices
public class TV : IDevice
{
    private int _volume = 50;
    private bool _isOn = false;

    public void TurnOn()
    {
        _isOn = true;
        Console.WriteLine("TV is ON");
    }

    public void TurnOff()
    {
        _isOn = false;
        Console.WriteLine("TV is OFF");
    }

    public void SetVolume(int volume)
    {
        _volume = Math.Clamp(volume, 0, 100);
        Console.WriteLine($"TV volume set to {_volume}");
    }

    public int GetVolume() => _volume;
}

public class Radio : IDevice
{
    private int _volume = 30;
    private bool _isOn = false;

    public void TurnOn()
    {
        _isOn = true;
        Console.WriteLine("Radio is ON");
    }

    public void TurnOff()
    {
        _isOn = false;
        Console.WriteLine("Radio is OFF");
    }

    public void SetVolume(int volume)
    {
        _volume = Math.Clamp(volume, 0, 50);
        Console.WriteLine($"Radio volume set to {_volume}");
    }

    public int GetVolume() => _volume;
}

// Abstraction
public abstract class RemoteControl
{
    protected IDevice _device;

    protected RemoteControl(IDevice device)
    {
        _device = device;
    }

    public void TogglePower()
    {
        if (_device.GetVolume() > 0)
            _device.TurnOn();
        else
            _device.TurnOff();
    }

    public void VolumeUp()
    {
        _device.SetVolume(_device.GetVolume() + 10);
    }

    public void VolumeDown()
    {
        _device.SetVolume(_device.GetVolume() - 10);
    }
}

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

    public void Mute()
    {
        _device.SetVolume(0);
        Console.WriteLine("Device muted");
    }
}

// Usage
var tv = new TV();
var basicRemote = new RemoteControl(tv);
basicRemote.TogglePower();
basicRemote.VolumeUp();

var radio = new Radio();
var advancedRemote = new AdvancedRemoteControl(radio);
advancedRemote.TogglePower();
advancedRemote.Mute();

Applications

Enterprise Use Cases:

  1. Cross-Platform UI Frameworks: Separating UI abstractions (buttons, windows) from platform-specific implementations (Windows, macOS, Linux)
  2. Database Drivers: Abstracting database operations from specific database implementations (SQL Server, PostgreSQL, MySQL)
  3. Payment Processing: Separating payment logic from payment gateway implementations (Stripe, PayPal, Square)
  4. Cloud Storage: Abstracting file operations from cloud provider implementations (AWS S3, Azure Blob, Google Cloud Storage)
  5. Messaging Systems: Separating messaging abstractions from transport implementations (HTTP, WebSocket, gRPC)
  6. Rendering Engines: Separating graphics abstractions from rendering implementations (OpenGL, DirectX, Vulkan)

Benefits:

  • Prevents exponential class growth
  • Allows runtime binding of abstraction and implementation
  • Hides implementation details from clients
  • Follows the Open/Closed Principle—open for extension, closed for modification
  • Enables platform-independent code

When to Use:

  • You want to avoid permanent binding between abstraction and implementation
  • Abstractions and implementations should be extensible independently
  • Changes to implementation shouldn’t affect clients
  • You want to hide implementation details from clients
  • You have a proliferation of classes due to combinations of abstractions and implementations

The Bridge pattern is crucial for building flexible systems where you need to vary both the abstraction and its implementation independently, preventing the combinatorial explosion problem.

Leave a Comment