Low-level designCore8 min
Observer
Let an object announce its changes to a list of listeners it doesn't know by name. Anything that cares subscribes, so a new reaction is a new listener, and the object that changed never has to be edited.
A tank and three reactions
Tank calls Display, Alarm and BrewLog on every reading. It holds each one by class.
1 · The situation
A fermentation tank and three things that care
A small brewery reads the temperature of each fermentation tank every few minutes. Three things react to each reading: a Display on the tank shows it, an Alarm sounds above 24 °C, and a BrewLog records it for the batch history.
The first version has Tank call all three directly in setTemperature(). It's given them in its constructor and knows each one's class and method. Python writes set_temperature().
class Display {
void show(String tank, int celsius) {
System.out.println("Display: " + tank + " at " + celsius + " C");
}
}
class Alarm {
private final int limit;
Alarm(int limit) {
this.limit = limit;
}
void check(String tank, int celsius) {
if (celsius > limit) {
System.out.println("ALARM: " + tank + " is at " + celsius + " C");
}
}
}
class BrewLog {
void record(String tank, int celsius) {
System.out.println("Log: " + tank + ", " + celsius + " C");
}
}
class Tank {
private final String name;
private final Display display;
private final Alarm alarm;
private final BrewLog log;
Tank(String name, Display display, Alarm alarm, BrewLog log) {
this.name = name;
this.display = display;
this.alarm = alarm;
this.log = log;
}
void setTemperature(int celsius) {
display.show(name, celsius);
alarm.check(name, celsius);
log.record(name, celsius);
}
}
public class Main {
public static void main(String[] args) {
Tank tank = new Tank("Tank 1", new Display(), new Alarm(24), new BrewLog());
for (int celsius : new int[] {19, 21, 25}) tank.setTemperature(celsius);
}
}The class diagram of this version, drawn from its code: the tank holds each of the three, by class.
Three readings, the last one too warm:
Display: Tank 1 at 19 C Log: Tank 1, 19 C Display: Tank 1 at 21 C Log: Tank 1, 21 C Display: Tank 1 at 25 C ALARM: Tank 1 is at 25 C Log: Tank 1, 25 C
2 · The change
Every new reaction edits the tank
The head brewer wants a text message when a tank runs warm, but only while someone is on shift. In this design that means editing Tank again:
class Tank {
private final String name;
private final Display display;
private final Alarm alarm;
private final BrewLog log;
Tank(String name, Display display, Alarm alarm, BrewLog log) {
this.name = name;
this.display = display;
this.alarm = alarm;
this.log = log;
}
void setTemperature(int celsius) {
display.show(name, celsius);
alarm.check(name, celsius);
log.record(name, celsius);
}
}Tank measures temperature, and it has become the place that knows everyone who cares about temperature. Each new reaction costs an edit to a class that has nothing to do with it:
Request | Change to |
|---|---|
Text the brewer when it runs warm | A new field, a constructor parameter and a call |
Text only while someone is on shift | A flag, and a check around that call |
Turn the display off while the tank is cleaned | Another flag |
A second tank without an alarm | A null check, or a fake alarm |
The dependency points the wrong way. The display, the alarm and the log depend on the tank's temperature; the tank shouldn't depend on them. It only needs to say "my temperature changed", to whoever is listening.
3 · The refactor
Let the tank announce, and let listeners sign up
Start from the first version. Give everything that reacts one interface, and let the tank keep a list of them instead of a field for each:
TemperatureListener declares temperatureChanged(tank, celsius). Display, Alarm and BrewLog each implement it, doing what they did before.
interface TemperatureListener {
void temperatureChanged(String tank, int celsius);
}
class Alarm implements TemperatureListener {
private final int limit;
Alarm(int limit) {
this.limit = limit;
}
public void temperatureChanged(String tank, int celsius) {
if (celsius > limit) {
System.out.println("ALARM: " + tank + " is at " + celsius + " C");
}
}
}After the second step the program prints exactly what it printed before. After the third, Mia gets a text at 25 °C, and none at 26 °C once she's off shift:
Display: Tank 1 at 19 C Log: Tank 1, 19 C Display: Tank 1 at 21 C Log: Tank 1, 21 C Display: Tank 1 at 25 C ALARM: Tank 1 is at 25 C Log: Tank 1, 25 C Text to Mia: Tank 1 is too warm (25 C) Mia goes off shift Display: Tank 1 at 26 C ALARM: Tank 1 is at 26 C Log: Tank 1, 26 C
Edits to Tank for the four requests in the table above. With listeners, each is a new class or a call to subscribe() or unsubscribe().
4 · The pattern
Observer: a subject and its subscribers
Observer lets one object, the subject, keep a list of dependents, the observers, and notify each of them when its state changes, by calling a method of a shared interface. Observers subscribe and unsubscribe at run time, and the subject knows them only through that interface.
The roles in the brewery's code: Tank is the subject, TemperatureListener the observer interface, and each reaction a concrete observer.
Role | In the brewery | Its job |
|---|---|---|
Subject |
| Keeps the list, and notifies it when its state changes |
Observer |
| The interface the subject calls |
Concrete observer |
| Reacts in its own way |
Client |
| Subscribes and unsubscribes observers |
One change, one call per subscriber, in the order they subscribed.
The subject can push what changed, as Tank passes the tank name and the temperature, or let observers pull it by passing only itself and letting each ask for what it needs. Pushing keeps observers simple; pulling suits observers that each need different data.
5 · What it costs
Who runs when is no longer in one place
- The flow is harder to follow. Reading
setTemperature()no longer tells you what happens on a reading; you have to find who subscribed, and where. - Order and failure are easy to get wrong. Observers run in subscription order, one after another. A slow one delays the rest, and an exception from one stops the ones after it unless the subject catches it.
- Forgotten observers leak. The subject holds a reference to every observer. An observer that's never unsubscribed is kept alive, and keeps receiving calls, after the code that made it is done with it.
- Cascades. An observer that changes another subject's state can set off a chain of notifications, sometimes back to where it started.
Situation | Observer? |
|---|---|
Several independent reactions to one kind of change, which come and go | Yes |
Exactly one thing reacts, and always will | No: call it directly |
The reactions must run in a strict order, or as one transaction | No: call them in order from one place |
Senders and receivers are in different processes | Use a message broker: publish-subscribe |
6 · Getting it right
Unsubscribe, and let one listener fail alone
- Always offer
unsubscribe(), and use it. Tie a subscription to the lifetime of the observer: unsubscribe when a screen closes or a shift ends. - Don't change the list while walking it. An observer that unsubscribes during a notification can skip the next one or throw. Walk a copy, or queue the change until the notification is done.
- Contain failures. Catch an exception from one observer, record it, and carry on with the rest, so a broken display doesn't silence the alarm.
- Notify after the state is consistent. Change all of the subject's fields first, then notify, so observers that pull see a finished state.
Language | The usual shape |
|---|---|
Java | A listener interface and a |
C++ | A vector of non-owning pointers to an abstract listener, or of |
JavaScript | Functions rather than objects: browsers provide |
Python | A list of objects matching a |
7 · Look-alikes
Announcing, routing and passing along
Several patterns send one message to objects the sender doesn't know. What differs is who knows whom:
Pattern | Who knows whom | Typical use |
|---|---|---|
Observer | The subject holds its observers, through one interface | Reactions to one object's changes, in one program |
Publish-subscribe | Neither side knows the other; a broker routes messages by topic | Events across services or processes, often asynchronous |
Mediator | Colleagues know only the mediator, which decides who does what | Coordinating many objects that would otherwise call each other |
Chain of Responsibility | Each handler knows the next | One request, handled by one of several handlers |
An in-app event bus sits between Observer and publish-subscribe: subjects post events to the bus by type, and listeners subscribe to types, so neither holds the other. It decouples more, at the price of making the flow even harder to trace.
8 · In practice
Observers you have already used
Every browser event is an Observer. addEventListener() registers a function to be called whenever a given event is delivered to an element, and removeEventListener() removes it, given the same function. A button doesn't know what its click handlers do; it just calls each of them.
The practice checks recognize an Observer when a class holds a collection of an interface, lets callers add to it, and loops over it calling the interface's method. A subject with no way to unsubscribe is reported as missing a part, since its observers can never leave.
Check yourself
4 questions
Takeaways
Remember this
- Observer lets a subject notify a list of observers without knowing their classes.
- Observers subscribe and unsubscribe at run time; a new reaction is a new observer.
- The dependency points the right way: reactions depend on the subject, not the reverse.
- Observers run in turn: a slow or failing one affects the rest unless the subject guards against it.
- Unsubscribe what you subscribe, or the subject keeps observers alive.