Observer Design Pattern

Introduction

Observer is a behavioural design pattern that establishes a one-to-many dependency between objects. In this pattern, when one object (Subject) undergoes a change in state, all dependent objects (Observers) are automatically notified and updated. The actual set of dependent objects can be unknown beforehand or change dynamically.

So the beauty of the Observer pattern lies in its promotion of loose coupling between the subject and its observers, as they only depend on a common interface. Consequently, observers can be easily added or removed without modifying the subject. This will help us enhance maintainability, extensibility, and reusability by segregating the concerns of the subject and its observers.

Real life analogy

Suppose there is an online news delivery service and a group of subscribers who want to receive daily news. Here news delivery service act as the subject, while newspaper subscribers are observers.

  • News delivery service maintains a list of subscribers and provides methods for new users to subscribe or for existing users to unsubscribe.
  • Each subscriber registers with the news delivery service.
  • When the news delivery service receives new news, it iterates over the list of subscribers and notifies each subscriber.
  • Each subscriber receives the notification and retrieves the news from their mailbox. They can then read the news or take any other action based on the received information.

Here news service doesn’t need to know the specific details of each subscriber; it only needs to know that they are interested in receiving the news. So, new subscribers can be added or removed without affecting the news delivery service. Similarly, new delivery services can be introduced in the future without modifying the existing subscribers.

Structure and Components

Structure and components of observer pattern

  1. Subject (Observable): An interface that defines methods to add, remove and notify observers.
  2. Observer: An interface that declares the update method, which is called by the subject whenever a state change occurs. Here update method takes relevant information as arguments and enables the observer to retrieve the updated data.
  3. Concrete Subject: A specific implementation of the subject interface. It manages a list of observers and maintains the state that observers are interested in. So whenever the state changes, it iterates over the list of observers and sends notifications by calling the update method on each observer. It also implements the methods for existing observers to leave and new observers to join the list.
  4. Concrete Observer: A specific implementation of the observer interface. It registers with a concrete subject and implements the update method to perform some actions after receiving the notification from the subject. There can be multiple concrete observers, and each can be interested in different aspects of the subject’s state.

Note

  • Sometimes, observers need some additional details to implement the update method. For this, the subject can provide those details as an argument to the update method. If required, the subject can pass itself as an argument and let the observer fetch any required data directly using the get method of the subject.
  • Inside the client code, we create subject and observer objects separately and then register observers to receive updates whenever any state change happens.

Problem statement to understand Observer pattern

Suppose our goal is to implement a weather monitoring system where we want to notify various displays whenever the weather conditions (temperature, humidity, and barometric pressure) change.

  • Suppose there are three displays: current conditions, weather statistics, and a simple forecast. 
  • All these displays should be updated in real-time as we acquire the most recent measurements of the weather conditions.
  • This system should be expandable. For example, we need to release an API so that other developers can write their own weather displays and easily integrate them.

Solution using observer pattern

In the above problem, the weather monitoring system acts as the subject, while the displays are the observers.

Step 1: Our first step is to define the Subject interface which will define methods to register, remove and notify all observers.

// Interface for Subject
interface Subject {
    void registerObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers();
}

Step 2: Now we define a concrete subject (WeatherStation) which implements the Subject interface.

  • We add an array list to store observers.
  • We add three private variables to manage the state of weather: temperature, pressure, and humidity.
  • registerObserver(Observer observer): When an observer registers, we just add it to the list.
  • removeObserver(Observer observer): When an observer wants to un-register, we just remove it from the list.
  • setWeatherData(float temperature, float humidity, float pressure): We get updated weather data using this method. So whenever we get fresh data, we call notifyObservers() inside this method.
  • notifyObservers(): This will notify all the observers (stored in the list) about the weather data change by calling the update() method of each observer. Here we pass the new state of temperature, humidity, and pressure to the update method.

Note: If required, we can also implement get methods to fetch the state variables.

// Concrete subject
class WeatherStation implements Subject {
    private List<Observer> observers;
    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherStation() {
        observers = new ArrayList<>();
    }

    @Override
    public void registerObserver(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(temperature, humidity, pressure);
        }
    }

    public void setWeatherData(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        notifyObservers();
    }
}

Step 3: Now we define the Observer interface with the update method.

interface Observer {
    void update(float temperature, float humidity, float pressure);
}

Step 4: Now we define all three concrete observers (current conditions display, forecast display, statistics display) which implement the observer interface and provide a concrete implementation of the update() method.

Each display first receives the update notification and retrieves the updated weather data. It will then update its display or perform any other actions based on the received data. We can also maintain state variables inside observers to keep some or all received weather data via the update notification.

Inside each observer, we keep a reference of the WeatherStation and initialize it inside the constructor. Why are we doing this? There could be several reasons: 

  • We use this to call the registerObserver() method inside the constructor. In other words, whenever the client creates an instance of the concrete observer by passing the reference of the WeatherStation object, it will automatically register the observer with the subject. 
  • We use this to call the removeObserver() method of the subject. For this, we define an unregister() method inside the observer, which will call removeObserver() on the WeatherStation reference. 
  • Suppose in the future, some observer wants to get some particular set of weather data from the WeatherStation, then it can directly fetch the required weather data by calling the get methods on the stored WeatherStation reference.

Implementation of CurrentConditionsDisplay class

// Concrete observer 1
class CurrentConditionsDisplay implements Observer {
    private Subject weatherStation;
    private float temperature;
    private float humidity;
    private float pressure;

    public CurrentConditionsDisplay(Subject weatherStation) {
        this.weatherStation = weatherStation;
        weatherStation.registerObserver(this);
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        display();
    }

    public void display() {
        System.out.println("Current conditions: " + temperature + "F degrees, " + humidity + "% humidity, " + pressure + " pressure");
    }

    public void unregister() {
        weatherStation.removeObserver(this);
    }
}

Implementation of ForecastDisplay class

// Concrete observer 2
class ForecastDisplay implements Observer {
    private Subject weatherStation;

    public ForecastDisplay(Subject weatherStation) {
        this.weatherStation = weatherStation;
        weatherStation.registerObserver(this);
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        display();
    }

    public void display() {
        System.out.println("Forecast: More of the same");
    }

    public void unregister() {
        weatherStation.removeObserver(this);
    }
}

Implementation of StatisticsDisplay class

// Concrete observer 3
class StatisticsDisplay implements Observer {
    private Subject weatherStation;
    private float temperature;
    private float humidity;
    private float pressure;

    public StatisticsDisplay(Subject weatherStation) {
        this.weatherStation = weatherStation;
        weatherStation.registerObserver(this);
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        display();
    }

    public void display() {
        System.out.println("Statistics: " + temperature + "F degrees, " + humidity + "% humidity, " + pressure + " pressure");
    }

    public void unregister() {
        weatherStation.removeObserver(this);
    }
}

Step 5: Inside the client code: We first create an object of the WeatherStation (subject). 

Then we create instances of various display classes (observers) by passing the reference of the WeatherStation object. As mentioned above, this will also register each display object by calling registerObserver() inside the constructor of each observer.

Now we simulate the new weather updates by calling the setWeatherData() method with different values of temperature, humidity, and pressure. On each call, it notifies all registered observers about the new measurements. Note: setWeatherData() may be receiving updates from some other parts of the system.

After a few weather updates, we have removed the forecast display from the list of displays using the unregister() method. As mentioned above, this method will call the removeObserver() method using the private reference of the WeatherStation. Now after this, forecast display will not receive the notification about the new weather data.

Note: We can also directly call the registerObserver() and removeObserver() methods on the instance of the WeatherStation. What would be difference between this and above approach? Think and explore.

// Demo
class ObserverPatternDemo {
    public static void main(String[] args) {
        WeatherStation weatherStation = new WeatherStation();

        CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay(weatherStation);
        ForecastDisplay forecastDisplay = new ForecastDisplay(weatherStation);
        StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherStation);

        // Simulate weather updates
        weatherStation.setWeatherData(80, 65, 30.4f);
        weatherStation.setWeatherData(82, 70, 29.2f);
        weatherStation.setWeatherData(78, 90, 29.2f);
        // Unregister an observer
        forecastDisplay.unregister();
        // Simulate weather updates after unregistering an observer
        weatherStation.setWeatherData(77, 80, 29.2f);
        // Unregister another observer
        currentConditionsDisplay.unregister();
        // Simulate weather updates after unregistering an observer
        weatherStation.setWeatherData(75, 85, 29.0f);
    }
}

Note: There can be other different ways to implement observer Pattern, but most of them will be based on the design that includes Subject and Observer interfaces.

Weather station problem solution using observer pattern

Key takeaway from the above implementation

  • WeatherStation doesn’t need to know the specific details of each display; it just needs to know that they implement the Observer interface. So, we have achieved loose coupling between WeatherStation and displays.
  • We can easily add new types of displays in the future without modifying the existing code of the WeatherStation. For example, suppose a new display class wants to be an observer. All we need to do is implement the Observer interface in the new display class and register it as an observer.
  • We can easily replace any display class at runtime with another display class, and the WeatherStation will keep functioning smoothly. Similarly, we can remove displays at any time.
  • The dynamic relationship between WeatherStation and multiple displays ensures that changes in the weather data are automatically propagated to the displays.

Steps to Implement Observer Pattern

  1. Define a Subject interface with the methods for registering, removing, and notifying observers.
  2. Create a concrete subject class that implements the Subject interface. This will also maintain a list of observers and provide methods to get and set the state that observers are interested in.
  3. Define an Observer interface that defines the method that observers must implement to receive updates from the subject.
  4. Create concrete observer classes that implement the Observer interface. They should also maintain any necessary internal state to store the received information.
  5. Create instances of the subject class and observer classes in your application.
  6. Now you can register or unregister observer instances.

Pros and cons of the observer pattern

Pros

  1. Promote loose coupling: Changes to either the subject or an observer will not affect the other.
  2. Open/Closed Principle: We can add new observers to a subject without modifying its code. So, it will make it simple to introduce new functionality or behaviour.
  3. Reusability: Observers can be reused across different subjects because we can easily attach multiple observers to different subjects.
  4. Better data control: The subject is the exclusive owner of the data. This results in a cleaner object-oriented design compared to allowing multiple objects to control the same data.

Cons:

  1. Each observer must be notified individually. So there can be some performance overhead, especially when there are a large number of observers and frequent updates.
  2. The order in which observers are notified may not be deterministic. This can be a concern if the order of execution matters.
  3. Observers may receive updates that they are not interested in or are not relevant to their current state. This can lead to unnecessary processing if observers need to filter out irrelevant updates.
  4. As the number of observers and subjects increases, managing the relationships between them can become more complex.

Relations with Other Patterns

  • Similar to Observer pattern, we also use Chain of Responsibility, Command and Mediator to connect senders and receivers in various ways.
  • The mediator pattern focuses on coordinating the interactions between objects by centralising the communication logic in a mediator object. So, Observer can be used within the Mediator to notify interested objects about changes or events. Here Mediator can act as the subject and the objects it communicates with can be the observers.
  • In some cases, subject can be implemented as the Singleton in the Observer pattern. This allows for a centralized point of control.
  • In Model-View-Controller (MVC), Observer pattern is often used to establish the communication between the model and the view. The view observes the model for changes and updates its presentation accordingly. This decouples the model and view components and keeps them synchronized.

Applications of Observer Pattern

  1. When an event occurs in event-driven systems, there is a need to notify and update multiple components or modules. So observer pattern helps us to implement this mechanism.
  2. During UI development, sometimes changes in one part of the UI often need to be reflected in other parts. For example, when a user modifies data, we may need to update other UI components accordingly. We can implement this functionality using the observer pattern.
  3. In publish-subscribe systems, publishers generate messages and subscribers are interested in receiving and taking actions based on those messages. Here publishers act as subjects, while subscribers act as observers.
  4. In distributed systems or multi-threaded environments, we can use observer patterns to synchronize data across different components or threads. Observers can listen for changes in the shared data and update their local copies or perform required actions based on the changes.

If you have any queries or feedback, please write us at contact@enjoyalgorithms.com. Enjoy learning!

Share Your Insights

☆ 16-Week Live DSA Course
☆ 10-Week Live DSA Course

More from EnjoyAlgorithms

Self-paced Courses and Blogs

Coding Interview

Machine Learning

System Design

Our Newsletter

Subscribe to get well designed content on data structure and algorithms, machine learning, system design, object orientd programming and math.