Files
Design-Patterns/docs/bridge.md
2026-08-13 22:47:17 -04:00

7.1 KiB
Raw Permalink Blame History

← Design patterns index

Bridge

Category: Structural | Source: patterns/bridge/Bridge Example/

Problem

A Notification hierarchy (Alert, Reminder) needs a second, independent dimension — delivery channel (Email, SMS). Subclassing both at once yields EmailAlertNotification, SmsReminderNotification, … and the count grows geometrically: each new notification type needs one subclass per existing channel, and each new channel needs one per existing notification type.

Solution

Stop expressing both dimensions with inheritance. Keep one as a class hierarchy (the Abstraction), extract the other into its own hierarchy (the Implementation), and join them by composition — the abstraction holds a reference to an implementor and delegates the real work to it. Each hierarchy now grows independently, and the implementation is swappable at runtime.

The GoF words "Abstraction" and "Implementation" do not mean your language's abstract/interface keywords. They mean "the control-facing half" and "the platform-facing half" of a split. (Dive Into Design Patterns, p.165)

Participants (GoF names → this repo)

  • Abstraction → Notification — defines the client-facing interface, holds an IMessageSender.
  • RefinedAbstraction → AlertNotification, ReminderNotification — variants of control logic.
  • Implementor → IMessageSender — primitive operations; need not mirror the Abstraction's interface.
  • ConcreteImplementor → EmailSender, SmsSender — the channel-specific code.
classDiagram
    class Notification {
        <<abstract>>
        #IMessageSender _sender
        +Notify(message)*
    }
    class IMessageSender {
        <<interface>>
        +SendMessage(message)
    }
    Notification o--> IMessageSender : THE BRIDGE - delegates to
    Notification <|-- AlertNotification
    Notification <|-- ReminderNotification
    IMessageSender <|.. EmailSender
    IMessageSender <|.. SmsSender

The single o--> arrow is the bridge. Left of it, control logic grows freely; right of it, platforms grow freely; neither side multiplies the other.

Code

// IMessageSender.cs — Implementor
public interface IMessageSender
{
    void SendMessage(string message);
}

// EmailSender.cs — ConcreteImplementor
public class EmailSender : IMessageSender
{
    public void SendMessage(string message)
    {
        Console.WriteLine($"Email sent: {message}");
    }
}

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

// Notification.cs — Abstraction
public abstract class Notification
{
    protected readonly IMessageSender _sender;

    protected Notification(IMessageSender sender)
    {
        _sender = sender;    // the bridge is established here, at construction
    }

    public abstract void Notify(string message);
}

// AlertNotification.cs — RefinedAbstraction (ReminderNotification is identical but for its prefix)
public class AlertNotification : Notification
{
    public AlertNotification(IMessageSender sender) : base(sender) { }

    public override void Notify(string message)
    {
        _sender.SendMessage($"ALERT: {message}");   // high-level op built from the primitive
    }
}
// Program.cs — the client picks both halves independently, at runtime
Notification n1 = new AlertNotification(new EmailSender());
Notification n2 = new AlertNotification(new SmsSender());
Notification n3 = new ReminderNotification(new EmailSender());
Notification n4 = new ReminderNotification(new SmsSender());

n1.Notify("Server is down");
n2.Notify("Server is down");
n3.Notify("Meeting at 3pm");
n4.Notify("Meeting at 3pm");

The payoff: at no point is there an EmailAlertNotification class or an SmsReminderNotification class — the notification type and the delivery channel bind together at runtime, not compile time. 2 abstractions × 2 implementors = 4 usable combinations from 4 classes, not 4 combination classes. At 5 × 5 it would be 10 classes instead of 25.

Output (verified)

Email sent: ALERT: Server is down
SMS sent: ALERT: Server is down
Email sent: Reminder: Meeting at 3pm
SMS sent: Reminder: Meeting at 3pm

GoF pseudocode (remote controls × devices)

class RemoteControl is
  protected field device: Device
  constructor RemoteControl(device: Device) is this.device = device
  method togglePower() is
     if (device.isEnabled()) then device.disable() else device.enable()
  method volumeUp() is
     device.setVolume(device.getVolume() + 10)   // high-level op from primitives

class AdvancedRemoteControl extends RemoteControl is
  method mute() is device.setVolume(0)

interface Device is
  method isEnabled()  method enable()  method disable()
  method getVolume()  method setVolume(percent)

class Tv implements Device is // ...
class Radio implements Device is // ...

tv = new Tv()
remote = new RemoteControl(tv)
remote.togglePower()

Applicability

  • You want to avoid permanently binding an abstraction to one implementation — for example, when the implementation must be selected or switched at run-time.
  • Both abstraction and implementation should be extensible by subclassing, independently of each other.
  • Changes to an implementation must not force clients to recompile.
  • You already have a proliferation of combination classes (GoF calls this smell "nested generalizations").
  • You want to share one implementor among several abstraction objects, invisibly to the client.

Pros / Cons

Pros Cons
Platform-independent classes; implementor swappable at runtime Can over-complicate a class that was already small and cohesive
Client only ever sees the high-level abstraction
Open/Closed — add abstractions or implementors independently
Single Responsibility — control logic vs. platform detail live apart

Seen in the wild (.NET)

  • ADO.NET — DbConnection/DbCommand as abstraction, per-provider drivers as implementors.
  • ILogger as an abstraction over pluggable logging providers.
  • Any HttpMessageHandler swapped into HttpClient.
  • Adapter is retrofitted onto existing incompatible classes; Bridge is designed up front.
  • Bridge / State / Strategy share the "delegate to a composed object" shape but differ in intent — the name still communicates the problem being solved.
  • Abstract Factory pairs well when only certain abstraction/implementor combinations are legal.

All patterns · Next → Decorator