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

206 lines
7.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
[← Design patterns index](README.md)
# 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.
```mermaid
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
```csharp
// 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
}
}
```
```csharp
// 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`.
## 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](README.md) · **Next →** [Decorator](decorator.md)