Design Patterns · Behavioral Patterns
Observer
Notify interested subscribers when an event occurs. Make subscription lifetime, delivery order, and failure behavior explicit.
This lesson follows Memento. It also connects to the event coordination in Mediator.
Observer defines a subscription mechanism so a publisher can notify several interested objects. Subscribers can join and leave. The publisher depends on their notification contract. The cost is managing connections and the work triggered by an event.
Notify interested listeners
An editor updates a preview and an audit panel when text changes. Hard-coding both panels into the editor couples core editing to optional features. A subscriber list lets the client decide which features observe the editor.
type Changed = Readonly<{ text: string }>;
type Listener = (event: Changed) => void;
class ChangePublisher {
private listeners = new Set<Listener>();
subscribe(listener: Listener): () => void {
this.listeners.add(listener);
return () => { this.listeners.delete(listener); };
}
publish(text: string): void {
const event = Object.freeze({ text });
const recipients = [...this.listeners];
for (const listener of recipients) listener(event);
}
}
const changes = new ChangePublisher();
const unsubscribe = changes.subscribe(event => {
console.log("preview:", event.text);
});
changes.publish("draft");
unsubscribe();
changes.publish("revised"); // preview no longer receives events
This implementation calls listeners synchronously in registration order. Re-registering the same function is deduplicated by Set. It snapshots recipients at publish start, so changes to subscriptions apply to the next event. Other implementations can choose different rules. State them.
Change the subscription list
Try it
Choose who observes the next event
no subscribers
editing still succeeds
preview receives text = revised
audit receives text = revised
preview receives text = revised
audit receives text = revised
Step through
Give the subscription a lifetime
Connect when the panel opens
subscribe(listener) → unsubscribe function
The application registers an observer when the panel needs updates.
Publish after the state change
change editor state
publish the new text
Provide an event payload or let the observer read from the source. Choose whether the payload represents this event's state or whatever state exists later.
Disconnect when the panel closes
unsubscribe()
publisher drops its reference to the callback
A long-lived publisher can otherwise retain a callback and everything its closure captures.
Define delivery instead of assuming it
Observer does not provide durable delivery
The in-memory publisher has no persistence, retry, or replay. A slow synchronous listener delays publish(). In this sample, a throwing listener aborts the loop and later listeners miss the event. Isolate failures or keep fail-fast behavior deliberately.
| Decision | Why it matters |
|---|---|
| Synchronous or asynchronous | decides whether publisher work waits for listener work |
| Order and reentrant publication | prevents hidden dependencies and notification loops |
| Unsubscribe during an event | decides whether an already captured recipient still runs |
| Failure and cleanup | decides who misses a notification and what stays retained |
An event broker can implement a wider publish-subscribe design, but a broker adds its own delivery guarantees. Mediator owns cooperation policy. Observer owns the changing notification links. They can work together.
Check yourself
A synchronous listener throws halfway through the sample's publish loop. Who receives the event afterward?
Not quite. Observer alone does not supply retries or delivery isolation.
Correct. Failure behavior follows the actual publisher code. Isolation needs an explicit mechanism.
Map the roles
| Role | In this example | Job |
|---|---|---|
| Publisher | ChangePublisher | Issues events when its state changes or it performs some behavior. Holds the subscription list and lets subscribers join and leave. |
| Subscriber | the Listener function type | Declares the notification interface. Usually one update method, or here one callback, with parameters for event details. |
| Concrete subscribers | the preview and audit callbacks | React to notifications. They all follow the same interface, so the publisher is not coupled to their classes. |
| Client | the code that calls subscribe() | Creates publishers and subscribers separately, then registers subscribers for updates |
Check yourself
How can a subscriber get the data it needs about an event?
Correct. Both are common. Choose whether the payload reflects this event or whatever state exists later.
Not quite. Polling is what Observer replaces.
Reach for it when a change must reach an open-ended set of objects
- a change to one object's state may require changing other objects, and that set of objects is unknown in advance or changes at runtime. Graphical interfaces hit this constantly. A custom button class must let clients hook their own code to its click event without knowing that code ahead of time.
- some objects must observe others, but only for a limited time or in specific cases. The subscription list is dynamic, so subscribers can join and leave whenever they need to.
Check yourself
A reusable button component must run whatever code different apps attach to its click. Which Observer use is this?
Not quite. Duration is not the point here. The audience is unknown to the button's author.
Correct. Apps subscribe their own handlers, and the button notifies them without knowing their classes.
Implement it in seven steps
Refactor existing code toward the pattern in this order:
- Split your business logic in two: the core functionality, independent of other code, becomes the publisher, and the rest becomes subscriber classes.
- Declare the subscriber interface. At minimum it declares a single
updatemethod. - Declare the publisher interface with methods to add and remove subscribers. Publishers must work with subscribers only through the subscriber interface.
- Decide where the real subscription list and the subscribe methods live. Usually they go in an abstract class that concrete publishers extend. For an existing class hierarchy, use composition instead: put the subscription logic in a separate object that every real publisher uses, as
ChangePublisherdoes here. - Create the concrete publishers. Each time something important happens, a publisher notifies all its subscribers.
- Implement the notification method in the concrete subscribers. Most need context data, which the publisher can pass as an argument. Alternatively, the publisher passes itself and the subscriber reads what it needs. A less flexible option links a subscriber to one publisher permanently through its constructor.
- Have the client create all the subscribers and register them with the right publishers.
Check yourself
Your Editor already extends Document, so it cannot also extend an abstract publisher class. What does step 4 suggest?
Not quite. That duplicates exactly the code a helper can own.
Correct. A helper such as ChangePublisher holds the list, and the editor delegates subscribe and publish calls to it.
Name what it costs
| You gain | You pay |
|---|---|
| New subscriber classes without changing the publisher, and new publishers too if there is a publisher interface (open/closed) | Subscribers are notified in an order the design does not guarantee |
| Relations between objects can be set up at runtime | Callbacks create indirect dependencies that are hard to trace |
| Features connect only while they need updates | A forgotten unsubscribe keeps work and memory alive |
| One event can trigger multiple reactions | Failures and cascades of notifications need rules |
Check yourself
The audit panel must always run after the preview panel. Can you rely on subscription order?
Not quite. Many implementations, brokers especially, make no such promise.
Correct. Observer does not promise an order. This sample happens to use registration order, but a dependency between subscribers is a design smell.
Do not confuse it with its neighbors
| Pattern | How it relates |
|---|---|
| Chain of Responsibility, Command, Mediator | All four connect senders and receivers differently. Observer lets receivers dynamically subscribe to and unsubscribe from receiving requests. |
| Mediator | Mediator removes mutual dependencies among components, which all depend on one mediator. Observer builds dynamic one-way connections, where some objects act as subordinates of others. A common Mediator implementation uses Observer: the mediator publishes, and components subscribe. |
Check yourself
A dialog object subscribes to all its controls' events and decides how the others react. Is that only Observer?
Not quite. Patterns combine. The intent here is centralized coordination.
Correct. The subscriptions are the transport. The central object that owns the cooperation rules makes it a mediator.
Retrieve and apply
Check yourself
A preview closes, but its callback stays subscribed to a long-lived editor. What can happen?
Correct. The subscriber list owns a reference. Closing the visual panel does not remove that reference automatically.
Not quite. Visibility is not part of the pattern's subscription contract. The application must disconnect the listener.
State the delivery and cleanup rules for one observer in an application. Continue to State for behavior that changes with an object's lifecycle.
Source: Alexander Shvets, Dive Into Design Patterns (深入设计模式), Chinese edition v2021-1.25. Observer, printed pages 315–328. Explanations, examples, and exercises are adapted for this course.