24 — Design Patterns: Module, Observer, Strategy & State Machine

Module Pattern (Revealing Module)

javascript
// ── Revealing module pattern: private state via closures, public API via return ──
const UserService = (() => {
    // Private state (closure-captured, not accessible outside)
    const users = new Map();
    let nextId = 1;

    // Private methods
    const validate = (user) => {
        if (!user.name || typeof user.name !== "string") {
            throw new Error("invalid name");
        }
    };

    const generateId = () => nextId++;

    // Public API (revealed via the return object)
    const create = (userData) => {
        validate(userData);
        const id = generateId();
        const user = { id, ...userData, createdAt: Date.now() };
        users.set(id, user);
        return user;
    };

    const getById = (id) => users.get(id);
    const getAll = () => [...users.values()];
    const remove = (id) => users.delete(id);

    return { create, getById, getAll, remove };  // reveal only the public API
})();

// Usage:
UserService.create({ name: "Alice" });
UserService.create({ name: "Bob" });
console.log(UserService.getAll());  // [{ id: 1, name: "Alice", ... }, { id: 2, ... }]
// UserService.validate({});  // ✗ TypeError: not a function (private — not revealed)

Observer / Pub-Sub Pattern

javascript
// ── Observer pattern: subjects notify subscribers on state change ──
class EventEmitter {
    #handlers = new Map();

    on(event, handler) {
        if (!this.#handlers.has(event)) this.#handlers.set(event, new Set());
        this.#handlers.get(event).add(handler);
        return () => this.off(event, handler);  // return unsubscribe function
    }

    once(event, handler) {
        const unsubscribe = this.on(event, (...args) => {
            unsubscribe();  // auto-remove after first call
            handler(...args);
        });
        return unsubscribe;
    }

    off(event, handler) {
        this.#handlers.get(event)?.delete(handler);
    }

    emit(event, ...args) {
        const handlers = this.#handlers.get(event);
        if (handlers) for (const handler of handlers) handler(...args);
    }

    clear() {
        this.#handlers.clear();
    }
}

// ── Usage ──
const emitter = new EventEmitter();
const unsubscribe = emitter.on("data", (data) => console.log("received:", data));
emitter.emit("data", { id: 1 });  // "received: { id: 1 }"
unsubscribe();  // remove the listener
emitter.emit("data", { id: 2 });  // no output (listener removed)

// ── WeakRef for auto-cleanup (subscriber can be GC'd without unsubscribing) ──
class WeakEventEmitter {
    #handlers = new Map();

    on(event, handler, target = {}) {
        if (!this.#handlers.has(event)) this.#handlers.set(event, new Map());
        const weakTarget = new WeakRef(target);
        this.#handlers.get(event).set(handler, { weakTarget, handler });
    }

    emit(event, ...args) {
        const handlers = this.#handlers.get(event);
        if (!handlers) return;
        for (const [handler, { weakTarget }] of handlers) {
            if (weakTarget.deref()) {  // only call if the target still exists
                handler(...args);
            } else {
                handlers.delete(handler);  // auto-cleanup: target was GC'd
            }
        }
    }
}

Strategy Pattern

javascript
// ── Strategy: interchangeable algorithms behind a common interface ──
class Sorter {
    constructor(strategy) {
        this.strategy = strategy;
    }

    setStrategy(strategy) {
        this.strategy = strategy;
    }

    sort(data) {
        return this.strategy(data);
    }
}

// Concrete strategies (interchangeable algorithms)
const strategies = {
    ascending: (data) => [...data].sort((a, b) => a - b),
    descending: (data) => [...data].sort((a, b) => b - a),
    shuffle: (data) => [...data].sort(() => Math.random() - 0.5),
    alphabetical: (data) => [...data].sort((a, b) => String(a).localeCompare(String(b))),
};

// Usage:
const sorter = new Sorter(strategies.ascending);
console.log(sorter.sort([3, 1, 2]));  // [1, 2, 3]
sorter.setStrategy(strategies.descending);
console.log(sorter.sort([3, 1, 2]));  // [3, 2, 1]
sorter.setStrategy(strategies.shuffle);
console.log(sorter.sort([1, 2, 3, 4, 5]));  // shuffled

// ── Strategy for payment processing ──
const paymentStrategies = {
    creditCard: (amount, cardInfo) => {
        // process credit card payment
        return { status: "paid", method: "creditCard", amount };
    },
    paypal: (amount, paypalInfo) => {
        return { status: "paid", method: "paypal", amount };
    },
    crypto: (amount, walletInfo) => {
        return { status: "paid", method: "crypto", amount };
    },
};

function processPayment(method, amount, info) {
    const strategy = paymentStrategies[method];
    if (!strategy) throw new Error(`unknown payment method: ${method}`);
    return strategy(amount, info);
}

State Machine Pattern

javascript
// ── Finite state machine: explicit states, transitions, and side effects ──
class StateMachine {
    #state;
    #states;
    #emitter = new EventEmitter();

    constructor(initialState, states) {
        this.#state = initialState;
        this.#states = states;
    }

    get state() { return this.#state; }

    transition(action) {
        const currentState = this.#states[this.#state];
        const nextState = currentState?.transitions?.[action];
        if (!nextState) {
            throw new Error(`invalid transition: ${this.#state} → ${action}`);
        }
        // Run exit action for current state
        currentState.onExit?.(this);
        // Transition
        this.#state = nextState;
        // Run entry action for new state
        this.#states[nextState].onEntry?.(this);
        // Notify listeners
        this.#emitter.emit("transition", { from: currentState, action, to: nextState });
    }

    on(event, handler) { return this.#emitter.on(event, handler); }
    can(action) { return Boolean(this.#states[this.#state]?.transitions?.[action]); }
}

// ── Traffic light state machine ──
const trafficLight = new StateMachine("red", {
    red:    { transitions: { go: "green" }, onEntry: (m) => console.log("STOP") },
    green:  { transitions: { warn: "yellow" }, onEntry: (m) => console.log("GO") },
    yellow: { transitions: { stop: "red" }, onEntry: (m) => console.log("SLOW DOWN") },
});

trafficLight.on("transition", ({ to }) => console.log(`→ ${to}`));
trafficLight.transition("go");    // → green, "GO"
trafficLight.transition("warn");  // → yellow, "SLOW DOWN"
trafficLight.transition("stop");  // → red, "STOP"
// trafficLight.transition("go"); from yellow → Error: invalid transition

// ── HTTP request state machine ──
const requestStates = {
    idle: { transitions: { send: "pending" } },
    pending: { transitions: { resolve: "success", reject: "error" } },
    success: { transitions: { reset: "idle" } },
    error: { transitions: { retry: "pending", reset: "idle" } },
};

Builder Pattern for Fluent APIs

javascript
// ── Builder: construct complex objects step-by-step with method chaining ──
class QueryBuilder {
    #table = "";
    #columns = [];
    #conditions = [];
    #orderBy = "";
    #limit = null;

    select(...columns) { this.#columns = columns; return this; }
    from(table) { this.#table = table; return this; }
    where(condition) { this.#conditions.push(condition); return this; }
    orderBy(column) { this.#orderBy = column; return this; }
    limit(n) { this.#limit = n; return this; }

    build() {
        let sql = `SELECT ${this.#columns.join(", ") || "*"} FROM ${this.#table}`;
        if (this.#conditions.length) sql += ` WHERE ${this.#conditions.join(" AND ")}`;
        if (this.#orderBy) sql += ` ORDER BY ${this.#orderBy}`;
        if (this.#limit !== null) sql += ` LIMIT ${this.#limit}`;
        return sql;
    }
}

// Fluent API (method chaining — each method returns `this`)
const query = new QueryBuilder()
    .select("name", "email")
    .from("users")
    .where("age > 18")
    .where("active = true")
    .orderBy("name")
    .limit(10)
    .build();
console.log(query);  // "SELECT name, email FROM users WHERE age > 18 AND active = true ORDER BY name LIMIT 10"

💡 Tips & Tricks

javascript
// ── Factory pattern for object creation ──
function createButton({ text = "Click", onClick = () => {}, disabled = false } = {}) {
    const button = document.createElement("button");
    button.textContent = text;
    button.disabled = disabled;
    button.addEventListener("click", onClick);
    return button;
}

// ── Singleton via module (ESM is a singleton by default) ──
// config.js:
// const config = { apiUrl: "..." };
// export default config;  // ESM modules are singletons (same instance everywhere)

// ── Singleton with lazy initialization ──
let _instance = null;
class Database {
    constructor() {
        if (_instance) return _instance;  // return existing instance
        _instance = this;
        this.connection = connect();
    }
}

// ── Decorator pattern (function decorators) ──
function withLogging(fn) {
    return function(...args) {
        console.log(`calling ${fn.name} with`, args);
        const result = fn.apply(this, args);
        console.log(`result:`, result);
        return result;
    };
}
const add = withLogging((a, b) => a + b);
add(1, 2);  // logs "calling with [1, 2]", "result: 3"

⚠️ Edge Cases & Gotchas

javascript
// ── Event listeners and memory leaks ──
// Always provide a way to unsubscribe (return a cleanup function or use AbortController).
// If the subscriber is GC'd but the publisher holds a strong reference, the listener leaks.

// ── `this` in strategy methods ──
// When passing a method as a strategy, `this` is lost (detached).
// Fix: use .bind(this) or arrow functions: strategy = strategy.bind(this)

// ── State machine: invalid transitions should throw (not silently ignore) ──
// Silent ignoring makes debugging harder. Explicit errors catch bugs early.

// ── Singleton is an anti-pattern for testing ──
// Singletons make testing harder (global state, hard to reset between tests).
// Use dependency injection instead for testable code.

// ── Builder must return `this` for chaining ──
// Forgetting `return this` breaks the chain — each method returns undefined.

🧠 Quick Quiz

What pattern is this, and what's the issue?

javascript
const emitter = new EventEmitter();
emitter.on("update", function() { this.render(); });
Answer

This is the Observer pattern (event emitter), but there's a this binding issue.

When the emitter calls the handler, this inside the regular function is determined by the call site — which is the emitter (not the component that registered the handler). So this.render() would fail because this is the emitter, not the component.

Fix: use an arrow function (lexical this) or bind:

emitter.on("update", () => this.render());  // arrow: `this` is the enclosing scope
// or:
emitter.on("update", this.render.bind(this));  // bind: `this` is the component

The lesson: event handler callbacks lose their this binding (the emitter's call site determines this, not the registration site). Use arrow functions or .bind(this) to preserve the correct this.