A subject (model, sensor, store) calls each of its consumers by name when its state changes. Adding a new consumer means editing the subject's constructor (to receive the consumer) and its mutator (to call it). The subject changes for unrelated reasons each time a new consumer ships.
Per-consumer references inside the subject are N×M cells (N subjects × M consumers each) the agent verifies on every consumer-set change. Adding a consumer forces the agent to edit subject + caller in lockstep; the subject's diff history conflates unrelated concerns.
The subject exposes subscribe(observer) and an internal observer list. State-change methods iterate the list and notify each observer through a uniform notify() interface. Adding a consumer is a subscribe call; the subject stays unedited.
One subscribe/notify protocol the agent reads once per subject. Consumer registration is one expression at the call site; subject edits do not scale with consumer count. Static analysis of 'who subscribes to this subject' is grep-able and complete.
Before the pattern
class TemperatureSensor {constructor(currentDisplay, chartDisplay, alertSystem) {this.currentDisplay = currentDisplay;this.chartDisplay = chartDisplay;this.alertSystem = alertSystem;this.temperature = 0;}setTemperature(t) {this.temperature = t;this.currentDisplay.update(t);this.chartDisplay.recordSample(t);if (t > 80) this.alertSystem.fire('high temperature');}}// Adding a mobile-push consumer or a CSV-logger consumer requires editing// the sensor's constructor and setTemperature in lockstep. Subject changes// for unrelated reasons; one consumer's bug forces a sensor redeploy.
After the pattern
class TemperatureSensor {constructor() {this.observers = [];this.temperature = 0;}subscribe(observer) {this.observers.push(observer);return () => {this.observers = this.observers.filter((o) => o !== observer);};}setTemperature(t) {this.temperature = t;for (const observer of this.observers) {observer.notify(t);}}}const sensor = new TemperatureSensor();sensor.subscribe({ notify: (t) => currentDisplay.update(t) });sensor.subscribe({ notify: (t) => chartDisplay.recordSample(t) });sensor.subscribe({ notify: (t) => { if (t > 80) alertSystem.fire('high temperature'); }});
Per-consumer wiring inside the subject is Shotgun Surgery on every consumer addition and Divergent Change on the subject. The subject's git history accumulates 'add X consumer' entries that have nothing to do with the subject's actual responsibilities (sensing, storing, computing).
Hard-coded subject→consumer wiring forces the agent to verify each consumer's contract on every subject edit. The diff blast radius for a new consumer is unpredictable; the verification budget grows linearly with consumer count.
Notification order is implicit in subscription order; observers that depend on each other for correct state (e.g., one updates a cache that another reads) become order-sensitive in a way the subject's API does not document. Memory leaks from forgotten unsubscribes are the classic Observer footgun.
Notify dispatch order and re-entrancy semantics are runtime-only properties the agent cannot statically derive. Observers that depend on dispatch order produce flaky test failures; the agent investigating one must trace runtime subscription order, which is invisible in the source.
Subject's responsibilities are pure subject-things (state + notification dispatch); consumers register from wherever; new consumers ship without subject edits; testing the subject covers notification dispatch once, consumers test independently.
Subject edits scope to one file; consumer additions are zero-edit on the subject; static analysis of subscriber set is complete by grep. Tests for the subject cover notification dispatch exhaustively without per-consumer setup.
Observers that mutate the subject during notify() (re-entrant state changes), or whose notify methods throw, produce subtle bugs in observer ordering and re-entrancy. The pattern's promise breaks down without explicit re-entrancy and error-handling discipline; document the rules or watch them get violated quietly.
Re-entrancy bugs (observer A's notify() triggers a state change that re-fires notify() on A) and silent unsubscribe leaks are runtime-only failure modes. The agent reading subscribe/notify trusts the contract; runtime ordering and memory bugs survive review and surface as flaky behaviour in production.