Replace Hard-Coded Notifications With Observer

Destination
Compare
Symptom
Human

A class hardcodes calls to specific collaborators whenever something happens — `this.inventory.reserve(...)`, `this.analytics.recordAdd(...)`. Adding a new interested party means editing the publisher; the publisher's responsibilities expand with every new consumer; tests must mock every downstream collaborator.

Agent

Publisher methods the agent must trace through to know what side effects occur. Adding a new consumer requires editing the publisher; per-edit context cost grows with consumer count; tests for the publisher load every consumer's mock.

Goal
Human

The publisher emits events; listeners subscribe at the composition root and receive notifications without the publisher knowing they exist. Adding a consumer is a new listener + one subscription line, with no publisher edit.

Agent

The publisher is short and consumer-agnostic; the agent reads it once and trusts it. Each consumer is one file with one event-handling concern; the agent verifies each consumer in isolation.

Before the refactoring

// Cart knows which collaborators to notify and how.
class Cart {
constructor(inventory, analytics, shipping) {
this.items = [];
this.inventory = inventory;
this.analytics = analytics;
this.shipping = shipping;
}
addItem(item, quantity) {
this.items.push({ item, quantity });
this.inventory.reserve(item, quantity);
this.analytics.recordAdd(item, quantity);
this.shipping.updateEstimate(this.items);
}
}

After the refactoring

// Cart publishes events; listeners subscribe at the composition root.
class Cart {
constructor() {
this.items = [];
this.listeners = [];
}
subscribe(listener) {
this.listeners.push(listener);
}
addItem(item, quantity) {
this.items.push({ item, quantity });
this.notify({ type: 'item-added', item, quantity, cart: this });
}
notify(event) {
this.listeners.forEach((listener) => listener.handle(event));
}
}
class InventoryListener {
constructor(inventory) { this.inventory = inventory; }
handle(event) {
if (event.type === 'item-added') this.inventory.reserve(event.item, event.quantity);
}
}
class AnalyticsListener {
constructor(analytics) { this.analytics = analytics; }
handle(event) {
if (event.type === 'item-added') this.analytics.recordAdd(event.item, event.quantity);
}
}
class ShippingListener {
constructor(shipping) { this.shipping = shipping; }
handle(event) {
if (event.type === 'item-added') this.shipping.updateEstimate(event.cart.items);
}
}
const cart = new Cart();
cart.subscribe(new InventoryListener(inventory));
cart.subscribe(new AnalyticsListener(analytics));
cart.subscribe(new ShippingListener(shipping));
Example source: Illustrative example written for this site, faithful to Kerievsky's pattern shape in Refactoring to Patterns (Addison-Wesley, 2004), chapter 10. The book uses a Stock class with hard-coded calls to InvestmentTracker and PortfolioView; this JavaScript version uses a Cart with three listeners — same pattern, e-commerce host.
Pressure
Human

Hard-coded notifications are Insider Trading — the publisher knows too much about who cares about what, and tests can't isolate the publisher from its consumers. Each new consumer couples the publisher to more of the domain it shouldn't have to know.

Agent

Hard-coded notifications couple the publisher to the full consumer surface. The agent's per-publisher-edit verification cost scales with consumer count; static analysis cannot enforce that the publisher's events fully describe its state changes.

Tradeoff
Human

Observer scatters the system's reactive logic across loosely-coupled listeners; the agent or developer must read the subscription site to know what runs when an event fires. Event ordering becomes implicit; debugging requires recovering subscription order. Memory leaks via uncleaned subscriptions are a real failure mode.

Agent

Observer's dynamic dispatch defeats static call-graph analysis at the event boundary. The agent cannot statically determine which listeners fire on which events without reading the subscription wiring; ordering assumptions are invisible in the code.

Relief
Human

The publisher's responsibilities shrink to 'do the work, announce completion'; consumers attach without modifying the publisher. New consumers don't risk breaking existing ones; tests for the publisher mock no consumers, tests for consumers exercise event handling in isolation.

Agent

Adding a consumer is one new observer class that subscribes to the publisher's protocol; publisher tests do not load consumer mocks, and the publisher's signature stays fixed across additions.

Trap
Human

An Observer system without explicit event types or ordering guarantees turns into a debugging nightmare — the agent must trace 'who listens to what, in what order' across the whole subscription graph. The pattern earns its keep when subscribers are stable, semantic event types are documented, and ordering doesn't matter.

Agent

Subscription wiring scattered across the composition root requires the agent to grep for `subscribe(` calls to enumerate the consumer set. Stale subscriptions (uncleaned references) cause hard-to-debug memory and behaviour leaks the agent cannot detect statically.