Observer is a behavioral design pattern that establishes a one-to-many dependency between objects. When one object (the Subject) changes state, all dependent objects (Observers) will get automatically notified and updated. Here actual set of dependent objects can be unknown beforehand or change dynamically.
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. Due to this, we can easily add or remove observers without modifying the subject. This enhances maintainability, extensibility, and reusability by segregating the concerns of the subject and its observers.
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.
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.
Note
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.
In the above problem, the weather monitoring system acts as the subject, while the displays are the observers.
Step 1: We define the Subject interface and include 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. Here concrete subjects will include these attributes and methods:
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 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.
Inside each observer, we can keep a reference of the WeatherStation and initialize it inside the constructor. Why are we doing this? There could be several reasons:
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 WeatherStation object. As mentioned above, this will 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 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.
If you have any queries or feedback, please write us at contact@enjoyalgorithms.com. Enjoy learning!