The Weather Station Broadcast (Observer)
Stop phoning every listener by name — hand out a letterbox and let anyone subscribe while the data is still flowing.
The idea
Start with a weather station class that gets a fresh temperature, humidity and pressure reading and then personally calls three displays by name. It works fine until a fourth display is wanted, and you are editing the data class again. Every new listener means reopening the one file whose only real job was holding the data.
Observer is a single inversion. Instead of the data source knowing every listener, the listeners know the source. The source keeps one anonymous list of things that asked to be told, and offers three doors: register, remove, notify. It never learns what a display is, only that it has an update method. Newspaper subscription, not a phone tree.
The prize is that subject and observers can now change and ship independently. You can add a display type that did not exist when the station was written, and you can unplug one mid-broadcast without the station flinching. There are two honest flavours of delivery: push, where the subject ships all the data in the call, and pull, where it just says something changed and each observer fetches only what it cares about. You will build both and feel the difference the first time a fourth measurement appears.
The naive alternative is polling on a timer, which burns work when nothing changes and adds latency when something does. But polling is not wrong, it is often the primitive that event systems are built on top of. Once you see the shape of one producer and N anonymous consumers, you will recognise it in DOM events, message queues and Redis pub/sub.
The bench — 4 exercises
Cut the Hardwired Calls
Refactor a WeatherData class that calls three named displays inline into a Subject/Observer pair, then plug a new display in and yank an old one out while readings are flowing.
- Write a naive WeatherData whose measurementsChanged() calls three concrete displays by name, and a fixture file of about 40 readings.
- Introduce Subject (register/remove/notify) and Observer (update) interfaces and make the three displays registered observers.
- Add a fourth HeatIndexDisplay that registers partway through the feed, and unregister the statistics display partway through.
- Have every display append what it sees to its own log file, and diff those logs against the feed by hand or with a tiny script.
hint
Store the observers in a list the subject owns; notify is a dumb loop, nothing clever.
hint
Good smell test: if you can delete a display's source file and the project still compiles, you are done.
DONE WHEN
· Grepping WeatherData for any concrete display class name returns nothing
· The unplugged display's log stops exactly at the unplug reading
· The late-plugged display's log starts exactly at the plug reading
· A brand-new display class can be added and registered with zero edits outside its own file
Push vs. Pull, Same Storm
Implement both delivery styles over the same feed, then add a fourth measurement and count how many files each variant made you touch.
- Build a push variant where update(temp, humidity, pressure) carries the data in the call.
- Build a pull variant where update() only signals a change and observers call getters for what they need.
- Run the identical feed through both and confirm the display output is byte-identical.
- Add wind speed plus a WindDisplay to both variants and list every file you had to modify in each.
- Write one sentence in NOTES.md naming which variant you would ship and why.
hint
Pull is more code per observer and less code per change; the second one is what you live with.
hint
Add a counter to each getter to check which data an observer actually touches.
DONE WHEN
· Both variants produce identical display output for the same feed
· Adding wind speed to the pull variant changed only the subject and the new display file
· The push variant's update signature change rippled into every observer, and you can name the file count
· The wind display never calls the humidity getter in the pull variant
Unsubscribe Mid-Broadcast
Make notify safe when observers register or remove themselves from inside their own update — the trap everyone hits exactly once.
- Add a OneShotDisplay that calls subject.remove(this) inside its own update(), and a display that registers a friend inside update().
- Run the naive notify and observe the skipped observer or concurrent-modification error.
- Fix notify by snapshotting the observer list before iterating, so changes take effect from the next broadcast.
- Drive a few hundred seeded-random rounds of self-removal and re-registration and check every log for consistency.
- State the rule you implemented in one line in NOTES.md.
hint
This is single-threaded — do not reach for locks. The bug is mutating the list you are walking.
hint
Copy the list at the top of notify and the semantics fall out on their own.
DONE WHEN
· No crash or concurrent-modification error across the randomized rounds
· No observer misses a broadcast it was registered for at broadcast start
· No observer receives a broadcast that started before it registered
· Two runs with the same seed produce identical logs
Transfer: The File-Watcher
Rebuild the same pattern in a new domain — a directory watcher that polls inside but exposes register/remove/notify outside.
- Write a DirectoryWatcher subject that lists and stats a fixture directory on an injectable clock, with no OS file-watch APIs.
- Diff each snapshot against the previous one and notify observers with created/modified/deleted events.
- Attach a console observer and an append-only changes.log observer.
- Add a third observer of your own design, such as a filter that only reacts to .md files, without editing the watcher.
- Script some file creations, edits and deletes, tick the clock, and compare the logs to what you expect.
hint
The subject's insides can be ugly — a poll loop and a dict diff — as long as its outside is still the same three doors.
hint
Injecting the clock lets you drive the whole test without real sleeps.
DONE WHEN
· Both base observers' logs contain exactly the expected event sequence
· The filter observer's log contains only matching events
· DirectoryWatcher's source references no concrete observer type
· You can name one real system that wraps polling in an event interface the same way
Go deeper (after the bench)
Read Head First Design Patterns Ch. 2 alongside this module — the Weather-O-Rama narrative is the same brief, and the push/pull spread is worth a re-read right before exercise 2. For a second angle afterwards, Refactoring Guru's Observer page (refactoring.guru/design-patterns/observer, free) gives clean structure diagrams, samples in nine languages, and a good comparison of Observer against Mediator and pub/sub middlemen.