13 KiB
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
Related Patterns
- 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.