Design Patterns: Observer Pattern

These notes were adapted from various readings detailed in the References section.


The Observer Pattern defines a one-to-many dependency between objects so that when one object changes state, all of its dependents are notified and updated automatically.

Suppose we are tasked with building the next generation internet-based weather monitoring station. It’s 2020, we have to put everything up on the cloud, right?

The specific requirements for the monitor is that system should be able to track the current conditions, mainly: temperature, humidity, and barometric pressure.

You have decided that a WeatherData object should exist and that it should somehow know how to communicate with the weather station via a pull mechanism. So, we get the data by making requests from the weather station.

We work to define the WeatherData interface like so:

class WeatherData {
    public getTemperature { ... }
    public getHumidity() { ... }
    public getPressure() { ... }
    public measurementsChanged() { ... }
}

There are a variety of displays in which we want to read data acquired byWeatherData . These displays all serve a different purpose in that they provide 3 separate views:

  1. Current Conditions Display
  2. Statistics Display
  3. Forecast Display

Our job is to implement measurementsChanged so that it updates the three displays for current conditions, weather statistics, and the forecast. It is called any time new weather measurement data is available.

Here is our first pass at an implementation:

class WeatherData {
    ...
    
    public measurementsChanged() {
        const temp = getTemperature();
        const humidity = getHumidity();
        const pressure = getPressure();
        
        currentConditionsDisplay.update(temp, humidity, pressure);
        statisticsDisplay.update(temp, humidity, pressure);
        forecastDisplay.update(temp, humidity, pressure);
    }
}

We fail to realize that the WeatherData must be expandable and resilient to changing requirements. What if there are new displays interested in the weather data provided by WeatherData? What if displays are removed?

The update calls are effectively hardcoded. This makes it difficult to add, or remove other display elements without being forced to make changes to the program.

One thing we both notice is that each display shares some common interface in that the update method is available. Let’s figure out how to encapsulate those update calls.

We can use the observer pattern.

The observer pattern is made up of publishers, and subscribers. Sometimes we can refer to publishers as the subject and the subscribers as the observers. The subject will know who the observers are at any instance, and push updates to them when some event happens.

We can model this type of architecture like so:

interface Subject {
    registerObserver();
    removeObserver();
    notifyObservers();
}

interface Observer {
    update();
}

class ConcreteSubject implements Subject {
    public registerObserver() { ... }
    public removeObserver() { ... }
    public notifyObservers() { ... }
    
    public getState() { ... }
    public setState() { ... }
}

class ConcreteObserver implements Observer {
    public update() { ... }
    
    // ... other methods
}

We can see that an Observer should implement an update method in which the Subject should be aware of in order to push its updates to. The subject should implement methods that can manage a list of Observers so that it can send out its updates to the observers as needed. This is whatregisterObserver, removeObserver and notifyObservers all implement.

The observer pattern allows for loose coupling for objects interested in receiving messages from another (subject) in a one-to-many relational manner.

Now, let’s reshape our WeatherData system to fit this type of pattern.

interface DisplayElement {
    display();
}

class WeatherData implements Subject {
    private observers = [];
    
    public registerObserver() { ... }
    public removeObserver() { ... }
    public notifyObservers() { ... }
    
    public getTemperature { ... }
    public getHumidity() { ... }
    public getPressure() { ... }
    
    public measurementsChanged() {
        ... // To implement
    }
}

class CurrentConditionsDisplay implements Observer, DisplayElement {
    public update() { ... }
    public display() { ... }
}

class StatisticsDisplay implements Observer, DisplayElement {
    public update() { ... }
    public display() { ... }
}

class ForecastDisplayimplements Observer, DisplayElement {
    public update() { ... }
    public display() { ... }
}

class ThirdPartyDisplay implements Observer, DisplayElement {
    public update() { ... }
    public display() { ... }
}

WeatherData can maintain an internal array of observers where registerObserver can add a display into the list. removeObserver will remove the display from this list, and notifyObservers can iterate through the list by calling the update method of the subscribed display.

When WeatherData has received new data, it can now push that new state to all registered observers by iterating through them within notifyObservers. We can simply just call measurementsChanged internally for this.

In addition to all this, we can also decide how we want to set the state of the observer through other custom implementations.

References

  • Head First Design Patterns - Eric Freeman & Elisabeth Freeman - https://www.oreilly.com/library/view/head-first-design/0596007124/