Files

13 KiB

Observer Pattern

Problem

A weather station collects temperature, humidity, and pressure data. Multiple displays (current conditions, statistics, forecast) need to update whenever the weather data changes. If the station directly calls each display, adding a new display requires modifying the station. The station becomes tightly coupled to all display implementations.

Solution

Define a Subject (WeatherStation) that maintains a list of Observers (displays). When data changes, the subject notifies all registered observers. Observers can subscribe and unsubscribe at runtime. The subject doesn't know the concrete types of its observers.

Key participants:

  • Subject — maintains observer list, provides subscribe/unsubscribe/notify
  • Observer — interface with update() method
  • ConcreteSubject — WeatherStation that holds weather data
  • ConcreteObserver — each display that reacts to weather changes

When to Use

  • A one-to-many dependency exists: one object changes, many need to react
  • The set of dependents is dynamic (observers join/leave at runtime)
  • You want loose coupling between the subject and its dependents
  • Event-driven architectures where components react to state changes

When to Avoid

  • Only one observer exists — direct callback is simpler
  • Updates are very frequent and observers do heavy processing (performance issue)
  • Circular dependencies between observers (observer A triggers B triggers A)
  • Order of notification matters and is hard to control

Pseudocode

INTERFACE Observer:
    method update(temperature, humidity, pressure)

CLASS WeatherStation:
    observers = []
    temperature, humidity, pressure

    method subscribe(observer):
        observers.add(observer)

    method unsubscribe(observer):
        observers.remove(observer)

    method notify():
        FOR observer IN observers:
            observer.update(temperature, humidity, pressure)

    method set_measurements(temp, hum, pres):
        temperature = temp
        humidity = hum
        pressure = pres
        notify()

Python

from __future__ import annotations
from abc import ABC, abstractmethod


class Observer(ABC):
    @abstractmethod
    def update(self, temperature: float, humidity: float, pressure: float) -> None:
        pass


class WeatherStation:
    def __init__(self) -> None:
        self._observers: list[Observer] = []
        self.temperature = 0.0
        self.humidity = 0.0
        self.pressure = 0.0

    def subscribe(self, observer: Observer) -> None:
        self._observers.append(observer)
        print(f"  {observer.__class__.__name__} subscribed")

    def unsubscribe(self, observer: Observer) -> None:
        self._observers.remove(observer)
        print(f"  {observer.__class__.__name__} unsubscribed")

    def notify(self) -> None:
        for observer in self._observers:
            observer.update(self.temperature, self.humidity, self.pressure)

    def set_measurements(self, temp: float, humidity: float, pressure: float) -> None:
        print(f"\n  Station: new data -> temp={temp}, humidity={humidity}, pressure={pressure}")
        self.temperature = temp
        self.humidity = humidity
        self.pressure = pressure
        self.notify()


class CurrentConditionsDisplay(Observer):
    def update(self, temperature: float, humidity: float, pressure: float) -> None:
        print(f"  [Current] Temperature: {temperature}°F, Humidity: {humidity}%")


class StatisticsDisplay(Observer):
    def __init__(self) -> None:
        self.temperatures: list[float] = []

    def update(self, temperature: float, humidity: float, pressure: float) -> None:
        self.temperatures.append(temperature)
        avg = sum(self.temperatures) / len(self.temperatures)
        mn = min(self.temperatures)
        mx = max(self.temperatures)
        print(f"  [Stats]   Avg: {avg:.1f}°F, Min: {mn}°F, Max: {mx}°F")


class ForecastDisplay(Observer):
    def __init__(self) -> None:
        self.last_pressure = 0.0

    def update(self, temperature: float, humidity: float, pressure: float) -> None:
        if pressure > self.last_pressure:
            forecast = "Improving weather ahead!"
        elif pressure < self.last_pressure:
            forecast = "Cooler, rainy weather expected."
        else:
            forecast = "More of the same."
        self.last_pressure = pressure
        print(f"  [Forecast] {forecast} (pressure: {pressure} hPa)")


def main() -> None:
    station = WeatherStation()

    current = CurrentConditionsDisplay()
    stats = StatisticsDisplay()
    forecast = ForecastDisplay()

    station.subscribe(current)
    station.subscribe(stats)
    station.subscribe(forecast)

    station.set_measurements(80, 65, 1013.1)
    station.set_measurements(82, 70, 1012.5)

    print("\n  --- Unsubscribing ForecastDisplay ---")
    station.unsubscribe(forecast)

    station.set_measurements(78, 90, 1015.2)


if __name__ == "__main__":
    main()

Output:

  CurrentConditionsDisplay subscribed
  StatisticsDisplay subscribed
  ForecastDisplay subscribed

  Station: new data -> temp=80, humidity=65, pressure=1013.1
  [Current] Temperature: 80°F, Humidity: 65%
  [Stats]   Avg: 80.0°F, Min: 80°F, Max: 80°F
  [Forecast] Improving weather ahead! (pressure: 1013.1 hPa)

  Station: new data -> temp=82, humidity=70, pressure=1012.5
  [Current] Temperature: 82°F, Humidity: 70%
  [Stats]   Avg: 81.0°F, Min: 80°F, Max: 82°F
  [Forecast] Cooler, rainy weather expected. (pressure: 1012.5 hPa)

  --- Unsubscribing ForecastDisplay ---
  ForecastDisplay unsubscribed

  Station: new data -> temp=78, humidity=90, pressure=1015.2
  [Current] Temperature: 78°F, Humidity: 90%
  [Stats]   Avg: 80.0°F, Min: 78°F, Max: 82°F

Go

package main

import "fmt"

type Observer interface {
	Update(temperature, humidity, pressure float64)
	Name() string
}

type WeatherStation struct {
	observers   []Observer
	Temperature float64
	Humidity    float64
	Pressure    float64
}

func (ws *WeatherStation) Subscribe(o Observer) {
	ws.observers = append(ws.observers, o)
	fmt.Printf("  %s subscribed\n", o.Name())
}

func (ws *WeatherStation) Unsubscribe(o Observer) {
	for i, obs := range ws.observers {
		if obs == o {
			ws.observers = append(ws.observers[:i], ws.observers[i+1:]...)
			fmt.Printf("  %s unsubscribed\n", o.Name())
			return
		}
	}
}

func (ws *WeatherStation) notify() {
	for _, o := range ws.observers {
		o.Update(ws.Temperature, ws.Humidity, ws.Pressure)
	}
}

func (ws *WeatherStation) SetMeasurements(temp, humidity, pressure float64) {
	fmt.Printf("\n  Station: new data -> temp=%.0f, humidity=%.0f, pressure=%.1f\n",
		temp, humidity, pressure)
	ws.Temperature = temp
	ws.Humidity = humidity
	ws.Pressure = pressure
	ws.notify()
}

type CurrentConditionsDisplay struct{}

func (d *CurrentConditionsDisplay) Name() string { return "CurrentConditionsDisplay" }
func (d *CurrentConditionsDisplay) Update(temp, humidity, pressure float64) {
	fmt.Printf("  [Current] Temperature: %.0f°F, Humidity: %.0f%%\n", temp, humidity)
}

type StatisticsDisplay struct {
	temps []float64
}

func (d *StatisticsDisplay) Name() string { return "StatisticsDisplay" }
func (d *StatisticsDisplay) Update(temp, humidity, pressure float64) {
	d.temps = append(d.temps, temp)
	sum := 0.0
	mn, mx := d.temps[0], d.temps[0]
	for _, t := range d.temps {
		sum += t
		if t < mn {
			mn = t
		}
		if t > mx {
			mx = t
		}
	}
	avg := sum / float64(len(d.temps))
	fmt.Printf("  [Stats]   Avg: %.1f°F, Min: %.0f°F, Max: %.0f°F\n", avg, mn, mx)
}

type ForecastDisplay struct {
	lastPressure float64
}

func (d *ForecastDisplay) Name() string { return "ForecastDisplay" }
func (d *ForecastDisplay) Update(temp, humidity, pressure float64) {
	forecast := "More of the same."
	if pressure > d.lastPressure {
		forecast = "Improving weather ahead!"
	} else if pressure < d.lastPressure {
		forecast = "Cooler, rainy weather expected."
	}
	d.lastPressure = pressure
	fmt.Printf("  [Forecast] %s (pressure: %.1f hPa)\n", forecast, pressure)
}

func main() {
	station := &WeatherStation{}

	current := &CurrentConditionsDisplay{}
	stats := &StatisticsDisplay{}
	forecast := &ForecastDisplay{}

	station.Subscribe(current)
	station.Subscribe(stats)
	station.Subscribe(forecast)

	station.SetMeasurements(80, 65, 1013.1)
	station.SetMeasurements(82, 70, 1012.5)

	fmt.Println("\n  --- Unsubscribing ForecastDisplay ---")
	station.Unsubscribe(forecast)

	station.SetMeasurements(78, 90, 1015.2)
}

Output:

  CurrentConditionsDisplay subscribed
  StatisticsDisplay subscribed
  ForecastDisplay subscribed

  Station: new data -> temp=80, humidity=65, pressure=1013.1
  [Current] Temperature: 80°F, Humidity: 65%
  [Stats]   Avg: 80.0°F, Min: 80°F, Max: 80°F
  [Forecast] Improving weather ahead! (pressure: 1013.1 hPa)

  Station: new data -> temp=82, humidity=70, pressure=1012.5
  [Current] Temperature: 82°F, Humidity: 70%
  [Stats]   Avg: 81.0°F, Min: 80°F, Max: 82°F
  [Forecast] Cooler, rainy weather expected. (pressure: 1012.5 hPa)

  --- Unsubscribing ForecastDisplay ---
  ForecastDisplay unsubscribed

  Station: new data -> temp=78, humidity=90, pressure=1015.2
  [Current] Temperature: 78°F, Humidity: 90%
  [Stats]   Avg: 80.0°F, Min: 78°F, Max: 82°F

JavaScript

class WeatherStation {
  constructor() {
    this.observers = [];
    this.temperature = 0;
    this.humidity = 0;
    this.pressure = 0;
  }

  subscribe(observer) {
    this.observers.push(observer);
    console.log(`  ${observer.constructor.name} subscribed`);
  }

  unsubscribe(observer) {
    const idx = this.observers.indexOf(observer);
    if (idx !== -1) {
      this.observers.splice(idx, 1);
      console.log(`  ${observer.constructor.name} unsubscribed`);
    }
  }

  notify() {
    for (const observer of this.observers) {
      observer.update(this.temperature, this.humidity, this.pressure);
    }
  }

  setMeasurements(temp, humidity, pressure) {
    console.log(`\n  Station: new data -> temp=${temp}, humidity=${humidity}, pressure=${pressure}`);
    this.temperature = temp;
    this.humidity = humidity;
    this.pressure = pressure;
    this.notify();
  }
}

class CurrentConditionsDisplay {
  update(temperature, humidity, pressure) {
    console.log(`  [Current] Temperature: ${temperature}°F, Humidity: ${humidity}%`);
  }
}

class StatisticsDisplay {
  constructor() {
    this.temps = [];
  }

  update(temperature, humidity, pressure) {
    this.temps.push(temperature);
    const avg = (this.temps.reduce((a, b) => a + b, 0) / this.temps.length).toFixed(1);
    const min = Math.min(...this.temps);
    const max = Math.max(...this.temps);
    console.log(`  [Stats]   Avg: ${avg}°F, Min: ${min}°F, Max: ${max}°F`);
  }
}

class ForecastDisplay {
  constructor() {
    this.lastPressure = 0;
  }

  update(temperature, humidity, pressure) {
    let forecast;
    if (pressure > this.lastPressure) {
      forecast = "Improving weather ahead!";
    } else if (pressure < this.lastPressure) {
      forecast = "Cooler, rainy weather expected.";
    } else {
      forecast = "More of the same.";
    }
    this.lastPressure = pressure;
    console.log(`  [Forecast] ${forecast} (pressure: ${pressure} hPa)`);
  }
}

// --- Demo ---
const station = new WeatherStation();

const current = new CurrentConditionsDisplay();
const stats = new StatisticsDisplay();
const forecast = new ForecastDisplay();

station.subscribe(current);
station.subscribe(stats);
station.subscribe(forecast);

station.setMeasurements(80, 65, 1013.1);
station.setMeasurements(82, 70, 1012.5);

console.log("\n  --- Unsubscribing ForecastDisplay ---");
station.unsubscribe(forecast);

station.setMeasurements(78, 90, 1015.2);

Output:

  CurrentConditionsDisplay subscribed
  StatisticsDisplay subscribed
  ForecastDisplay subscribed

  Station: new data -> temp=80, humidity=65, pressure=1013.1
  [Current] Temperature: 80°F, Humidity: 65%
  [Stats]   Avg: 80.0°F, Min: 80°F, Max: 80°F
  [Forecast] Improving weather ahead! (pressure: 1013.1 hPa)

  Station: new data -> temp=82, humidity=70, pressure=1012.5
  [Current] Temperature: 82°F, Humidity: 70%
  [Stats]   Avg: 81.0°F, Min: 80°F, Max: 82°F
  [Forecast] Cooler, rainy weather expected. (pressure: 1012.5 hPa)

  --- Unsubscribing ForecastDisplay ---
  ForecastDisplay unsubscribed

  Station: new data -> temp=78, humidity=90, pressure=1015.2
  [Current] Temperature: 78°F, Humidity: 90%
  [Stats]   Avg: 80.0°F, Min: 78°F, Max: 82°F
  • Event Bus — In-process Observer where the bus replaces direct subject-observer coupling. Use Observer when subjects and observers know each other; use Event Bus when you want full decoupling.
  • Publish-Subscribe — Distributed Observer. Pub-Sub adds a message broker between publishers and subscribers, enabling cross-process and cross-service event delivery.
  • Mediator — Both decouple objects. Observer is for one-to-many notification; Mediator is for many-to-many interaction coordination where the mediator routes messages.
  • Command — Commands can be fired via Observer; the observer triggers command execution.
  • Event Sourcing — Event Sourcing emits events that Observers consume to build read models or trigger side effects.