// ───────────────────────────────────────────────────────────────────────────── // BRIDGE [Structural] // ───────────────────────────────────────────────────────────────────────────── // INTENT : Decouple an abstraction from its implementation so that the two can vary independently. // PROBLEM : N kinds of thing (Alert, Digest, Reminder) × M ways to deliver them (Email, SMS, Push) and // subclassing gives you N×M classes (EmailAlert, SmsAlert, PushAlert, EmailDigest, ...). // SAY : Participants: Abstraction (Notification — holds an Implementor), RefinedAbstraction (UrgentAlert, // Digest), Implementor (Transport interface), ConcreteImplementor (EmailTransport, SmsTransport). // Pitch: "two hierarchies composed, not multiplied — N+M classes instead of N×M." // SEEN IN : Winston/pino (Logger abstraction × Transport implementors); react-reconciler (React component // model × host renderers react-dom / react-native / ink); ORMs (Model/Query API × dialect driver); // Expo/React Native modules (JS API × iOS/Android native implementations); Node `http`/`https` // sharing one request API over different sockets. // VS : Adapter retrofits an interface AFTER the fact onto something you don't control; Bridge is // designed UP FRONT so both sides can grow. Strategy swaps one algorithm; Bridge swaps a whole // implementation hierarchy (same shape — context delegates to an object — different intent). // ───────────────────────────────────────────────────────────────────────────── 'use strict'; const assert = require('node:assert/strict'); // ── Classic shape (classes, so the participant names are visible) // Implementor interface: send(to, subject, body) -> record class EmailTransport { // ConcreteImplementor constructor(log) { this.log = log; } send(to, subject, body) { this.log.push({ via: 'email', to, subject, body }); return this.log.at(-1); } } class SmsTransport { // ConcreteImplementor — no subject; copes locally constructor(log) { this.log = log; } send(to, subject, body) { this.log.push({ via: 'sms', to, text: `${subject}: ${body}`.slice(0, 160) }); return this.log.at(-1); } } class PushTransport { // ConcreteImplementor constructor(log) { this.log = log; } send(to, subject, body) { this.log.push({ via: 'push', to, title: subject, body }); return this.log.at(-1); } } class Notification { // Abstraction — delegates to the implementor constructor(transport) { this.transport = transport; } notify(user, text) { return this.transport.send(user.contact, this.subject(), text); } subject() { return 'Notification'; } } class UrgentAlert extends Notification { // RefinedAbstraction — varies WHAT, not HOW subject() { return '[URGENT] Action required'; } notify(user, text) { return super.notify(user, text.toUpperCase()); } } class Digest extends Notification { // RefinedAbstraction constructor(transport, items) { super(transport); this.items = items; } subject() { return `Your ${this.items.length} updates`; } notify(user) { return super.notify(user, this.items.join(' | ')); } } const log = []; const ada = { contact: 'ada@x.io' }; const bob = { contact: '+15550100' }; new UrgentAlert(new EmailTransport(log)).notify(ada, 'disk 99% full'); new UrgentAlert(new SmsTransport(log)).notify(bob, 'disk 99% full'); new Digest(new PushTransport(log), ['3 comments', '1 mention']).notify(ada); assert.deepEqual(log[0], { via: 'email', to: 'ada@x.io', subject: '[URGENT] Action required', body: 'DISK 99% FULL' }); assert.deepEqual(log[1], { via: 'sms', to: '+15550100', text: '[URGENT] Action required: DISK 99% FULL' }); assert.deepEqual(log[2], { via: 'push', to: 'ada@x.io', title: 'Your 2 updates', body: '3 comments | 1 mention' }); // Swap the implementor at runtime — abstraction code untouched const alert = new UrgentAlert(new EmailTransport(log)); alert.transport = new PushTransport(log); assert.equal(alert.notify(ada, 'x').via, 'push'); // 2 abstractions × 3 implementors = 6 combinations from 5 classes (not 6 subclasses) assert.equal([UrgentAlert, Digest].length * [EmailTransport, SmsTransport, PushTransport].length, 6); // ── Idiomatic JS shape: the abstraction is a function/object that takes the implementor as a parameter const transports = { email: (to, subject, body) => ({ via: 'email', to, subject, body }), sms: (to, subject, body) => ({ via: 'sms', to, text: `${subject}: ${body}`.slice(0, 160) }), }; const makeNotifier = (send) => ({ // abstraction side alert: (to, text) => send(to, '[URGENT]', text.toUpperCase()), digest: (to, items) => send(to, `${items.length} updates`, items.join(' | ')), }); const emailer = makeNotifier(transports.email); const texter = makeNotifier(transports.sms); assert.equal(emailer.alert('a@x.io', 'late').body, 'LATE'); assert.equal(texter.digest('+1', ['a', 'b']).text, '2 updates: a | b'); assert.equal(makeNotifier((to) => ({ via: 'noop', to })).alert('x', 'y').via, 'noop'); // test-double implementor // Logger × transport — the shape you already use every day (winston/pino) const createLogger = (transport) => ({ info: (msg) => transport('info', msg), error: (msg) => transport('error', msg), }); const lines = []; const logger = createLogger((level, msg) => lines.push(`${level.toUpperCase()} ${msg}`)); logger.info('boot'); logger.error('oops'); assert.deepEqual(lines, ['INFO boot', 'ERROR oops']); console.log('ok: design-patterns/structural/bridge.js');