206 lines
7.1 KiB
Markdown
206 lines
7.1 KiB
Markdown
[← 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)
|