Skip to content

Behavioral Patterns

Behavioral patterns deal with object communication and the assignment of responsibilities between objects. They describe how objects interact and distribute work, making complex control flows easier to understand and maintain.

Analogy

Behavioral patterns solve the same problem any organization faces: how do people coordinate without everyone needing to know everyone else's job? A customer complaint escalates through support tiers until someone handles it (Chain of Responsibility). A restaurant order ticket can be queued, logged, or voided — the waiter never touches a stove (Command). A playlist's "Next" button hides whether songs are sorted by artist or shuffled (Iterator). An air traffic control tower prevents planes from radioing each other directly (Mediator). Saving your game before a boss fight lets you reload from that exact point (Memento). YouTube notifies all subscribers when a creator uploads (Observer). A vending machine changes behavior based on its state — same buttons, different results (State). Google Maps swaps between driving, walking, or transit for the same destination (Strategy). Corporate onboarding follows fixed steps, but "laptop setup" differs for iOS vs. backend devs (Template Method). A building inspector applies the same checklist differently at a restaurant vs. a warehouse (Visitor). Your brain parses "2 + 3 × 4" into a tree where multiplication binds tighter — that's an interpreter (Interpreter).

Creational Patterns

Object creation mechanisms — abstracting instantiation to make systems independent of how objects are created.

Read more →

Structural Patterns

Object composition and relationships — assembling larger structures while keeping them flexible and efficient.

Read more →

All patterns overview →

Chain of Responsibility

Passes a request along a chain of handlers, where each handler decides to process the request or pass it to the next handler. Use for middleware pipelines, event handling, or any scenario where multiple objects may handle a request.

Analogy

Like a corporate expense approval chain — you submit a $500 receipt to your manager. Under $100? They approve it. Over? They pass it to the director. Over $1,000? Up to the VP. Each level either handles it or passes it along without you knowing who ultimately signed off. Express.js middleware works exactly this way: each handler either responds or calls next().

When to Use

  • Multiple handlers may process a request, but you don't know which one in advance — e.g., Express.js middleware: auth → rate limit → route handler
  • You want to decouple senders from receivers — e.g., a support ticket escalating through L1 → L2 → L3 without the customer knowing who handles it
  • Processing order matters and should be configurable — e.g., input validation pipelines: required → format → length → uniqueness

Trade-offs

  • ✅ Loose coupling — senders don't know which handler processes the request
  • ✅ Dynamic chain composition — add/remove/reorder handlers at runtime
  • ⚠️ No guarantee of handling — a request can fall through the entire chain unprocessed. In production, add a default/catch-all handler at the end of the chain to log unhandled requests.
  • ⚠️ Hard to debug which handler acted — request may be modified by multiple handlers. Use structured logging in each handler to create an audit trail.

Fitness Test

"Could more than one handler process this request, and should I be able to add or remove handlers without changing the sender?"

  • Yes → ✅ Chain of Responsibility fits
  • No, there's always exactly one handler → ❌ Call it directly
flowchart LR
    A[Request] --> B[AuthHandler]
    B -->|pass| C[RateLimitHandler]
    C -->|pass| D[RouteHandler]
abstract class Handler {
    private next: Handler | null = null;

    setNext(handler: Handler): Handler {
        this.next = handler;
        return handler;
    }

    handle(request: string): string | null {
        if (this.next) {
            return this.next.handle(request);
        }
        return null;
    }
}

class AuthHandler extends Handler {
    handle(request: string): string | null {
        if (request === "unauthorized") {
            return "AuthHandler: Blocked — not authenticated";
        }
        return super.handle(request);
    }
}

class RateLimitHandler extends Handler {
    handle(request: string): string | null {
        if (request === "spam") {
            return "RateLimitHandler: Blocked — too many requests";
        }
        return super.handle(request);
    }
}

class RouteHandler extends Handler {
    handle(request: string): string | null {
        return `RouteHandler: Processing '${request}'`;
    }
}

const auth = new AuthHandler();
const rateLimit = new RateLimitHandler();
const route = new RouteHandler();

auth.setNext(rateLimit).setNext(route);

console.log(auth.handle("unauthorized")); // Blocked by auth
console.log(auth.handle("spam"));         // Blocked by rate limit
console.log(auth.handle("GET /users"));   // Processed by route

Command

Encapsulates a request as an object, letting you parameterize clients with different requests, queue them, log them, or support undo operations. Use for undo/redo, task queuing, macro recording, or transactional behavior.

Analogy

Like a restaurant order ticket — the waiter writes "one burger, no onions" (command object), clips it to the kitchen rail, and walks away. The ticket can be queued, prioritized, or voided — all without the waiter touching a stove. The kitchen (receiver) executes it later. This is why Ctrl+Z works: every action is a command object with execute() and undo() stored in a history stack.

When to Use

  • You need undo/redo functionality — e.g., text editors storing each action as a command with execute() and undo()
  • Operations should be queued, scheduled, or executed remotely — e.g., job queues (Laravel jobs, Celery tasks), message-driven architectures
  • You want to log or replay operations — e.g., database migrations with up() and down() methods, event sourcing

Trade-offs

  • ✅ Decouples invoker from receiver — the button doesn't know what it triggers
  • ✅ Commands are first-class objects — queue, log, serialize, replay them
  • ⚠️ Proliferation of command classes — one class per operation. Acceptable when operations are complex or need undo; for simple fire-and-forget actions, a lambda/callback is simpler.
  • ⚠️ Undo can be expensive or impossible for some operations — e.g., sending an email, deleting external data. In production, mark commands as reversible/irreversible and confirm before executing irreversible ones.

Fitness Test

"Do I need to undo, queue, log, or replay this operation later?"

  • Yes → ✅ Command fits
  • No, it's fire-and-forget → ❌ A direct method call is simpler
flowchart LR
    A[CommandHistory] -->|push| B[InsertTextCommand]
    B -->|execute| C[Editor]
    A -->|pop/undo| B
interface Command {
    execute(): void;
    undo(): void;
}

class Editor {
    content = "";
}

class InsertTextCommand implements Command {
    constructor(private editor: Editor, private text: string) {}

    execute(): void {
        this.editor.content += this.text;
    }

    undo(): void {
        this.editor.content = this.editor.content.slice(0, -this.text.length);
    }
}

class CommandHistory {
    private history: Command[] = [];

    push(command: Command): void {
        command.execute();
        this.history.push(command);
    }

    pop(): void {
        const command = this.history.pop();
        command?.undo();
    }
}

const editor = new Editor();
const history = new CommandHistory();

history.push(new InsertTextCommand(editor, "Hello "));
history.push(new InsertTextCommand(editor, "World"));
console.log(editor.content); // "Hello World"

history.pop();
console.log(editor.content); // "Hello "

Iterator

Provides a way to access elements of a collection sequentially without exposing its underlying representation. Use when you want to traverse complex data structures (trees, graphs, custom collections) with a uniform interface.

Analogy

Like a playlist's "Next" button — you skip through songs without knowing whether they're stored alphabetically, by artist, or in a shuffled array. The playlist gives you one song at a time and hides its internal organization. This is why for...of works on arrays, sets, maps, and generators in TypeScript — they all implement the iterator protocol.

When to Use

  • You need to traverse a collection without exposing its internal structure — e.g., iterating a tree, graph, or custom data structure uniformly
  • You want multiple simultaneous traversals over the same collection — e.g., database cursors allowing independent read positions
  • Lazy evaluation over large or infinite sequences — e.g., paginated API results, streaming file processing with Node.js readable streams

Trade-offs

  • ✅ Uniform traversal interface — for...of works on arrays, sets, maps, and generators
  • ✅ Lazy evaluation — only compute/fetch what's needed, saving memory
  • ⚠️ Forward-only by default — random access or backward traversal needs extra work. Acceptable for streaming/pipeline use cases; use indexed access if you need arbitrary element lookup.
  • ⚠️ Collection modification during iteration can cause bugs. In production, iterate over a snapshot or use concurrent-safe iterators (e.g., Java's ConcurrentHashMap iterators).

Fitness Test

"Do I need to traverse this collection without exposing its internal structure, or support multiple concurrent traversals?"

  • Yes → ✅ Iterator fits
  • No, a simple for loop over an array is enough → ❌ Skip the abstraction
flowchart LR
    A[RangeIterator] -->|next| B["1"]
    B --> C["3"]
    C --> D["5"]
    D --> E["..."]
class RangeIterator implements IterableIterator<number> {
    private current: number;

    constructor(
        private start: number,
        private end: number,
        private step: number = 1
    ) {
        this.current = start;
    }

    next(): IteratorResult<number> {
        if (this.current <= this.end) {
            const value = this.current;
            this.current += this.step;
            return { value, done: false };
        }
        return { value: undefined, done: true };
    }

    [Symbol.iterator](): IterableIterator<number> {
        return this;
    }
}

const range = new RangeIterator(1, 10, 2);
for (const num of range) {
    console.log(num); // 1, 3, 5, 7, 9
}

Mediator

Defines an object that encapsulates how a set of objects interact, promoting loose coupling by preventing objects from referring to each other directly. Use for chat rooms, air traffic control, or UI components that need to communicate.

Analogy

Like an air traffic control tower — planes don't radio each other ("Hey United 472, can I land before you?"). Instead, every plane talks only to the tower, and the tower orchestrates who lands when. Without it, N planes would need N² communication channels. Chat rooms work this way: messages go to the server (mediator), not directly between users.

When to Use

  • Multiple objects interact in complex ways and you want to centralize coordination — e.g., a form mediator enabling submit only when all fields are valid
  • Components should communicate without direct references to each other — e.g., chat rooms routing messages through a server, not peer-to-peer
  • N-to-N communication is becoming unmanageable — e.g., a wizard dialog where step changes update the progress bar, nav buttons, and content panel

Trade-offs

  • ✅ Reduces coupling — components only know the mediator, not each other
  • ✅ Centralizes interaction logic — easier to understand and modify in one place
  • ⚠️ The mediator can become a god object — all interaction logic in one place. In production, split into focused mediators per feature area (e.g., FormMediator, NavigationMediator) rather than one mega-mediator.
  • ⚠️ Single point of failure — if the mediator breaks, all communication breaks. Acceptable for in-process coordination; for distributed systems, use resilient message brokers (RabbitMQ, Kafka) instead.

Fitness Test

"If I removed one component, would the others still need to know about each other to work?"

  • Yes → ❌ No mediator — components are directly coupled
  • No → ✅ Mediator fits — it's already coordinating them
flowchart TB
    A[FormMediator] --- B[TextInput]
    A --- C[SubmitButton]
    B -.->|notify| A
    A -.->|setEnabled| C
interface Mediator {
    notify(sender: Component, event: string): void;
}

class Component {
    constructor(protected mediator: Mediator) {}
}

class TextInput extends Component {
    value = "";

    onChange(text: string): void {
        this.value = text;
        this.mediator.notify(this, "textChanged");
    }
}

class SubmitButton extends Component {
    enabled = false;

    setEnabled(state: boolean): void {
        this.enabled = state;
        console.log(`Button ${state ? "enabled" : "disabled"}`);
    }
}

class FormMediator implements Mediator {
    constructor(
        public input: TextInput,
        public button: SubmitButton
    ) {}

    notify(sender: Component, event: string): void {
        if (event === "textChanged") {
            this.button.setEnabled(this.input.value.length > 0);
        }
    }
}

const input = new TextInput(null as any);
const button = new SubmitButton(null as any);
const mediator = new FormMediator(input, button);
input.mediator = mediator;

input.onChange("hello"); // Button enabled
input.onChange("");      // Button disabled

Memento

Captures and externalizes an object's internal state so it can be restored later, without violating encapsulation. Use for undo mechanisms, save/load in games, or snapshotting application state.

Analogy

Like saving your game before a boss fight — you capture a snapshot of your health, inventory, and position so you can reload from that exact point if you die, without the save file exposing the game engine's internal data structures. Database transaction logs work the same way: they capture state snapshots so the system can rollback to a consistent point after a crash.

When to Use

  • You need to snapshot and restore object state — e.g., undo in text editors, game save/load checkpoints
  • State rollback must not violate encapsulation — e.g., the snapshot captures state without exposing internal data structures
  • You need version history or configuration rollback — e.g., network router config backups before applying changes, Git commits

Trade-offs

  • ✅ Preserves encapsulation — state is captured without exposing internals
  • ✅ Simple undo mechanism — push snapshot before change, pop to restore
  • ⚠️ Memory-intensive if snapshots are large or frequent — e.g., saving full game world state every second. In production, use incremental snapshots (deltas) or cap the history depth.
  • ⚠️ Caretaker must manage snapshot lifecycle — risk of memory leaks from unbounded history. Set a max history size and discard oldest mementos, or use weak references where appropriate.

Fitness Test

"Do I need to restore this object to a previous state without exposing its internals?"

  • Yes → ✅ Memento fits
  • No, I just need the latest state → ❌ No snapshots needed
flowchart LR
    A[TextEditor] -->|save| B[EditorMemento]
    C["Snapshots[]"] -->|store| B
    C -->|restore| A
class EditorMemento {
    constructor(readonly content: string, readonly cursorPos: number) {}
}

class TextEditor {
    private content = "";
    private cursorPos = 0;

    type(text: string): void {
        this.content += text;
        this.cursorPos = this.content.length;
    }

    save(): EditorMemento {
        return new EditorMemento(this.content, this.cursorPos);
    }

    restore(memento: EditorMemento): void {
        this.content = memento.content;
        this.cursorPos = memento.cursorPos;
    }

    toString(): string {
        return `"${this.content}" (cursor: ${this.cursorPos})`;
    }
}

const editor = new TextEditor();
const snapshots: EditorMemento[] = [];

editor.type("Hello");
snapshots.push(editor.save());

editor.type(" World");
console.log(editor.toString()); // "Hello World" (cursor: 11)

editor.restore(snapshots.pop()!);
console.log(editor.toString()); // "Hello" (cursor: 5)

Observer

Defines a one-to-many dependency so that when one object changes state, all its dependents are notified automatically. Use for event systems, data binding, pub/sub messaging, or reactive UIs.

Analogy

Like subscribing to a YouTube channel — when a creator uploads a video (state change), YouTube automatically notifies all subscribers. You didn't poll the channel page every 5 minutes; you registered once and get pushed updates. Unsubscribe anytime. This is the backbone of every reactive UI framework: React's state changes notify subscribed components to re-render.

When to Use

  • One object changing should automatically notify multiple dependents — e.g., React state changes re-rendering subscribed components
  • You need loose coupling between event producers and consumers — e.g., an order service emitting "OrderPlaced" consumed by inventory, email, and analytics
  • Dynamic subscription/unsubscription at runtime — e.g., webhook registrations, pub/sub topics, Stripe event listeners

Trade-offs

  • ✅ Loose coupling — publisher doesn't know or care who subscribes
  • ✅ Dynamic — subscribers added/removed at runtime without modifying the publisher
  • ⚠️ Unexpected cascades — one event triggers observers that emit more events. In production, avoid observer chains deeper than 2 levels; use async event buses to break synchronous cascades.
  • ⚠️ Memory leaks from forgotten subscriptions — observers hold references to subjects. Always unsubscribe in cleanup/teardown (React's useEffect return, Angular's ngOnDestroy).

Fitness Test

"When this object changes, do multiple other objects need to react — and should they be loosely coupled?"

  • Yes → ✅ Observer fits
  • No, only one consumer → ❌ A direct callback is simpler
flowchart LR
    A[EventEmitter] -->|notify| B[Observer 1]
    A -->|notify| C[Observer 2]
    A -->|notify| D[Observer N]
type Listener<T> = (data: T) => void;

class EventEmitter<T> {
    private listeners: Listener<T>[] = [];

    subscribe(listener: Listener<T>): () => void {
        this.listeners.push(listener);
        return () => {
            this.listeners = this.listeners.filter(l => l !== listener);
        };
    }

    emit(data: T): void {
        this.listeners.forEach(listener => listener(data));
    }
}

// Usage
const priceUpdates = new EventEmitter<{ symbol: string; price: number }>();

const unsubscribe = priceUpdates.subscribe(({ symbol, price }) => {
    console.log(`Dashboard: ${symbol} is now $${price}`);
});

priceUpdates.subscribe(({ symbol, price }) => {
    if (price > 100) console.log(`Alert: ${symbol} exceeded $100!`);
});

priceUpdates.emit({ symbol: "AAPL", price: 150 });
// Dashboard: AAPL is now $150
// Alert: AAPL exceeded $100!

unsubscribe(); // First listener removed

State

Allows an object to alter its behavior when its internal state changes, as if it changed its class. Use for objects with distinct modes or phases — e.g., order processing, media players, traffic lights, or document workflows.

Analogy

Like a vending machine — insert coins and it switches from "idle" to "has money." Press a button and it switches to "dispensing." Same physical buttons, completely different behavior depending on the current state. Without the State pattern, you'd have giant if/else chains checking currentState in every method — which is exactly the mess traffic light controllers avoid by using this pattern.

When to Use

  • An object's behavior changes based on its internal state — e.g., an order transitioning through Pending → Paid → Shipped → Delivered with different allowed actions at each stage
  • You have large if/else or switch blocks checking state in every method — e.g., a media player behaving differently when playing, paused, or stopped
  • State transitions have clear rules and constraints — e.g., you can't ship an unpaid order, a TCP socket can't send data while closed

Trade-offs

  • ✅ Eliminates conditional sprawl — each state encapsulates its own behavior
  • ✅ Adding new states doesn't touch existing state classes — Open/Closed Principle
  • ⚠️ Class proliferation — one class per state. Acceptable when states have distinct behavior (order processing, game characters); overkill for simple 2-state toggles (use a boolean).
  • ⚠️ Transition logic can be scattered — each state decides its own next state. In production, consider a state transition table or state machine library for complex flows to keep transitions visible in one place.

Fitness Test

"Does this object behave differently depending on its current state, and am I writing if/else chains to check it in every method?"

  • Yes → ✅ State fits
  • No, behavior is the same regardless of state → ❌ No State pattern needed
flowchart LR
    A[Pending] -->|next| B[Paid]
    B -->|next| C[Shipped]
    C -->|next| D[Delivered]
interface OrderState {
    next(order: Order): void;
    toString(): string;
}

class PendingState implements OrderState {
    next(order: Order): void { order.setState(new PaidState()); }
    toString(): string { return "Pending"; }
}

class PaidState implements OrderState {
    next(order: Order): void { order.setState(new ShippedState()); }
    toString(): string { return "Paid"; }
}

class ShippedState implements OrderState {
    next(order: Order): void { order.setState(new DeliveredState()); }
    toString(): string { return "Shipped"; }
}

class DeliveredState implements OrderState {
    next(_order: Order): void { console.log("Already delivered"); }
    toString(): string { return "Delivered"; }
}

class Order {
    private state: OrderState = new PendingState();

    setState(state: OrderState): void { this.state = state; }
    next(): void { this.state.next(this); }
    getStatus(): string { return this.state.toString(); }
}

const order = new Order();
console.log(order.getStatus()); // Pending
order.next();
console.log(order.getStatus()); // Paid
order.next();
console.log(order.getStatus()); // Shipped
order.next();
console.log(order.getStatus()); // Delivered

Strategy

Defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime. Use when you have multiple ways to do the same thing and want to switch between them — e.g., sorting algorithms, payment methods, or compression strategies.

Analogy

Like GPS navigation offering driving, walking, or transit for the same destination — each routing algorithm is a self-contained strategy you can swap at runtime. The app doesn't care which one you pick; it just calls calculateRoute() on whichever strategy is active. Payment processors use this: swap between Stripe, PayPal, or bank transfer without changing the checkout flow.

When to Use

  • You have multiple algorithms for the same task and want to swap them at runtime — e.g., payment processing switching between Stripe, PayPal, or bank transfer
  • You want to eliminate conditional logic that selects an algorithm — e.g., replacing if (method === 'credit') ... else if (method === 'paypal') ...
  • The algorithm should be configurable per user, per request, or per environment — e.g., compression strategy based on file type, auth strategy per route

Trade-offs

  • ✅ Algorithms are interchangeable — swap without modifying client code
  • ✅ Open/Closed Principle — add new strategies without touching existing ones
  • ⚠️ Client must know enough to choose the right strategy. In production, pair with a factory or config-based selection to hide the choice from end users.
  • ⚠️ Overkill when the set of algorithms is small and static. If you only have 2 strategies and they won't grow, a simple conditional is clearer than the pattern machinery.

Fitness Test

"Am I choosing between multiple algorithms at runtime, and using conditionals to pick which one to run?"

  • Yes → ✅ Strategy fits
  • No, there's only one algorithm → ❌ A direct implementation is clearer
flowchart TB
    A[Sorter] -->|uses| B["SortStrategy"]
    C[BubbleSort] -.->|implements| B
    D[QuickSort] -.->|implements| B
interface SortStrategy {
    sort(data: number[]): number[];
}

class BubbleSort implements SortStrategy {
    sort(data: number[]): number[] {
        const arr = [...data];
        for (let i = 0; i < arr.length; i++) {
            for (let j = 0; j < arr.length - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
                }
            }
        }
        return arr;
    }
}

class QuickSort implements SortStrategy {
    sort(data: number[]): number[] {
        if (data.length <= 1) return data;
        const pivot = data[0];
        const left = data.slice(1).filter(x => x <= pivot);
        const right = data.slice(1).filter(x => x > pivot);
        return [...this.sort(left), pivot, ...this.sort(right)];
    }
}

class Sorter {
    constructor(private strategy: SortStrategy) {}

    setStrategy(strategy: SortStrategy): void {
        this.strategy = strategy;
    }

    sort(data: number[]): number[] {
        return this.strategy.sort(data);
    }
}

const sorter = new Sorter(new BubbleSort());
console.log(sorter.sort([3, 1, 4, 1, 5])); // [1, 1, 3, 4, 5]

sorter.setStrategy(new QuickSort());
console.log(sorter.sort([3, 1, 4, 1, 5])); // [1, 1, 3, 4, 5]

Template Method

Defines the skeleton of an algorithm in a base class, letting subclasses override specific steps without changing the overall structure. Use when multiple classes share the same algorithm but differ in certain steps — e.g., data parsers, report generators, or build pipelines.

Analogy

Like a corporate onboarding process — every new hire follows the same steps: paperwork → laptop setup → team intro → first task. But "laptop setup" means installing Xcode for iOS devs and VS Code for frontend devs. The overall sequence is fixed (template); individual steps are customizable (overridable methods). Testing frameworks work this way: setUp() → run test → tearDown(), where you override the test body.

When to Use

  • Multiple classes share the same algorithm skeleton but differ in specific steps — e.g., testing frameworks: setUp() → run test → tearDown()
  • You want to enforce an algorithm's structure while allowing step customization — e.g., ETL pipelines where each data source overrides extract() and parse()
  • Code duplication exists across subclasses that follow the same flow — e.g., different report formats all doing load → process → render

Trade-offs

  • ✅ Eliminates duplicate algorithm structure — DRY across subclasses
  • ✅ Enforces a consistent workflow — subclasses can't skip or reorder steps
  • ⚠️ Inheritance-based — tightly couples subclasses to the base class. In production, prefer Strategy (composition) when the varying parts are independent; use Template Method when the steps must follow a fixed sequence.
  • ⚠️ Can be hard to understand which methods are hooks vs required overrides. Document clearly with abstract (must override) vs default implementations (optional hooks).

Fitness Test

"Do my subclasses follow the same steps in the same order, but differ in specific details within those steps?"

  • Yes → ✅ Template Method fits
  • No, the steps themselves differ → ❌ Use Strategy (composition) instead
flowchart TB
    A[DataMiner] -->|1| B["extractData()"]
    A -->|2| C["parseData()"]
    A -->|3| D["analyzeData()"]
    A -->|4| E["sendReport()"]
    F[CsvMiner] -.->|overrides| B
    F -.->|overrides| C
abstract class DataMiner {
    // Template method — defines the algorithm skeleton
    mine(path: string): void {
        const raw = this.extractData(path);
        const parsed = this.parseData(raw);
        const analyzed = this.analyzeData(parsed);
        this.sendReport(analyzed);
    }

    abstract extractData(path: string): string;
    abstract parseData(rawData: string): object[];

    analyzeData(data: object[]): string {
        return `Analyzed ${data.length} records`;
    }

    sendReport(analysis: string): void {
        console.log(`Report: ${analysis}`);
    }
}

class CsvMiner extends DataMiner {
    extractData(path: string): string {
        return `CSV data from ${path}`;
    }

    parseData(rawData: string): object[] {
        return rawData.split("\n").map(line => ({ line }));
    }
}

class JsonMiner extends DataMiner {
    extractData(path: string): string {
        return `JSON data from ${path}`;
    }

    parseData(rawData: string): object[] {
        return [{ raw: rawData }];
    }
}

new CsvMiner().mine("data.csv");   // Report: Analyzed 1 records
new JsonMiner().mine("data.json"); // Report: Analyzed 1 records

Visitor

Lets you add new operations to existing object structures without modifying them. Use when you have a stable set of element types but frequently need to add new operations — e.g., AST processing, document export, or tax calculation across product types.

Analogy

Like a building inspector visiting different properties — they apply the same "inspection" operation but check completely different things at a restaurant (kitchen hygiene, fire exits) vs. an office (electrical panels, emergency routes) vs. a warehouse (structural load, ventilation). The buildings don't change; the inspector adds new operations to them. Compilers use this to walk an AST: one visitor for type-checking, another for code generation, another for optimization.

When to Use

  • You need to add operations to a stable set of element types without modifying them — e.g., compiler AST visitors for type-checking, optimization, and code generation
  • Different operations need completely different logic per element type — e.g., a tax visitor computing VAT differently for food, electronics, and services
  • The element hierarchy is stable but operations change frequently — e.g., new export formats, new report types, new static analysis rules

Trade-offs

  • ✅ Add new operations without modifying element classes — Open/Closed for operations
  • ✅ Related behavior grouped in one visitor class — not scattered across element types
  • ⚠️ Adding a new element type requires updating every visitor. Acceptable when element types are stable (AST nodes, document elements); painful when the element hierarchy evolves frequently.
  • ⚠️ Breaks encapsulation — visitors need access to element internals. In production, expose only what visitors need via a public interface rather than making all fields public.

Fitness Test

"Do I keep adding new operations to a stable set of element types, and modifying those types each time is painful?"

  • Yes → ✅ Visitor fits
  • No, element types change often → ❌ Visitor will fight you — use polymorphism
flowchart LR
    A[Circle] -->|accept| C[AreaCalculator]
    B[Rectangle] -->|accept| C
    C -->|visitCircle| A
    C -->|visitRectangle| B
interface ShapeVisitor {
    visitCircle(circle: Circle): string;
    visitRectangle(rect: Rectangle): string;
}

interface VisitableShape {
    accept(visitor: ShapeVisitor): string;
}

class Circle implements VisitableShape {
    constructor(public radius: number) {}

    accept(visitor: ShapeVisitor): string {
        return visitor.visitCircle(this);
    }
}

class Rectangle implements VisitableShape {
    constructor(public width: number, public height: number) {}

    accept(visitor: ShapeVisitor): string {
        return visitor.visitRectangle(this);
    }
}

// New operation — no changes to shape classes
class AreaCalculator implements ShapeVisitor {
    visitCircle(circle: Circle): string {
        return `Circle area: ${(Math.PI * circle.radius ** 2).toFixed(2)}`;
    }

    visitRectangle(rect: Rectangle): string {
        return `Rectangle area: ${rect.width * rect.height}`;
    }
}

const shapes: VisitableShape[] = [new Circle(5), new Rectangle(4, 6)];
const calculator = new AreaCalculator();
shapes.forEach(s => console.log(s.accept(calculator)));
// Circle area: 78.54
// Rectangle area: 24

Interpreter (GoF)

Defines a grammar for a language and provides an interpreter to evaluate sentences in that language. Use for simple scripting languages, rule engines, regular expressions, or mathematical expression parsers. This pattern is part of the original Gang of Four (GoF) catalog but is often omitted from modern references due to its limited practical use.

Analogy

Like how your brain parses "2 + 3 × 4" — you unconsciously build a tree where multiplication binds tighter than addition, then evaluate bottom-up. The expression is a mini-language with grammar rules, and your mental math is the interpreter. SQL engines do this at scale: they parse your query string into an AST, optimize it, then interpret each node.

When to Use

  • You need to evaluate sentences in a simple, well-defined grammar — e.g., mathematical expression evaluators, config DSLs, template engines like Handlebars
  • The grammar is small and performance isn't critical — e.g., business rule engines evaluating "if cart > $100 AND customer is premium"
  • You want a tree-based representation that can be evaluated, optimized, or transformed — e.g., SQL query parsing, regex pattern matching

Trade-offs

  • ✅ Easy to implement for simple grammars — one class per grammar rule
  • ✅ Easy to extend — add new expression types by adding new classes
  • ⚠️ Doesn't scale to complex grammars — class proliferation, slow evaluation. In production, use a proper parser generator (ANTLR, PEG.js) for anything beyond ~10 grammar rules.
  • ⚠️ Hard to maintain as the grammar evolves. Acceptable for internal DSLs that rarely change; for user-facing languages, invest in a real parser/compiler architecture.

Fitness Test

"Am I evaluating expressions in a small, well-defined grammar with fewer than ~10 rules?"

  • Yes → ✅ Interpreter fits
  • No, the grammar is complex → ❌ Use a parser generator (ANTLR, PEG.js)
flowchart TB
    A["Multiply (*)"] --> B["Add (+)"]
    A --> C["Variable (y)"]
    B --> D["Variable (x)"]
    B --> E["Number (3)"]
interface Expression {
    interpret(context: Map<string, number>): number;
}

class NumberExpression implements Expression {
    constructor(private value: number) {}

    interpret(_context: Map<string, number>): number {
        return this.value;
    }
}

class VariableExpression implements Expression {
    constructor(private name: string) {}

    interpret(context: Map<string, number>): number {
        return context.get(this.name) ?? 0;
    }
}

class AddExpression implements Expression {
    constructor(private left: Expression, private right: Expression) {}

    interpret(context: Map<string, number>): number {
        return this.left.interpret(context) + this.right.interpret(context);
    }
}

class MultiplyExpression implements Expression {
    constructor(private left: Expression, private right: Expression) {}

    interpret(context: Map<string, number>): number {
        return this.left.interpret(context) * this.right.interpret(context);
    }
}

// Expression: (x + 3) * y
const expression = new MultiplyExpression(
    new AddExpression(
        new VariableExpression("x"),
        new NumberExpression(3)
    ),
    new VariableExpression("y")
);

const context = new Map<string, number>([["x", 5], ["y", 2]]);
console.log(expression.interpret(context)); // (5 + 3) * 2 = 16