# 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

```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

```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

```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.
