// ───────────────────────────────────────────────────────────────────────────── // FACTORY METHOD [Creational] // ───────────────────────────────────────────────────────────────────────────── // INTENT : Define an interface for creating ONE object, but let subclasses decide which class to // instantiate — defer the `new` to a hook method. // PROBLEM : A base algorithm must create a product but shouldn't know the concrete class; the // `new ConcreteThing()` is the only line that varies between variants. // SAY : Participants: Creator (declares factoryMethod()), ConcreteCreator (overrides it), Product // (interface), ConcreteProduct. Pitch: "the `new` is the hook; subclasses (or a keyed function) // pick the class." // SEEN IN : `document.createElement(tag)`; `http.createServer()` / `net.createServer()` / `crypto.createHash()`; // `React.createElement` / `React.createContext`; static factories `Array.from`, `Buffer.from`, // `Promise.resolve`; ORM `Model.create()` / `Model.build()`; `express.Router()`. // (Everyday JS uses parameterized factory FUNCTIONS; the subclass-override form is rarer.) // VS : Abstract Factory = a FAMILY of related products from one factory object; Factory Method = one // product from one overridable method. Builder = one complex product, many steps. // ───────────────────────────────────────────────────────────────────────────── 'use strict'; const assert = require('node:assert/strict'); // ── Classic shape (classes, so the participant names are visible) class Notification { // Product constructor(to, body) { this.to = to; this.body = body; } render() { throw new Error('abstract'); } } class EmailNotification extends Notification { // ConcreteProduct render() { return `MAIL to=${this.to} subject="Update" body=${this.body}`; } } class SmsNotification extends Notification { // ConcreteProduct render() { return `SMS to=${this.to} ${this.body.slice(0, 160)}`; } } class NotificationService { // Creator — owns the algorithm, defers the `new` send(to, body) { const n = this.createNotification(to, body); // ← the factory method return { channel: this.channel, payload: n.render() }; } createNotification() { throw new Error('subclass must implement createNotification'); } } class EmailService extends NotificationService { // ConcreteCreator channel = 'email'; createNotification(to, body) { return new EmailNotification(to, body); } } class SmsService extends NotificationService { // ConcreteCreator channel = 'sms'; createNotification(to, body) { return new SmsNotification(to, body); } } const email = new EmailService().send('a@x.io', 'Your order shipped'); const sms = new SmsService().send('+15550100', 'Your order shipped'); assert.equal(email.channel, 'email'); assert.match(email.payload, /^MAIL to=a@x\.io/); assert.match(sms.payload, /^SMS to=\+15550100/); assert.throws(() => new NotificationService().send('x', 'y'), /subclass must implement/); // Same algorithm (send), different product — the only thing that varied was the creation step. const long = 'x'.repeat(200); assert.equal(new SmsService().send('+1', long).payload.length, 'SMS to=+1 '.length + 160); // ── Idiomatic JS shape: a function (or keyed map) that returns the right object const createNotification = (channel, to, body) => { const makers = { email: () => ({ channel, render: () => `MAIL to=${to} body=${body}` }), sms: () => ({ channel, render: () => `SMS to=${to} ${body.slice(0, 160)}` }), push: () => ({ channel, render: () => `PUSH ${to}: ${body}` }), }; const make = makers[channel]; if (!make) throw new Error(`unknown channel: ${channel}`); return make(); }; assert.equal(createNotification('push', 'dev-1', 'hi').render(), 'PUSH dev-1: hi'); assert.equal(createNotification('sms', '+1', 'hi').channel, 'sms'); assert.throws(() => createNotification('fax', 'x', 'y'), /unknown channel/); // Static `from()` / `create()` that hides WHICH representation you get is also "factory method" in // everyday speech (Array.from, Buffer.from, Promise.resolve all do this). class Money { static from(input) { return typeof input === 'string' ? new Money(Math.round(Number(input) * 100)) : new Money(input); } constructor(cents) { this.cents = cents; } } assert.equal(Money.from('12.34').cents, 1234); assert.equal(Money.from(500).cents, 500); assert.ok(Money.from('1') instanceof Money); console.log('ok: design-patterns/creational/factory-method.js');