<< Home

Chapter 12: How Patterns Interact

Patterns rarely live alone in real systems. They form small ecosystems where one pattern supplies structure, another supplies variation, and a third supplies coordination. This chapter explores several of these pairings. Each section focuses on how two patterns strengthen each other, why the pairing appears so often, and how to shape the interaction so it stays clear and maintainable.

Command and Memento

Command turns actions into objects, while Memento captures state so those actions can be reversed. Together they create undo and redo systems that behave predictably. The pairing works because actions and state snapshots complement each other; one describes what happened and the other preserves what must be restored.

Coordinated Roles

A Command stores just enough information to perform or reverse an operation. A Memento stores the internal state of the receiver. When used together, the command triggers changes and the memento anchors the state before and after those changes. This keeps the undo logic separate from business logic.

Compact Example

This demonstration uses simple placeholders inside braces to keep the focus on flow rather than implementation details. The command knows what to call; the memento holds the values needed to roll back the operation.

class Editor {
  constructor() { this.text = "" }
  type(s) { this.text += s }
  save() { return { text: this.text } }  // memento
  restore(m) { this.text = m.text }
}

class TypeCommand {
  constructor(ed, s) { this.ed = ed; this.s = s }
  execute() {
    this.before = this.ed.save()
    this.ed.type(this.s)
  }
  undo() { this.ed.restore(this.before) }
}
💡 Store only the minimum snapshot needed for rollback. Large snapshots slow down undo stacks and increase memory use.

Composite and Visitor

Composite represents hierarchical structures and lets clients treat groups and leaves uniformly. Visitor adds new operations to those structures without editing the existing classes. The interaction works because Composite exposes the shape of the tree and Visitor supplies pluggable behavior for walking it.

Why They Pair Well

Composite offers clean traversal. Visitor offers clean dispatch based on node type. When they meet, the traversal becomes predictable and the operations stay modular. You can add analytics, printing, exporting, or validation by swapping visitors without touching the tree structure.

Compact Example

This sketch shows nodes that accept a visitor. Internals that are not relevant are represented with … so that the mechanics of the interaction stay visible.

class Leaf {
  constructor(v) { this.v = v }
  accept(vis) { return vis.visitLeaf(this) }
}

class Group {
  constructor(children) { this.children = children }
  accept(vis) { return vis.visitGroup(this) }
}

class SumVisitor {
  visitLeaf(n) { return n.v }
  visitGroup(g) {
    return g.children.reduce((a, c) => a + c.accept(this), 0)
  }
}
⚠️ Visitor becomes heavy when node types change frequently. Each new node requires updates to every visitor.

Observer and Mediator

Observer broadcasts updates to subscribers, while Mediator centralises coordination among participants. Together they create systems with clear communication boundaries. Observer handles event flow; Mediator handles decision flow. This pairing appears wherever many components must stay informed without developing direct dependencies.

Balancing Broadcast and Control

Observer ensures that changes propagate. Mediator ensures that reactions remain orderly. Used together, they prevent the uncontrolled fan out that pure Observer setups sometimes suffer. The mediator becomes a traffic officer that listens to events and decides what should happen next.

Compact Example

This example shows a subject that emits events through simple callbacks. A mediator receives the event and coordinates the next steps. The inner details are trimmed where not essential.

class Subject {
  constructor() { this.subs = [] }
  subscribe(f) { this.subs.push(f) }
  setValue(v) {
    this.value = v
    this.subs.forEach(f => f(v))
  }
}

class Mediator {
  constructor() { this.count = 0 }
  onUpdate(v) {
    this.count += 1
    // coordinate …
  }
}

const s = new Subject()
const m = new Mediator()
s.subscribe(v => m.onUpdate(v))
💡 Use the mediator to keep event storms under control. It can throttle, batch, or filter notifications before they trigger real work.

Factory Method and Strategy

Factory Method selects which object to create, while Strategy selects how an object behaves. Combining them creates flexible systems where construction and behaviour vary independently. This pairing appears in configuration driven systems, plugin systems, and applications that support multiple execution modes.

Independent Axes Of Variation

The factory controls object type. The strategy controls behaviour. When you separate the two, you avoid nested conditionals that mix object selection with operational logic. The result is easier testing and a clearer mental model of the system.

Compact Example

This demonstration shows a factory that chooses between processors and a strategy that selects the algorithm each processor uses. The shapes are small so the flow remains clear, and placeholder content is marked with ….

class FastStrategy {
  run(xs) { return xs.reduce((a, b) => a + b, 0) }
}

class SafeStrategy {
  run(xs) { return xs.filter(x => x > 0).reduce((a, b) => a + b, 0) }
}

class Processor {
  constructor(strat) { this.strat = strat }
  process(xs) { return this.strat.run(xs) }
}

function makeProcessor(kind) {
  if (kind === "fast") return new Processor(new FastStrategy())
  return new Processor(new SafeStrategy())
}

// usage: makeProcessor("fast").process([1,2,3])
⚠️ Factories can become overloaded with configuration. Keep them small and let strategies capture variation so the selection logic remains simple.