Loading learning content...
In the previous page, we dissected the painful reality of inheritance-based designs: M × N class explosion, tight coupling, fragile base classes, and compile-time rigidity. The conclusion was clear—inheritance fails when we have two or more independent dimensions of variation.
The Bridge Pattern offers an elegant escape: separate the abstraction from its implementation, placing them in distinct, independent hierarchies. Then connect them through composition—a "bridge"—that allows both sides to evolve without affecting the other.
This isn't merely a clever trick. It's a fundamental shift in how we model variation: from inheritance (is-a) to composition (has-a).
By the end of this page, you will understand the complete structure of the Bridge Pattern, including its key participants (Abstraction, Refined Abstraction, Implementor, Concrete Implementor) and how they collaborate. You'll see how composition replaces inheritance for cross-dimensional variation, understand why this structure enables independent evolution, and be able to recognize opportunities to apply the pattern in real-world designs.
The Bridge Pattern embodies one of object-oriented design's most important principles: favor composition over inheritance. While inheritance ties classes together at compile time in a rigid parent-child relationship, composition connects objects at runtime through flexible references.
The key insight:
Instead of creating WindowsCircle that inherits from both a shape hierarchy and a platform hierarchy (which languages like Java and TypeScript don't even allow), we:
Circle that contains a reference to a RendererWindowsRenderer that implements the Renderer interfaceNow Circle and WindowsRenderer are independent. Neither inherits from the other. They collaborate through a well-defined interface.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
// BEFORE: Inheritance-based approach (what we saw fails)// // Shape// ┌──────────┴──────────┐// Circle Rectangle// ┌──┴──┐ ┌──┴──┐// WinCircle LinCircle WinRect LinRect // AFTER: Composition-based Bridge Pattern//// Abstraction Hierarchy Implementation Hierarchy// Shape ────────────▶ Renderer (interface)// ┌──┴──┐ ┌──┴──┐// Circle Rectangle WindowsRenderer LinuxRenderer//// The arrow represents composition: Shape HAS-A Renderer// Both hierarchies can grow independently! // Implementation interface (the "Implementor")interface Renderer { renderCircle(centerX: number, centerY: number, radius: number): void; renderRectangle(x: number, y: number, width: number, height: number): void;} // Abstraction base classabstract class Shape { // The BRIDGE: composition instead of inheritance protected renderer: Renderer; constructor(renderer: Renderer) { this.renderer = renderer; } abstract draw(): void;} // Refined Abstractions (shape variants)class Circle extends Shape { constructor( private centerX: number, private centerY: number, private radius: number, renderer: Renderer ) { super(renderer); } draw(): void { // Delegates to the implementation this.renderer.renderCircle(this.centerX, this.centerY, this.radius); }} class Rectangle extends Shape { constructor( private x: number, private y: number, private width: number, private height: number, renderer: Renderer ) { super(renderer); } draw(): void { this.renderer.renderRectangle(this.x, this.y, this.width, this.height); }} // Concrete Implementations (platform variants)class WindowsRenderer implements Renderer { renderCircle(centerX: number, centerY: number, radius: number): void { console.log(`Windows GDI: Drawing circle at (${centerX}, ${centerY}) with radius ${radius}`); // Actual Windows GDI calls would go here } renderRectangle(x: number, y: number, width: number, height: number): void { console.log(`Windows GDI: Drawing rectangle at (${x}, ${y}) size ${width}x${height}`); }} class LinuxRenderer implements Renderer { renderCircle(centerX: number, centerY: number, radius: number): void { console.log(`Linux X11: Drawing circle at (${centerX}, ${centerY}) with radius ${radius}`); // Actual X11 calls would go here } renderRectangle(x: number, y: number, width: number, height: number): void { console.log(`Linux X11: Drawing rectangle at (${x}, ${y}) size ${width}x${height}`); }} // Usage: Any shape can use any rendererconst windowsRenderer = new WindowsRenderer();const linuxRenderer = new LinuxRenderer(); const circleOnWindows = new Circle(100, 100, 50, windowsRenderer);const circleOnLinux = new Circle(100, 100, 50, linuxRenderer);const rectangleOnWindows = new Rectangle(0, 0, 200, 100, windowsRenderer); circleOnWindows.draw(); // Uses Windows renderingcircleOnLinux.draw(); // Uses Linux renderingrectangleOnWindows.draw(); // Uses Windows renderingThe mathematics of composition:
With inheritance, M shapes × N platforms = M × N classes.
With the Bridge Pattern:
For 5 shapes and 4 platforms: 5 × 4 = 20 classes vs. 5 + 4 = 9 classes. As dimensions grow, the savings become dramatic.
| Abstraction Variants | Implementation Variants | Inheritance (M×N) | Bridge Pattern (M+N) | Savings |
|---|---|---|---|---|
| 3 | 2 | 6 | 5 | 1 class |
| 5 | 4 | 20 | 9 | 11 classes |
| 10 | 5 | 50 | 15 | 35 classes |
| 10 | 10 | 100 | 20 | 80 classes |
| 20 | 10 | 200 | 30 | 170 classes |
The Bridge Pattern defines four key participants, each with a distinct role in achieving the abstraction-implementation separation. Understanding these roles precisely is essential for correct application of the pattern.
Shape.Circle, Rectangle.Renderer.WindowsRenderer, LinuxRenderer.123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
/** * IMPLEMENTOR * * Defines the interface for implementation classes. * This interface doesn't have to correspond exactly to Abstraction's interface; * in fact, they're typically quite different. The Implementor provides * primitive operations, and Abstraction defines higher-level operations * built from those primitives. */interface Implementor { // Primitive operations that concrete implementors must provide operationImpl(): string;} /** * ABSTRACTION * * Defines the abstraction's interface. * Maintains a reference to an Implementor object. * Delegates work to the Implementor through the bridge. */abstract class Abstraction { protected implementor: Implementor; constructor(implementor: Implementor) { this.implementor = implementor; } // High-level operation that uses the implementor's primitives operation(): string { // The BRIDGE in action: delegating to the implementor return `Abstraction: Base operation with:${this.implementor.operationImpl()}`; }} /** * REFINED ABSTRACTION * * Extends the Abstraction to add additional behavior * or refine the abstraction in some way. There can be * multiple refined abstractions, forming an inheritance * hierarchy on the abstraction side. */class RefinedAbstraction extends Abstraction { operation(): string { // Can add behavior before delegating... const prefix = "RefinedAbstraction: Extended operation with:"; // ...still uses the implementor through the bridge... const implResult = this.implementor.operationImpl(); // ...and can add behavior after return prefix + implResult; } // Can also add entirely new operations additionalOperation(): string { return "RefinedAbstraction: Additional behavior unique to this refinement"; }} /** * CONCRETE IMPLEMENTOR A * * Implements the Implementor interface for a specific * platform, algorithm, or variant. */class ConcreteImplementorA implements Implementor { operationImpl(): string { return "ConcreteImplementorA: Result using strategy/platform A"; }} /** * CONCRETE IMPLEMENTOR B * * Another implementation with different characteristics. * Can be selected at runtime or configured externally. */class ConcreteImplementorB implements Implementor { operationImpl(): string { return "ConcreteImplementorB: Result using strategy/platform B"; }} // Client code: works with any combinationfunction clientCode(abstraction: Abstraction): void { console.log(abstraction.operation());} // Runtime composition: Abstraction A with Implementor Aconst implA = new ConcreteImplementorA();const abstractionWithA = new RefinedAbstraction(implA);clientCode(abstractionWithA); // Runtime composition: Same Abstraction with different Implementorconst implB = new ConcreteImplementorB();const abstractionWithB = new RefinedAbstraction(implB);clientCode(abstractionWithB);A critical design decision is the granularity of the Implementor interface. The Implementor should expose primitive operations—the fundamental building blocks. The Abstraction combines these primitives into high-level operations. If the Implementor interface mirrors the Abstraction too closely, you've likely missed opportunities for reuse and flexibility.
The term "Bridge" refers to the composition relationship that connects the Abstraction to the Implementor. This is the structural centerpiece of the pattern—the bridge that allows both sides to vary independently.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
// The Bridge is this reference:abstract class Shape { // ┌─────── THE BRIDGE ───────┐ // ▼ │ protected renderer: Renderer; // <──┘ constructor(renderer: Renderer) { this.renderer = renderer; }} // Why is this a "bridge"?// // 1. It CONNECTS two hierarchies that would otherwise be unrelated// - Shapes don't know about Windows/Linux directly// - Renderers don't know about Circles/Rectangles directly// - They connect ONLY through this reference//// 2. It can be CROSSED in either direction// - Start with a Shape → get its Renderer → perform rendering// - Some patterns: Start with Renderer → render any Shape passed in//// 3. It can be RECONFIGURED// - Same Shape, different Renderer (change bridge target)// - Different Shape, same Renderer (change bridge source)//// 4. It's a STABLE crossing point// - The Renderer interface is the contract// - As long as the contract holds, both sides can evolve // The bridge enables true independence:interface Renderer { renderCircle(cx: number, cy: number, r: number): void; renderRectangle(x: number, y: number, w: number, h: number): void;} // Shape hierarchy can grow without touching Rendererclass Triangle extends Shape { constructor(private vertices: [number, number][], renderer: Renderer) { super(renderer); } draw(): void { // Uses renderer primitives, but adds Triangle-specific logic // (Renderer might not have renderTriangle, so we compose from primitives) // This keeps Renderer interface stable }} // Renderer hierarchy can grow without touching Shapeclass WebGLRenderer implements Renderer { renderCircle(cx: number, cy: number, r: number): void { // WebGL-specific implementation console.log(`WebGL: Circle shader at (${cx},${cy}) radius ${r}`); } renderRectangle(x: number, y: number, w: number, h: number): void { // WebGL-specific implementation console.log(`WebGL: Rectangle mesh at (${x},${y}) size ${w}x${h}`); }} // Neither hierarchy changed when the other grew!const triangle = new Triangle([[0,0], [50,100], [100,0]], new WebGLRenderer());Three characteristics of the bridge:
Interface-Based — The Abstraction references the Implementor through an interface, not a concrete class. This is what enables substitutability.
Injected — The Implementor is injected into the Abstraction (typically via constructor), not created internally. This follows the Dependency Inversion Principle.
Stable — The interface defines a stable contract. Both hierarchies evolve independently as long as the contract holds.
Let's visualize the complete structure of the Bridge Pattern, showing how all participants relate:
┌─────────────────────────────────────────────────────────────────────────────┐│ BRIDGE PATTERN STRUCTURE │└─────────────────────────────────────────────────────────────────────────────┘ ABSTRACTION HIERARCHY IMPLEMENTATION HIERARCHY ────────────────────── ───────────────────────── ┌─────────────────────────┐ ┌─────────────────────────┐ │ <<abstract>> │ │ <<interface>> │ │ Abstraction │ │ Implementor │ ├─────────────────────────┤ ├─────────────────────────┤ │ - implementor: Impl │─────────────▶│ │ ├─────────────────────────┤ bridge ├─────────────────────────┤ │ + operation() │ │ + operationImpl() │ └────────────┬────────────┘ └────────────┬────────────┘ │ │ │ extends │ implements │ │ ┌────────────┴────────────┐ ┌────────────┴────────────┐ │ │ │ │ ▼ ▼ ▼ ▼┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐│ RefinedAbst │ │ RefinedAbst │ │ ConcreteImpl│ │ ConcreteImpl││ ractionA │ │ ractionB │ │ ementorA │ │ ementorB │├─────────────┤ ├─────────────┤ ├─────────────┤ ├─────────────┤│ │ │ │ │ │ │ │├─────────────┤ ├─────────────┤ ├─────────────┤ ├─────────────┤│ +operation()│ │ +operation()│ │+operationIm │ │+operationIm ││ +extraOp() │ │ +anotherOp()│ │ pl() │ │ pl() │└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ COLLABORATION EXAMPLE Client Abstraction Implementor │ │ │ │ operation() │ │ │───────────────────────────▶│ │ │ │ operationImpl() │ │ │───────────────────────────▶│ │ │ │ │ │ ┌──────────────────────┐ │ │ │ │ Implementation does │ │ │ │ │ low-level work │ │ │ │ └──────────────────────┘ │ │ │ │ │ │◀───────────────────────────│ │ │ │ │ │ ┌──────────────────────┐ │ │ │ │ Abstraction adds │ │ │ │ │ high-level behavior │ │ │ │ └──────────────────────┘ │ │ │ │ │◀───────────────────────────│ │ │ │ │Key structural observations:
Two parallel hierarchies — The left side (Abstraction) and right side (Implementor) are completely separate inheritance trees.
Bridge at the top — The composition relationship is between the abstract types, not the concrete types. This ensures all combinations inherit the connection.
Asymmetric relationship — Abstraction knows about Implementor, but Implementor doesn't know about Abstraction. This is intentional—it keeps Implementors reusable.
Any-to-any composition — Any Refined Abstraction can work with any Concrete Implementor, giving M × N combinations without M × N classes.
Let's implement a complete, production-ready example: a notification system that supports multiple message types and multiple delivery platforms.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
// ═══════════════════════════════════════════════════════════════════════════// IMPLEMENTOR: Defines the interface for message delivery platforms// ═══════════════════════════════════════════════════════════════════════════ interface MessageSender { // Primitive operations that all platforms must support sendText(recipient: string, text: string): Promise<SendResult>; sendRichContent(recipient: string, content: RichContent): Promise<SendResult>; supportsRichContent(): boolean; getDeliveryStatus(messageId: string): Promise<DeliveryStatus>;} interface SendResult { success: boolean; messageId: string; timestamp: Date; error?: string;} interface RichContent { html?: string; attachments?: Attachment[]; buttons?: ActionButton[];} interface Attachment { type: 'image' | 'document' | 'video'; url: string; name: string;} interface ActionButton { label: string; url: string;} type DeliveryStatus = 'pending' | 'sent' | 'delivered' | 'failed' | 'read'; // ═══════════════════════════════════════════════════════════════════════════// CONCRETE IMPLEMENTORS: Platform-specific message delivery// ═══════════════════════════════════════════════════════════════════════════ class EmailSender implements MessageSender { constructor( private smtpHost: string, private smtpPort: number, private credentials: { user: string; pass: string } ) {} async sendText(recipient: string, text: string): Promise<SendResult> { console.log(`[Email] Sending plain text to ${recipient} via ${this.smtpHost}`); // Actual SMTP implementation would go here return { success: true, messageId: `email-${Date.now()}`, timestamp: new Date() }; } async sendRichContent(recipient: string, content: RichContent): Promise<SendResult> { console.log(`[Email] Sending rich HTML email to ${recipient}`); if (content.html) { console.log(` HTML content: ${content.html.substring(0, 50)}...`); } if (content.attachments) { content.attachments.forEach(att => { console.log(` Attachment: ${att.name} (${att.type})`); }); } return { success: true, messageId: `email-rich-${Date.now()}`, timestamp: new Date() }; } supportsRichContent(): boolean { return true; // Email supports HTML, attachments, etc. } async getDeliveryStatus(messageId: string): Promise<DeliveryStatus> { // Email delivery is typically fire-and-forget return 'sent'; }} class SmsSender implements MessageSender { constructor( private twilioAccountSid: string, private twilioAuthToken: string, private fromNumber: string ) {} async sendText(recipient: string, text: string): Promise<SendResult> { // SMS has length limits const truncatedText = text.length > 160 ? text.substring(0, 157) + '...' : text; console.log(`[SMS] Sending to ${recipient}: "${truncatedText}"`); return { success: true, messageId: `sms-${Date.now()}`, timestamp: new Date() }; } async sendRichContent(recipient: string, content: RichContent): Promise<SendResult> { // SMS doesn't support rich content - fall back to plain text console.log(`[SMS] Rich content not supported, sending link instead`); const fallbackText = 'View content at: https://app.example.com/view'; return this.sendText(recipient, fallbackText); } supportsRichContent(): boolean { return false; // SMS is text-only } async getDeliveryStatus(messageId: string): Promise<DeliveryStatus> { // Twilio provides delivery receipts return 'delivered'; }} class PushNotificationSender implements MessageSender { constructor( private firebaseProjectId: string, private serverKey: string ) {} async sendText(recipient: string, text: string): Promise<SendResult> { console.log(`[Push] Sending notification to device ${recipient}`); return { success: true, messageId: `push-${Date.now()}`, timestamp: new Date() }; } async sendRichContent(recipient: string, content: RichContent): Promise<SendResult> { console.log(`[Push] Sending rich notification with ${content.buttons?.length || 0} actions`); return { success: true, messageId: `push-rich-${Date.now()}`, timestamp: new Date() }; } supportsRichContent(): boolean { return true; // Push supports images, action buttons } async getDeliveryStatus(messageId: string): Promise<DeliveryStatus> { return 'delivered'; }} // ═══════════════════════════════════════════════════════════════════════════// ABSTRACTION: Defines high-level notification operations// ═══════════════════════════════════════════════════════════════════════════ abstract class Notification { // THE BRIDGE protected sender: MessageSender; protected recipient: string; constructor(recipient: string, sender: MessageSender) { this.recipient = recipient; this.sender = sender; } // Template for sending - subclasses define content async send(): Promise<SendResult> { const content = this.buildContent(); // Abstraction decides how to use implementor based on capabilities if (this.requiresRichContent() && this.sender.supportsRichContent()) { return this.sender.sendRichContent(this.recipient, content); } else { const plainText = this.getPlainTextFallback(); return this.sender.sendText(this.recipient, plainText); } } abstract buildContent(): RichContent; abstract getPlainTextFallback(): string; abstract requiresRichContent(): boolean;} // ═══════════════════════════════════════════════════════════════════════════// REFINED ABSTRACTIONS: Specific notification types// ═══════════════════════════════════════════════════════════════════════════ class WelcomeNotification extends Notification { constructor( recipient: string, sender: MessageSender, private userName: string ) { super(recipient, sender); } buildContent(): RichContent { return { html: ` <h1>Welcome, ${this.userName}!</h1> <p>We're thrilled to have you join our platform.</p> <p>Get started by exploring our features.</p> `, buttons: [ { label: 'Get Started', url: 'https://app.example.com/onboarding' }, { label: 'Learn More', url: 'https://app.example.com/docs' } ] }; } getPlainTextFallback(): string { return `Welcome, ${this.userName}! Get started at https://app.example.com/onboarding`; } requiresRichContent(): boolean { return true; // We prefer rich welcome emails }} class OrderConfirmationNotification extends Notification { constructor( recipient: string, sender: MessageSender, private orderId: string, private total: number, private items: { name: string; quantity: number }[] ) { super(recipient, sender); } buildContent(): RichContent { const itemList = this.items .map(item => `<li>${item.name} (x${item.quantity})</li>`) .join(''); return { html: ` <h1>Order Confirmed! #${this.orderId}</h1> <h2>Order Total: $${this.total.toFixed(2)}</h2> <ul>${itemList}</ul> `, buttons: [ { label: 'Track Order', url: `https://app.example.com/orders/${this.orderId}` } ] }; } getPlainTextFallback(): string { return `Order #${this.orderId} confirmed! Total: $${this.total.toFixed(2)}. Track at: https://app.example.com/orders/${this.orderId}`; } requiresRichContent(): boolean { return true; }} class SecurityAlertNotification extends Notification { constructor( recipient: string, sender: MessageSender, private alertType: string, private ipAddress: string, private location: string ) { super(recipient, sender); } buildContent(): RichContent { return { html: ` <h1>⚠️ Security Alert</h1> <p><strong>Type:</strong> ${this.alertType}</p> <p><strong>IP:</strong> ${this.ipAddress}</p> <p><strong>Location:</strong> ${this.location}</p> <p>If this wasn't you, please secure your account immediately.</p> `, buttons: [ { label: 'Secure Account', url: 'https://app.example.com/security' }, { label: 'This Was Me', url: 'https://app.example.com/security/dismiss' } ] }; } getPlainTextFallback(): string { return `SECURITY ALERT: ${this.alertType} from ${this.ipAddress} (${this.location}). If this wasn't you, visit: https://app.example.com/security`; } requiresRichContent(): boolean { return false; // Security alerts should work on any platform }} // ═══════════════════════════════════════════════════════════════════════════// USAGE: Any notification type × Any sender platform// ═══════════════════════════════════════════════════════════════════════════ async function demonstrateBridge(): Promise<void> { // Create senders (Concrete Implementors) const emailSender = new EmailSender('smtp.example.com', 587, { user: 'api', pass: 'key' }); const smsSender = new SmsSender('AC123', 'token', '+15551234567'); const pushSender = new PushNotificationSender('my-project', 'server-key'); // Same notification type, different senders const welcomeViaEmail = new WelcomeNotification('user@example.com', emailSender, 'Alice'); const welcomeViaSms = new WelcomeNotification('+15559876543', smsSender, 'Alice'); const welcomeViaPush = new WelcomeNotification('device-token-123', pushSender, 'Alice'); // Same sender, different notification types const orderViaEmail = new OrderConfirmationNotification( 'user@example.com', emailSender, 'ORD-001', 99.99, [{ name: 'Widget', quantity: 2 }] ); const securityViaSms = new SecurityAlertNotification( '+15559876543', smsSender, 'New Login', '192.168.1.1', 'New York, USA' ); // All combinations work! await welcomeViaEmail.send(); await welcomeViaSms.send(); // Falls back to plain text await welcomeViaPush.send(); await orderViaEmail.send(); await securityViaSms.send();} demonstrateBridge();The Abstraction (Notification) intelligently handles platform differences by checking supportsRichContent() and falling back appropriately. This is a key benefit of the Bridge—the abstraction can adapt to implementor capabilities without tight coupling to any specific implementor.
The true power of the Bridge Pattern emerges when requirements change. Let's see how both hierarchies evolve independently.
1234567891011121314151617181920212223242526272829303132333435363738394041424344
// NEW REQUIREMENT: Add password reset notification// We ONLY add a new Refined Abstraction - no changes to senders! class PasswordResetNotification extends Notification { constructor( recipient: string, sender: MessageSender, private resetToken: string, private expiryMinutes: number ) { super(recipient, sender); } buildContent(): RichContent { return { html: ` <h1>Password Reset Request</h1> <p>Click the button below to reset your password.</p> <p>This link expires in ${this.expiryMinutes} minutes.</p> `, buttons: [ { label: 'Reset Password', url: `https://app.example.com/reset/${this.resetToken}` } ] }; } getPlainTextFallback(): string { return `Reset your password: https://app.example.com/reset/${this.resetToken} (expires in ${this.expiryMinutes} min)`; } requiresRichContent(): boolean { return false; // Password reset must work on all platforms }} // Immediately works with ALL existing senders:const resetViaEmail = new PasswordResetNotification('user@example.com', emailSender, 'tok123', 15);const resetViaSms = new PasswordResetNotification('+15559876543', smsSender, 'tok123', 15);const resetViaPush = new PasswordResetNotification('device-token', pushSender, 'tok123', 15); // Zero changes to EmailSender, SmsSender, PushNotificationSender!12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
// NEW REQUIREMENT: Add Slack notification support// We ONLY add a new Concrete Implementor - no changes to notifications! class SlackSender implements MessageSender { constructor( private webhookUrl: string, private defaultChannel: string ) {} async sendText(recipient: string, text: string): Promise<SendResult> { console.log(`[Slack] Posting to channel ${recipient}: "${text}"`); // POST to Slack webhook return { success: true, messageId: `slack-${Date.now()}`, timestamp: new Date() }; } async sendRichContent(recipient: string, content: RichContent): Promise<SendResult> { console.log(`[Slack] Posting rich block kit message to ${recipient}`); // Convert RichContent to Slack Block Kit format const blocks = this.convertToBlockKit(content); return { success: true, messageId: `slack-rich-${Date.now()}`, timestamp: new Date() }; } private convertToBlockKit(content: RichContent): any[] { // Convert our generic format to Slack-specific format const blocks: any[] = []; if (content.html) { blocks.push({ type: 'section', text: { type: 'mrkdwn', text: content.html } }); } if (content.buttons) { blocks.push({ type: 'actions', elements: content.buttons.map(btn => ({ type: 'button', text: { type: 'plain_text', text: btn.label }, url: btn.url })) }); } return blocks; } supportsRichContent(): boolean { return true; // Slack has excellent rich content support } async getDeliveryStatus(messageId: string): Promise<DeliveryStatus> { return 'delivered'; }} // Immediately works with ALL existing notification types:const slackSender = new SlackSender('https://hooks.slack.com/abc', '#general'); const welcomeViaSlack = new WelcomeNotification('#new-users', slackSender, 'Bob');const orderViaSlack = new OrderConfirmationNotification('#orders', slackSender, 'ORD-002', 149.99, []);const securityViaSlack = new SecurityAlertNotification('#security', slackSender, 'Login', '10.0.0.1', 'SF');const resetViaSlack = new PasswordResetNotification('#support', slackSender, 'tok456', 15); // Zero changes to WelcomeNotification, OrderConfirmation, etc.!This is the Open-Closed Principle exemplified: both hierarchies are open for extension (add new notification types, add new senders) but closed for modification (existing code unchanged). The Bridge Pattern is a structural realization of this SOLID principle.
Unlike inheritance-based designs where the abstraction-implementation binding is fixed at compile time, the Bridge Pattern enables runtime composition. This opens powerful possibilities.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
// Scenario 1: User Preferences// User chooses their preferred notification channel interface UserPreferences { preferredChannel: 'email' | 'sms' | 'push' | 'slack'; email?: string; phone?: string; slackId?: string; deviceToken?: string;} class NotificationService { private senders: Map<string, MessageSender>; constructor() { this.senders = new Map(); this.senders.set('email', new EmailSender('smtp.example.com', 587, { user: '', pass: '' })); this.senders.set('sms', new SmsSender('AC123', 'token', '+15551234567')); this.senders.set('push', new PushNotificationSender('proj', 'key')); this.senders.set('slack', new SlackSender('webhook', '#default')); } async sendWelcome(user: UserPreferences): Promise<void> { // RUNTIME selection based on user preference const sender = this.senders.get(user.preferredChannel); const recipient = this.getRecipientAddress(user); if (!sender || !recipient) { throw new Error('Invalid notification configuration'); } const notification = new WelcomeNotification(recipient, sender, 'User'); await notification.send(); } private getRecipientAddress(user: UserPreferences): string | undefined { switch (user.preferredChannel) { case 'email': return user.email; case 'sms': return user.phone; case 'push': return user.deviceToken; case 'slack': return user.slackId; } }} // Scenario 2: Fallback Chain// Try primary channel, fall back to secondary if it fails class RobustNotificationService { private senders: MessageSender[]; constructor(senders: MessageSender[]) { this.senders = senders; // Ordered by preference } async send(notification: Notification, recipients: string[]): Promise<SendResult> { for (let i = 0; i < this.senders.length; i++) { const sender = this.senders[i]; try { // Create notification with current sender const notificationWithSender = this.withSender(notification, sender, recipients[i]); const result = await notificationWithSender.send(); if (result.success) { console.log(`Sent via channel ${i + 1}`); return result; } } catch (error) { console.log(`Channel ${i + 1} failed, trying next...`); } } throw new Error('All notification channels failed'); } private withSender(notification: Notification, sender: MessageSender, recipient: string): Notification { // Clone notification with new sender - implementation depends on notification type // This is a simplified example return notification; }} // Scenario 3: A/B Testing Implementation Strategies class ABTestingNotificationService { private variantA: MessageSender; private variantB: MessageSender; private variantAPercentage: number; constructor(variantA: MessageSender, variantB: MessageSender, variantAPercentage: number) { this.variantA = variantA; this.variantB = variantB; this.variantAPercentage = variantAPercentage; } selectSender(userId: string): MessageSender { // Deterministic assignment based on user ID const hash = this.hashCode(userId); const bucket = Math.abs(hash) % 100; if (bucket < this.variantAPercentage) { console.log(`User ${userId} assigned to Variant A`); return this.variantA; } else { console.log(`User ${userId} assigned to Variant B`); return this.variantB; } } private hashCode(str: string): number { let hash = 0; for (let i = 0; i < str.length; i++) { hash = ((hash << 5) - hash) + str.charCodeAt(i); hash |= 0; } return hash; }} // Usage: 30% of users get new email template (variant B)const abService = new ABTestingNotificationService( new EmailSender('old-template.example.com', 587, { user: '', pass: '' }), new EmailSender('new-template.example.com', 587, { user: '', pass: '' }), 70 // 70% get variant A); const senderForUser = abService.selectSender('user-123');const notification = new WelcomeNotification('user@example.com', senderForUser, 'Test');Runtime flexibility enables:
We've explored the Bridge Pattern's solution to the abstraction-implementation coupling problem. Let's crystallize our understanding:
What's next:
Now that we understand the Bridge Pattern's structure and how it solves the coupling problem, we'll compare it with a superficially similar pattern: the Adapter Pattern. Understanding when to use Bridge versus Adapter is crucial for applying these patterns correctly.
You now understand the Bridge Pattern's solution: separating abstraction from implementation into independent hierarchies connected through composition. This structure enables independent evolution, reduces class explosion, and provides runtime flexibility. In the next page, we'll compare Bridge with Adapter to clarify when each pattern is appropriate.