LLevelUp
0
← Back to topic

Behavioral Patterns

Behavioral patterns define how objects communicate and collaborate. They shift focus from structure to flow — how responsibilities are distributed, how algorithms are encapsulated, how sequences are traversed. Several behavioral patterns are so deeply embedded in JavaScript that developers use them daily without recognising them: the event listener model is Observer, for...of is Iterator, and Array.prototype.map is a consumer of the Iterator protocol.

Observer

Problem: when one object changes state, an unknown number of other objects need to react — but the source should not be coupled to those dependents.

Solution: define a Subject that maintains a list of Observers. Observers register interest; the Subject notifies all of them when it changes.

sequenceDiagram
    participant Store
    participant ComponentA
    participant ComponentB
    ComponentA->>Store: subscribe(listener)
    ComponentB->>Store: subscribe(listener)
    Note over Store: state changes
    Store->>ComponentA: notify(newState)
    Store->>ComponentB: notify(newState)
type Listener<T> = (value: T) => void;

class Observable<T> {
  private listeners = new Set<Listener<T>>();
  private value: T;

  constructor(initial: T) { this.value = initial; }

  subscribe(fn: Listener<T>): () => void {
    this.listeners.add(fn);
    return () => this.listeners.delete(fn); // returns unsubscribe
  }

  set(newValue: T) {
    this.value = newValue;
    this.listeners.forEach(fn => fn(newValue));
  }

  get() { return this.value; }
}

const count = new Observable(0);
const unsub = count.subscribe(v => console.log("count:", v));
count.set(1); // count: 1
count.set(2); // count: 2
unsub();      // unsubscribed — no more logs

NOTE

EventTarget.addEventListener is Observer built into the browser. React’s useState + useEffect, Nanostores, Zustand, and RxJS Observables are all variations on this pattern. The key insight is that the Subject owns the what changed but not the what to do about it.

Strategy

Problem: a class needs to perform a task that can be done in several ways, and the algorithm should be swappable at runtime.

Solution: define a Strategy interface for the algorithm family and inject a concrete Strategy into the Context class.

interface SortStrategy<T> {
  sort(data: T[]): T[];
}

class QuickSort<T> implements SortStrategy<T> {
  sort(data: T[]) { return [...data].sort(); } // simplified
}

class BucketSort<T extends number> implements SortStrategy<T> {
  sort(data: T[]) { /* bucket sort impl */ return data; }
}

class DataGrid<T> {
  constructor(
    private data: T[],
    private strategy: SortStrategy<T>
  ) {}

  setStrategy(s: SortStrategy<T>) { this.strategy = s; }
  render() { return this.strategy.sort(this.data); }
}

const grid = new DataGrid([3, 1, 2], new QuickSort());
grid.render(); // [1, 2, 3]
grid.setStrategy(new BucketSort());
grid.render(); // also [1, 2, 3], different algorithm

Strategy is everywhere in production TypeScript: authentication strategies in Passport.js, sorting/filtering logic in table components, validation schemas, and form submission handlers.

Command

Problem: you want to encapsulate a request as an object so you can queue it, log it, undo it, or retry it.

Solution: define a Command interface with execute() (and optionally undo()). A CommandHistory can maintain a stack of executed commands.

interface Command {
  execute(): void;
  undo(): void;
}

class TextEditor {
  private content = "";
  getContent() { return this.content; }
  insert(text: string) { this.content += text; }
  delete(n: number) { this.content = this.content.slice(0, -n); }
}

class InsertCommand implements Command {
  constructor(private editor: TextEditor, private text: string) {}
  execute() { this.editor.insert(this.text); }
  undo() { this.editor.delete(this.text.length); }
}

class History {
  private stack: Command[] = [];
  execute(cmd: Command) { cmd.execute(); this.stack.push(cmd); }
  undo() { this.stack.pop()?.undo(); }
}

const editor = new TextEditor();
const history = new History();
history.execute(new InsertCommand(editor, "Hello"));
history.execute(new InsertCommand(editor, " World"));
// editor.getContent() === "Hello World"
history.undo();
// editor.getContent() === "Hello"

TIP

Ctrl+Z in every text editor, spreadsheet, and graphic tool is implemented with Command + History. In frontend state management, Redux actions are Commands — pure data objects describing what happened, applied to state by reducers.

Iterator

Problem: you want to traverse a collection without exposing its internal structure (array, linked list, tree, database cursor).

Solution: define an Iterator with next() returning { value, done }. In JavaScript/TypeScript this is the built-in Iterator protocol — any object with a [Symbol.iterator]() method works with for...of, spread, destructuring, and Array.from.

class Range {
  constructor(private start: number, private end: number) {}

  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    return {
      next(): IteratorResult<number> {
        if (current <= end) {
          return { value: current++, done: false };
        }
        return { value: undefined as unknown as number, done: true };
      }
    };
  }
}

const range = new Range(1, 5);
for (const n of range) console.log(n); // 1 2 3 4 5
const arr = [...range]; // [1, 2, 3, 4, 5]

Generators (function*) are syntax sugar for implementing iterators without the boilerplate. Node.js streams, the DOM NodeList, Map, Set, and String are all iterable natively.

NOTE

JavaScript also has AsyncIterator ([Symbol.asyncIterator]) for for await...of loops, used in streaming APIs, database cursors, and async generators — the same pattern, extended for asynchronous sequences.

Further Learning

Search these terms to go deeper:

  • “Design Patterns GoF behavioral chapter” — the original Observer, Strategy, Command, Iterator, plus Chain of Responsibility, Mediator, Memento, State, Template Method, Visitor
  • “JavaScript Iterator protocol MDN Symbol.iterator” — the native protocol and how to implement custom iterables
  • “Redux architecture command pattern” — how Redux actions, reducers, and middleware map to Command + History
  • “RxJS reactive programming Observer” — Observer taken to its logical extreme with composable pipelines
  • “Passport.js strategy pattern” — real-world Strategy: pluggable authentication algorithms