// ───────────────────────────────────────────────────────────────────────────── // ABSTRACT FACTORY [Creational] // ───────────────────────────────────────────────────────────────────────────── // INTENT : Provide an interface for creating FAMILIES of related or dependent objects without specifying // their concrete classes. // PROBLEM : Several products must be used together (Stripe charge + Stripe refund + Stripe webhook verifier); // mixing families (Stripe charge + PayPal refund) would be a bug the type system can't catch. // SAY : Participants: AbstractFactory (createX()/createY()), ConcreteFactory (one per family), // AbstractProduct, ConcreteProduct, Client (only sees abstract types). Pitch: "one factory // object hands you a whole consistent kit; swap the factory, swap the kit." // SEEN IN : Knex dialect Clients (each creates matching QueryCompiler/SchemaCompiler/ColumnCompiler); // payment/cloud provider kits (one `provider` object yields charge+refund+verify, or // bucket+queue+secrets); @testing-library per-framework packages (render/screen/fireEvent family); // cross-platform UI kits (React Native Paper: platform-consistent Button/Input/Dialog set). // VS : Factory Method = ONE product, chosen by a subclass override. Abstract Factory = a FAMILY, // chosen by which factory object you hold (usually implemented WITH factory methods). // ───────────────────────────────────────────────────────────────────────────── 'use strict'; const assert = require('node:assert/strict'); // ── Classic shape (classes, so the participant names are visible) // AbstractProducts (interfaces, documented by method shape): // Charger { charge(cents) -> { provider, id } } Refunder { refund(chargeId) -> { refunded } } // Verifier { verify(signature) -> boolean } // ConcreteProducts — Stripe family class StripeCharger { charge(c) { return { provider: 'stripe', id: `ch_${c}` }; } } class StripeRefunder { refund(id) { return { refunded: id.startsWith('ch_') }; } } class StripeVerifier { verify(sig) { return sig.startsWith('t='); } } // ConcreteProducts — PayPal family class PayPalCharger { charge(c) { return { provider: 'paypal', id: `PAYID-${c}` }; } } class PayPalRefunder { refund(id) { return { refunded: id.startsWith('PAYID-') }; } } class PayPalVerifier { verify(sig) { return sig.length === 8; } } class PaymentFactory { // AbstractFactory createCharger() { throw new Error('abstract'); } createRefunder() { throw new Error('abstract'); } createVerifier() { throw new Error('abstract'); } } class StripeFactory extends PaymentFactory { // ConcreteFactory createCharger() { return new StripeCharger(); } createRefunder() { return new StripeRefunder(); } createVerifier() { return new StripeVerifier(); } } class PayPalFactory extends PaymentFactory { // ConcreteFactory createCharger() { return new PayPalCharger(); } createRefunder() { return new PayPalRefunder(); } createVerifier() { return new PayPalVerifier(); } } // Client — depends only on the abstract factory & products function checkoutFlow(factory, cents) { const charger = factory.createCharger(); const refunder = factory.createRefunder(); const { id, provider } = charger.charge(cents); const { refunded } = refunder.refund(id); // works because both come from one family return { provider, id, refunded }; } const s = checkoutFlow(new StripeFactory(), 1999); const p = checkoutFlow(new PayPalFactory(), 1999); assert.deepEqual(s, { provider: 'stripe', id: 'ch_1999', refunded: true }); assert.deepEqual(p, { provider: 'paypal', id: 'PAYID-1999', refunded: true }); // Mixing families is exactly the bug the pattern prevents: const stripeChargeId = new StripeFactory().createCharger().charge(1).id; assert.equal(new PayPalFactory().createRefunder().refund(stripeChargeId).refunded, false); assert.equal(new StripeFactory().createVerifier().verify('t=123,v1=abc'), true); assert.equal(new PayPalFactory().createVerifier().verify('t=123,v1=abc'), false); assert.throws(() => new PaymentFactory().createCharger(), /abstract/); // ── Idiomatic JS shape: a plain object per family, selected once by config const providers = { stripe: { charger: () => ({ charge: (c) => ({ provider: 'stripe', id: `ch_${c}` }) }), refunder: () => ({ refund: (id) => ({ refunded: id.startsWith('ch_') }) }), verifier: () => ({ verify: (sig) => sig.startsWith('t=') }), }, paypal: { charger: () => ({ charge: (c) => ({ provider: 'paypal', id: `PAYID-${c}` }) }), refunder: () => ({ refund: (id) => ({ refunded: id.startsWith('PAYID-') }) }), verifier: () => ({ verify: (sig) => sig.length === 8 }), }, }; const PAYMENT_PROVIDER = 'paypal'; // would come from env/config const kit = providers[PAYMENT_PROVIDER]; // the "factory" is just the selected object const charge = kit.charger().charge(500); assert.equal(charge.provider, 'paypal'); assert.equal(kit.refunder().refund(charge.id).refunded, true); assert.equal(kit.verifier().verify('12345678'), true); assert.deepEqual(Object.keys(providers.stripe), Object.keys(providers.paypal)); // same kit shape per family console.log('ok: design-patterns/creational/abstract-factory.js');