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.

Watch it evolve1 / 10
The problem

A tank and three reactions

Tank calls Display, Alarm and BrewLog on every reading. It holds each one by class.

80
Start
Stage 1
Stage 2
Final

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().

The first versionJava
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);
    }
}
Display~ show(String, int)Alarm− limit: int~ check(String, int)BrewLog~ record(String, int)Tank− name: String− display: Display− alarm: Alarm− log: BrewLog~ setTemperature(int)

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:

Every reaction is wired into the tankJava
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 Tank today

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.

Java
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");
        }
    }
}
*«interface»TemperatureListener+ temperatureChanged(String, int)Display+ temperatureChanged(String, int)Alarm− limit: int+ temperatureChanged(String, int)BrewLog+ temperatureChanged(String, int)Tank− name: String− listeners: List<TemperatureListener>~ subscribe(TemperatureListener)~ unsubscribe(TemperatureListener)~ setTemperature(int)

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
Reactions wired into Tank
4
Reactions as listeners
0

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.

*«interface»TemperatureListener+ temperatureChanged(String, int)Display+ temperatureChanged(String, int)Alarm− limit: int+ temperatureChanged(String, int)BrewLog+ temperatureChanged(String, int)PhoneAlert− person: String− limit: int+ temperatureChanged(String, int)Tank− name: String− listeners: List<TemperatureListener>~ subscribe(TemperatureListener)~ unsubscribe(TemperatureListener)~ setTemperature(int)

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

Tank

Keeps the list, and notifies it when its state changes

Observer

TemperatureListener

The interface the subject calls

Concrete observer

Display, Alarm, BrewLog, PhoneAlert

Reacts in its own way

Client

main

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 List of them; many libraries name the methods addXListener() and removeXListener()

C++

A vector of non-owning pointers to an abstract listener, or of std::function callbacks; the subscriber must unsubscribe before it is destroyed

JavaScript

Functions rather than objects: browsers provide addEventListener() and removeEventListener(), which take a callback

Python

A list of objects matching a Protocol, or of plain functions

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

1. The brewery adds a dashboard on the office screen that charts every reading. What changes?
2. The display throws an exception on one reading. What happens to the alarm, which subscribed after it?
3. A screen subscribes a listener each time it opens, and never unsubscribes. What happens over a day?
4. How does Observer differ from publish-subscribe?

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.