// ───────────────────────────────────────────────────────────────────────────── // ADAPTER [Structural] // ───────────────────────────────────────────────────────────────────────────── // INTENT : Convert the interface of a class into another interface clients expect, so classes with // incompatible interfaces can work together. // PROBLEM : Your code speaks `gateway.charge({ amountCents, currency, token })`; the vendor SDK speaks // `sale(dollarsString, nonce, callback)`. You own neither side's shape. // SAY : Participants: Target (the interface you want), Adaptee (the thing you have), Adapter (implements // Target by wrapping the Adaptee — "object adapter" = composition), Client. Pitch: "a plug // converter — same electricity, new shape." // SEEN IN : `util.promisify` / `util.callbackify` (callback API ↔ Promise API); axios adapters (one API over // XHR / Node http / fetch); `Readable.toWeb` / `Readable.fromWeb` (Node ↔ WHATWG streams); Knex // dialect drivers (pg/mysql2/sqlite behind one interface); your own wrappers around Stripe/Twilio/ // S3 SDKs (`PaymentGateway`, `Mailer`, `BlobStore`); API response → domain model mappers. // VS : Decorator keeps the SAME interface and adds behavior; Adapter CHANGES the interface. Facade // simplifies a whole subsystem with a new API; Adapter makes one object fit an EXISTING API. // Proxy = same interface, controls access. // ───────────────────────────────────────────────────────────────────────────── 'use strict'; const assert = require('node:assert/strict'); const { promisify } = require('node:util'); // ── Classic shape (classes, so the participant names are visible) class LegacyBraintreeSdk { // Adaptee — vendor shape we can't change sale(amountDollars, nonce, cb) { if (!/^\d+\.\d{2}$/.test(amountDollars)) return cb(new Error(`bad amount ${amountDollars}`)); cb(null, { success: true, transaction: { id: `bt_${nonce}`, amount: amountDollars } }); } } // Target interface: charge({ amountCents, currency, token }) -> Promise<{ ok, id }> class StripeGateway { // a native Target implementation async charge({ amountCents, token }) { return { ok: true, id: `ch_${token}_${amountCents}` }; } } class BraintreeGatewayAdapter { // Adapter — implements Target, delegates to Adaptee #sdk; constructor(sdk) { this.#sdk = sdk; } charge({ amountCents, currency, token }) { if (currency !== 'USD') return Promise.reject(new Error('braintree adapter: USD only')); const dollars = (amountCents / 100).toFixed(2); // cents → "12.34" return new Promise((resolve, reject) => { // callback → promise this.#sdk.sale(dollars, token, (err, res) => { if (err) return reject(err); resolve({ ok: res.success, id: res.transaction.id }); // vendor result → our result }); }); } } async function checkout(gateway, cart) { // Client — only knows Target const amountCents = cart.items.reduce((s, i) => s + i.cents * i.qty, 0); return gateway.charge({ amountCents, currency: 'USD', token: cart.token }); } // ── Idiomatic JS shape: a function that maps one shape onto another (+ promisify as a ready-made adapter) const adaptBraintree = (sdk) => { const sale = promisify(sdk.sale.bind(sdk)); return { charge: async ({ amountCents, token }) => { const res = await sale((amountCents / 100).toFixed(2), token); return { ok: res.success, id: res.transaction.id }; }, }; }; // Data-shape adapters are the most common kind: vendor payload → your domain model const toCustomer = (vendor) => ({ id: vendor.customer_id, name: `${vendor.first_name} ${vendor.last_name}`.trim(), email: vendor.contact?.email_address ?? null, }); (async () => { const cart = { items: [{ cents: 1999, qty: 2 }], token: 'nonce42' }; const viaStripe = await checkout(new StripeGateway(), cart); const viaBraintree = await checkout(new BraintreeGatewayAdapter(new LegacyBraintreeSdk()), cart); assert.deepEqual(viaStripe, { ok: true, id: 'ch_nonce42_3998' }); assert.deepEqual(viaBraintree, { ok: true, id: 'bt_nonce42' }); // same client code, vendor swapped await assert.rejects( new BraintreeGatewayAdapter(new LegacyBraintreeSdk()).charge({ amountCents: 1, currency: 'EUR', token: 'x' }), /USD only/, ); const sdk = new LegacyBraintreeSdk(); const sale = promisify(sdk.sale.bind(sdk)); assert.equal((await sale('10.00', 'n1')).transaction.id, 'bt_n1'); // promisify adapted the callback await assert.rejects(sale('10', 'n1'), /bad amount/); // and errors became rejections assert.deepEqual(await adaptBraintree(sdk).charge({ amountCents: 250, token: 'n2' }), { ok: true, id: 'bt_n2' }); assert.deepEqual( toCustomer({ customer_id: 'c1', first_name: 'Ada', last_name: 'L', contact: { email_address: 'a@x.io' } }), { id: 'c1', name: 'Ada L', email: 'a@x.io' }, ); assert.equal(toCustomer({ customer_id: 'c2', first_name: 'Bob', last_name: '' }).email, null); console.log('ok: design-patterns/structural/adapter.js'); })();