Observer
Observer is a behavioral pattern that lets any number of objects register interest in another object's changes and get notified automatically whenever those changes happen.
Problem
A weather station gathers new sensor readings every few minutes. A phone app, a desktop widget, and a logging service all need to know the instant fresh data lands, but none of them should have to ask the station directly on a timer.
Polling is the naive fix - each consumer calls the station repeatedly and compares against the last value it saw. That wastes cycles when nothing has changed and adds lag between an update and the moment consumers see it. Hardcoding calls from the station to each specific consumer is worse: the station now has to know about the phone app, the widget, and the logger by name, and adding a fourth consumer means editing the station's code.
Solution
Observer gives the object holding the interesting state - the publisher - a guest list it doesn't need to know the contents of in advance. Any object can join that list by subscribing, and any object can leave by unsubscribing; the publisher just keeps a generic collection of registered listeners.
When the publisher's state changes, it walks that list and calls the same notification method on every entry, without caring what each subscriber actually does with the news - update a display, write a log line, trigger a recalculation. New subscriber types can be added at runtime with zero changes to the publisher, because the publisher only ever depends on a small notification interface, not on any concrete consumer.
When to Use
- Reach for Observer when one object's state change needs to ripple out to a set of other objects that isn't fixed or fully known in advance.
- Use it when interested parties need to come and go dynamically - watching only during a specific screen, session, or operation.
Real-World Examples
- **DOM events** - `element.addEventListener('click', handler)` subscribes a handler to notifications the element publishes.
- **RxJS Observables** - a stream publisher notifies every subscribed operator chain each time a new value is emitted.
- **Spreadsheet formulas** - changing one cell automatically recalculates and re-renders every cell whose formula references it.
Structure
The Publisher owns the state worth watching and a collection of subscriber references it notifies through a uniform interface. Subscribers implement that interface and register themselves with the publisher; each reacts to notifications in its own way once called.
Participants
Holds the observable state, keeps a registry of current subscribers, and walks that registry to notify everyone whenever the state changes.
In the diagram: Weather Station.
A minimal contract - usually one notification method - that lets the publisher call any registered subscriber without knowing its concrete type.
Registers itself with a publisher and implements its own reaction to each notification, independent of what other subscribers do.
In the diagram: Phone App, Desktop Widget, Logging Service.
class Subscriber {
update(temperature) { throw new Error('update() must be implemented'); }
}
class WeatherStation {
#subscribers = new Set();
subscribe(subscriber) {
this.#subscribers.add(subscriber);
}
unsubscribe(subscriber) {
this.#subscribers.delete(subscriber);
}
setReading(temperature) {
this.#subscribers.forEach(subscriber => subscriber.update(temperature));
}
}
class PhoneApp extends Subscriber {
update(temperature) {
console.log(`Phone app: new reading ${temperature}°C`);
}
}
class DesktopWidget extends Subscriber {
update(temperature) {
console.log(`Desktop widget: new reading ${temperature}°C`);
}
}
class LoggingService extends Subscriber {
update(temperature) {
console.log(`Logged reading: ${temperature}°C`);
}
}
const station = new WeatherStation();
station.subscribe(new PhoneApp());
station.subscribe(new DesktopWidget());
station.subscribe(new LoggingService());
station.setReading(21.5);
Step 1 of 5
Subscriber is the interface every observer must implement
The base `update(temperature)` throws - it exists only to define the contract that `WeatherStation` will call on every subscriber.
Step 2 of 5
WeatherStation is the subject - it tracks subscribers and notifies them
`#subscribers` is a `Set`, so `subscribe`/`unsubscribe` are just add/delete; `setReading` calls `subscriber.update(temperature)` on every current subscriber - the station never knows what kind of subscriber it's talking to.
Step 3 of 5
Three concrete subscribers react differently to the same update
`PhoneApp`, `DesktopWidget` and `LoggingService` all `extends Subscriber` and implement `update` with their own `console.log` - each decides independently what to do with the same `temperature` value.
Step 4 of 5
Subscribers register themselves with the station
Three different `Subscriber` instances are added via `station.subscribe(...)` - the station just accumulates them into `#subscribers` without distinguishing their types.
Step 5 of 5
One setReading() call fans out to all three subscribers
`station.setReading(21.5)` triggers the `forEach` in `setReading`, so `PhoneApp`, `DesktopWidget` and `LoggingService` each log their own message with the same `21.5` value.
interface Subscriber {
update(temperature: number): void;
}
class WeatherStation {
private subscribers = new Set<Subscriber>();
subscribe(subscriber: Subscriber): void {
this.subscribers.add(subscriber);
}
unsubscribe(subscriber: Subscriber): void {
this.subscribers.delete(subscriber);
}
setReading(temperature: number): void {
this.subscribers.forEach(subscriber => subscriber.update(temperature));
}
}
class PhoneApp implements Subscriber {
update(temperature: number): void {
console.log(`Phone app: new reading ${temperature}°C`);
}
}
class DesktopWidget implements Subscriber {
update(temperature: number): void {
console.log(`Desktop widget: new reading ${temperature}°C`);
}
}
class LoggingService implements Subscriber {
update(temperature: number): void {
console.log(`Logged reading: ${temperature}°C`);
}
}
const station = new WeatherStation();
station.subscribe(new PhoneApp());
station.subscribe(new DesktopWidget());
station.subscribe(new LoggingService());
station.setReading(21.5);
interface Subscriber {
void update(double temperature);
}
class WeatherStation {
private final Set<Subscriber> subscribers = new HashSet<>();
void subscribe(Subscriber subscriber) {
subscribers.add(subscriber);
}
void unsubscribe(Subscriber subscriber) {
subscribers.remove(subscriber);
}
void setReading(double temperature) {
for (Subscriber subscriber : subscribers) subscriber.update(temperature);
}
}
class PhoneApp implements Subscriber {
public void update(double temperature) {
System.out.println("Phone app: new reading " + temperature + "°C");
}
}
class DesktopWidget implements Subscriber {
public void update(double temperature) {
System.out.println("Desktop widget: new reading " + temperature + "°C");
}
}
class LoggingService implements Subscriber {
public void update(double temperature) {
System.out.println("Logged reading: " + temperature + "°C");
}
}
WeatherStation station = new WeatherStation();
station.subscribe(new PhoneApp());
station.subscribe(new DesktopWidget());
station.subscribe(new LoggingService());
station.setReading(21.5);
interface ISubscriber
{
void Update(double temperature);
}
class WeatherStation
{
private readonly HashSet<ISubscriber> subscribers = new();
public void Subscribe(ISubscriber subscriber) => subscribers.Add(subscriber);
public void Unsubscribe(ISubscriber subscriber) => subscribers.Remove(subscriber);
public void SetReading(double temperature)
{
foreach (var subscriber in subscribers) subscriber.Update(temperature);
}
}
class PhoneApp : ISubscriber
{
public void Update(double temperature) => Console.WriteLine($"Phone app: new reading {temperature}°C");
}
class DesktopWidget : ISubscriber
{
public void Update(double temperature) => Console.WriteLine($"Desktop widget: new reading {temperature}°C");
}
class LoggingService : ISubscriber
{
public void Update(double temperature) => Console.WriteLine($"Logged reading: {temperature}°C");
}
var station = new WeatherStation();
station.Subscribe(new PhoneApp());
station.Subscribe(new DesktopWidget());
station.Subscribe(new LoggingService());
station.SetReading(21.5);
from __future__ import annotations
from abc import ABC, abstractmethod
class Subscriber(ABC):
@abstractmethod
def update(self, temperature: float) -> None: ...
class WeatherStation:
def __init__(self) -> None:
self._subscribers: set[Subscriber] = set()
def subscribe(self, subscriber: Subscriber) -> None:
self._subscribers.add(subscriber)
def unsubscribe(self, subscriber: Subscriber) -> None:
self._subscribers.discard(subscriber)
def set_reading(self, temperature: float) -> None:
for subscriber in self._subscribers:
subscriber.update(temperature)
class PhoneApp(Subscriber):
def update(self, temperature: float) -> None:
print(f'Phone app: new reading {temperature}°C')
class DesktopWidget(Subscriber):
def update(self, temperature: float) -> None:
print(f'Desktop widget: new reading {temperature}°C')
class LoggingService(Subscriber):
def update(self, temperature: float) -> None:
print(f'Logged reading: {temperature}°C')
station = WeatherStation()
station.subscribe(PhoneApp())
station.subscribe(DesktopWidget())
station.subscribe(LoggingService())
station.set_reading(21.5)
Step 1 of 5
Subscriber is the interface every observer must implement
`update(temperature)` is `@abstractmethod` - it exists only to define the contract `WeatherStation` will call on every subscriber.
Step 2 of 5
WeatherStation is the subject - it tracks subscribers and notifies them
`_subscribers` is a `set`, so `subscribe`/`unsubscribe` just add/`discard`; `set_reading` loops over `_subscribers` and calls `update(temperature)` on each - the station never knows what kind of subscriber it's talking to.
Step 3 of 5
Three concrete subscribers react differently to the same update
`PhoneApp`, `DesktopWidget` and `LoggingService` all subclass `Subscriber` and implement `update` with their own `print` call - each decides independently what to do with the same `temperature` value.
Step 4 of 5
Subscribers register themselves with the station
Three different `Subscriber` instances are added via `station.subscribe(...)` - the station just accumulates them into `_subscribers` without distinguishing their types.
Step 5 of 5
One set_reading() call fans out to all three subscribers
`station.set_reading(21.5)` triggers the loop in `set_reading`, so `PhoneApp`, `DesktopWidget` and `LoggingService` each print their own message with the same `21.5` value.
Click "Run" to execute this code in a sandboxed frame and see console output here.
TypeScript runs as plain JavaScript here - type annotations are stripped, not type-checked.
Click "Run" to execute this code in a sandboxed frame and see console output here.
Advantages
- New kinds of subscribers can be added at any time without touching the publisher's source at all.
- Subscriptions form and dissolve while the program is running, so the set of listeners can grow or shrink based on live conditions.
- The publisher depends only on a thin notification interface, never on any subscriber's concrete class, keeping the two sides loosely coupled.
Disadvantages
- Nothing guarantees the order subscribers are notified in, which becomes a problem if one subscriber's reaction depends on another running first.
- A subscriber that forgets to unsubscribe keeps a live reference in the publisher's list, quietly leaking memory long after it should have been collected.
- A chain of notifications triggering further state changes and further notifications can be hard to trace and debug.
Question 1 of 10
When should you reach for Observer according to its recommended use cases?
Correct! Reach for Observer when one object's state change needs to ripple out to a set of other objects that isn't fixed or fully known in advance.
Not quite. Reach for Observer when one object's state change needs to ripple out to a set of other objects that isn't fixed or fully known in advance.
Question 2 of 10
What is a stated risk of a subscriber that forgets to unsubscribe?
Correct! A subscriber that forgets to unsubscribe keeps a live reference in the publisher's list, quietly leaking memory long after it should have been collected.
Not quite. A subscriber that forgets to unsubscribe keeps a live reference in the publisher's list, quietly leaking memory long after it should have been collected.
Question 3 of 10
What is wrong with polling as a fix for the weather-station problem?
Correct! Polling wastes cycles when nothing has changed and adds lag between an update and the moment consumers see it.
Not quite. Polling wastes cycles when nothing has changed and adds lag between an update and the moment consumers see it.
Question 4 of 10
According to the pros listed for Observer, what keeps the publisher and its subscribers loosely coupled?
Correct! The publisher depends only on a thin notification interface, never on any subscriber's concrete class, keeping the two sides loosely coupled.
Not quite. The publisher depends only on a thin notification interface, never on any subscriber's concrete class, keeping the two sides loosely coupled.
Question 5 of 10
What is a stated benefit of Observer for adding new subscriber types?
Correct! New subscriber types can be added at runtime with zero changes to the publisher, because the publisher only depends on a small notification interface.
Not quite. New subscriber types can be added at runtime with zero changes to the publisher, because the publisher only depends on a small notification interface.
Question 6 of 10
Why does Observer offer no guarantee about notification order?
Correct! Nothing guarantees the order subscribers are notified in, which becomes a problem if one subscriber's reaction depends on another running first.
Not quite. Nothing guarantees the order subscribers are notified in, which becomes a problem if one subscriber's reaction depends on another running first.
Question 7 of 10
In the weather-station example, why can't the station just call the phone app, widget, and logger directly by name?
Correct! Hardcoding calls from the station to each specific consumer means the station has to know about the phone app, the widget, and the logger by name, and adding a fourth consumer means editing the station's code.
Not quite. Hardcoding calls from the station to each specific consumer means the station has to know about the phone app, the widget, and the logger by name, and adding a fourth consumer means editing the station's code.
Question 8 of 10
What does the publisher need to know about its subscribers in advance?
Correct! Observer gives the publisher a guest list it doesn't need to know the contents of in advance - it just keeps a generic collection of registered listeners.
Not quite. Observer gives the publisher a guest list it doesn't need to know the contents of in advance - it just keeps a generic collection of registered listeners.
Question 9 of 10
How does a publisher notify its subscribers when its state changes?
Correct! When the publisher's state changes, it walks the subscriber list and calls the same notification method on every entry, without caring what each subscriber actually does with the news.
Not quite. When the publisher's state changes, it walks the subscriber list and calls the same notification method on every entry, without caring what each subscriber actually does with the news.
Question 10 of 10
What does Observer let objects do?
Correct! Observer lets any number of objects register interest in another object's changes and get notified automatically whenever those changes happen.
Not quite. Observer lets any number of objects register interest in another object's changes and get notified automatically whenever those changes happen.