7.1 KiB
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/interfacekeywords. 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 anIMessageSender. - 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/DbCommandas abstraction, per-provider drivers as implementors. ILoggeras an abstraction over pluggable logging providers.- Any
HttpMessageHandlerswapped intoHttpClient.
Related patterns
- 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