Observer Pattern

Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically

Understanding the Observer Pattern

The Observer pattern is a behavioral design pattern that establishes a one-to-many relationship between objects. When one object (the subject) changes state, all its dependents (observers) are notified and updated automatically. This pattern is a cornerstone of event-driven programming.

Problem

Imagine you're developing an application where changes to one object require actions in other objects. For example, when a stock price changes, multiple displays need to update: a price chart, a list view, and a statistics panel. Without a proper pattern, you might end up with tight coupling between these components, making your code rigid and difficult to maintain.

The challenge is to establish communication between these objects while keeping them loosely coupled. You want the price object to notify the displays of changes without knowing which displays are listening.

Solution

The Observer pattern suggests defining a one-to-many dependency between objects. The key objects in this pattern are the Subject (also called Observable) and Observer:

When the subject's state changes, it notifies all observers. The observers can then query the subject to get the updated state and take appropriate actions.

Structure

Subject attach(Observer) detach(Observer) notify() ConcreteSubject getState() setState() Observer update() ConcreteObserverA ConcreteObserverB update() update() observers notify

Participants

When to Use

Use the Observer Pattern when:

Benefits

Real-World Uses

Implementation Example

Here's a JavaScript implementation of the Observer pattern for a simple weather station:

// Observer interface
class Observer {
    update(data) {
        throw new Error("Method 'update()' must be implemented");
    }
}

// Subject (Observable)
class Subject {
    constructor() {
        this.observers = [];
    }
    
    attach(observer) {
        if (observer instanceof Observer && !this.observers.includes(observer)) {
            this.observers.push(observer);
        }
    }
    
    detach(observer) {
        const index = this.observers.indexOf(observer);
        if (index !== -1) {
            this.observers.splice(index, 1);
        }
    }
    
    notify(data) {
        for (const observer of this.observers) {
            observer.update(data);
        }
    }
}

// Concrete Subject
class WeatherStation extends Subject {
    constructor() {
        super();
        this.temperature = 0;
        this.humidity = 0;
        this.pressure = 0;
    }

    setMeasurements(temperature, humidity, pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;

        // Notify all observers about the change
        this.notify({
            temperature: this.temperature,
            humidity: this.humidity,
            pressure: this.pressure
        });
    }
}

// Concrete Observer 1
class CurrentConditionsDisplay extends Observer {
    constructor() {
        super();
    }

    update(data) {
        const { temperature, humidity } = data;
        console.log(`Current conditions: ${temperature}°C and ${humidity}% humidity`);
    }
}

// Concrete Observer 2
class StatisticsDisplay extends Observer {
    constructor() {
        super();
        this.temperatures = [];
    }

    update(data) {
        const { temperature } = data;
        this.temperatures.push(temperature);
        
        const avg = this.temperatures.reduce((sum, t) => sum + t, 0) / this.temperatures.length;
        const max = Math.max(...this.temperatures);
        const min = Math.min(...this.temperatures);
        
        console.log(`Avg/Max/Min temperature = ${avg.toFixed(1)}/${max}/${min}`);
    }
}

// Concrete Observer 3
class ForecastDisplay extends Observer {
    constructor() {
        super();
        this.lastPressure = 0;
        this.currentPressure = 0;
    }

    update(data) {
        const { pressure } = data;
        this.lastPressure = this.currentPressure;
        this.currentPressure = pressure;
        
        let forecast = "";
        if (this.currentPressure > this.lastPressure) {
            forecast = "Improving weather on the way!";
        } else if (this.currentPressure === this.lastPressure) {
            forecast = "More of the same";
        } else {
            forecast = "Watch out for cooler, rainy weather";
        }
        
        console.log(`Forecast: ${forecast}`);
    }
}

// Using the pattern
const weatherStation = new WeatherStation();

const currentDisplay = new CurrentConditionsDisplay();
const statisticsDisplay = new StatisticsDisplay();
const forecastDisplay = new ForecastDisplay();

weatherStation.attach(currentDisplay);
weatherStation.attach(statisticsDisplay);
weatherStation.attach(forecastDisplay);

// Simulate weather changes
weatherStation.setMeasurements(22, 65, 1012);
weatherStation.setMeasurements(24, 70, 1010);
weatherStation.setMeasurements(20, 90, 1005);

In this example, the WeatherStation is our subject, and the various displays are the observers. When the weather data changes, all registered displays are automatically updated to show the new information. This demonstrates how the Observer pattern can be used to implement a loose coupling between related objects.

Interactive Demo

Experience the Observer pattern in action with this interactive weather station demo. You can change the weather measurements and see how different displays update in response to the changes.

Interactive Demo: Weather Station

Adjust the weather measurements and observe how the different displays update automatically.

Weather Station (Subject)

22°C
65%
1012 hPa

Weather Displays (Observers)

Current Conditions Display
Statistics Display
Forecast Display
// Observer pattern events will be logged here