The Observer Pattern: Stop Hard-Coding Your Dependencies
Learn the most widely used behavioral design pattern by experiencing the pain of tightly coupled code first, then discovering how the Observer pattern makes your systems flexible and extensible. You will refactor a broken weather station into clean, decoupled architecture — guided by UML diagrams that update live as you code.
When One Display Breaks the Whole System
Why this matters
Hard-coded dependencies feel fine when there are only three of them — until your manager asks for a fourth, and you realize every new display means editing the class that’s supposed to just collect sensor data. You’re about to live that pain on purpose, so the fix in later steps lands as relief rather than abstract theory.
🎯 You will learn to
- Identify the symptoms of tightly coupled code in a real codebase
- Analyze why direct dependencies create maintenance headaches as systems grow
The Scenario
Prerequisites: This tutorial assumes you are comfortable with Python classes, inheritance, abstract base classes (
ABC,@abstractmethod), and@propertydecorators. If any of these are unfamiliar, review them first — you will need all four.
You work at a weather company. Your WeatherStation collects temperature, humidity, and pressure data from sensors, and updates three displays:
| Display | What it shows |
|---|---|
| CurrentDisplay | Current temperature and humidity |
| StatisticsDisplay | Average, min, and max temperature |
| ForecastDisplay | Weather forecast based on pressure |
The Architecture Problem
Look at the UML Diagram tab (bottom-right). Notice how WeatherStation has direct arrows to every single display. That means the station hard-codes every display it talks to:
The code works. Ship it, right? Not so fast. Your manager just dropped a new requirement:
“Add a Heat Index Display that shows the computed heat index based on temperature and humidity.”
Your Task
Open weather_station.py and:
- Create a
HeatIndexDisplayclass with ashow(self, temp, humidity, pressure)method. Use this simplified formula:heat_index = temp - 0.55 * (1 - humidity/100) * (temp - 58)Print:
[Heat] Heat Index: {value:.1f}°F -
Integrate it into
WeatherStationso it gets called whenever new data arrives. - Run the code and verify all four displays update.
What to Notice
Pay attention to how many places you had to change just to add one new display. Count them. Then check the UML Diagram tab — watch the new dependency arrow appear. That observation is the entire point of this exercise.
Errors Are Expected
If something breaks, good — that is the lesson. Tightly coupled code resists change. Read the error messages and fix them.
class CurrentDisplay:
"""Shows current temperature and humidity."""
def show(self, temp: float, humidity: float, pressure: float) -> None:
print(f"[Current] {temp}°F, {humidity}% humidity")
class StatisticsDisplay:
"""Shows average, min, max temperature."""
def __init__(self) -> None:
self._temps: list[float] = []
def show(self, temp: float, humidity: float, pressure: float) -> None:
self._temps.append(temp)
avg = sum(self._temps) / len(self._temps)
lo, hi = min(self._temps), max(self._temps)
print(f"[Stats] Avg={avg:.1f}°F Min={lo}°F Max={hi}°F")
class ForecastDisplay:
"""Shows forecast based on pressure changes."""
def __init__(self) -> None:
self._last_pressure: float = 0.0
def show(self, temp: float, humidity: float, pressure: float) -> None:
if pressure > self._last_pressure:
outlook = "Improving weather!"
elif pressure < self._last_pressure:
outlook = "Cooler, rainy weather"
else:
outlook = "More of the same"
self._last_pressure = pressure
print(f"[Forecast] {outlook}")
class WeatherStation:
"""Collects weather data and updates all displays.
NOTICE: This class directly creates and calls every display.
To add a new display, you must modify THIS class."""
def __init__(self) -> None:
self.current: CurrentDisplay = CurrentDisplay()
self.stats: StatisticsDisplay = StatisticsDisplay()
self.forecast: ForecastDisplay = ForecastDisplay()
def new_measurement(self, temp: float, humidity: float, pressure: float) -> None:
"""Called when sensors report new weather data."""
print(f"\n--- Measurement: {temp}°F, {humidity}%, {pressure} hPa ---")
self.current.show(temp, humidity, pressure)
self.stats.show(temp, humidity, pressure)
self.forecast.show(temp, humidity, pressure)
# --- Demo ---
if __name__ == "__main__":
station = WeatherStation()
station.new_measurement(80, 65, 1013.25)
station.new_measurement(82, 70, 1012.50)
station.new_measurement(78, 90, 1015.00)
Step 1 — Knowledge Check
Min. score: 80%
1. How many places in WeatherStation did you have to modify to add the HeatIndexDisplay?
You had to modify WeatherStation in two places: creating the display in __init__() and calling it in new_measurement(). This means the station is tightly coupled to every display it serves. Look at the UML diagram — every arrow from WeatherStation is a dependency that makes the system harder to change.
2. What is tight coupling in software design?
Tight coupling means a class directly depends on the concrete details of other classes. In our weather station, the station directly knows about CurrentDisplay, StatisticsDisplay, ForecastDisplay, and now HeatIndexDisplay — by name, by type, by how they work. Change or remove any display, and you must edit the station.
3. If a product manager asks you to remove the ForecastDisplay from the current version, what do you have to change?
Because the station directly references the forecast display, you cannot just delete the class — you would get a NameError. You must also clean up the two references in WeatherStation. This is the maintenance tax of tight coupling: every addition or removal forces you to edit the core class.
4. Imagine the weather station needs to support 50 different display types, and users can enable or disable them while the program is running. What fundamental problem does the current design have?
With 50 displays, the station’s __init__ would have 50 lines creating displays, and new_measurement would have 50 lines calling them. Worse, you cannot add or remove displays while the program is running — everything is hard-coded at construction time. The station should not care how many displays exist or what they do.
Design Patterns: A Shared Vocabulary
Why this matters
Step 1 hurt because of a structural problem — but “things felt coupled” isn’t a useful diagnosis a senior engineer can act on. Design patterns give you precise vocabulary like Subject, Observer, attach, notify that lets two engineers describe the same architecture without drawing UML on a whiteboard. Naming the pattern is the first step to applying it.
🎯 You will learn to
- Define what a design pattern is and where the Gang of Four catalog comes from
- Apply the YouTube subscription analogy to map Observer’s roles onto a familiar mental model
What Are Design Patterns?
The YouTube Analogy
Think about how YouTube notifications work:
- A creator (like MrBeast) uploads a new video
- All subscribers automatically get notified
- The creator does not need to know who the subscribers are or what they will do
- Subscribers can subscribe or unsubscribe at any time without the creator changing anything
This is exactly the Observer Design Pattern:
| YouTube | Observer Pattern | Weather Station |
|---|---|---|
| Channel (MrBeast) | Subject | WeatherStation |
| “Upload” event | State change | new_measurement() |
| Subscribers | Observers | Display objects |
| Subscribe button | attach() |
Register a display |
| Unsubscribe | detach() |
Remove a display |
| Push notification | notify() |
Call update() on all displays |
What Is a “Design Pattern”?
A design pattern is a proven, reusable solution to a common software design problem. Design patterns are not code you copy-paste — they are blueprints for structuring your classes and objects to solve specific types of architectural problems.
The most famous catalog comes from the 1994 book Design Patterns by Gamma, Helm, Johnson, and Vlissides — the Gang of Four (GoF). They cataloged 23 patterns in three categories:
| Category | What It Solves | Examples |
|---|---|---|
| Creational | How objects are created | Factory, Singleton, Builder |
| Structural | How objects are composed | Adapter, Decorator, Facade |
| Behavioral | How objects communicate | Observer, Strategy, State |
The Observer is a behavioral pattern — it defines how objects talk to each other in a loosely coupled way.
The Observer Pattern in UML
Here is the formal structure. Compare this to the tightly coupled diagram from Step 1 — notice how Subject no longer points to specific concrete observers:
Key UML relationships:
- Generalization = inheritance (“is-a”)
- Realization = implements interface
- Aggregation = “has-a” (loosely owned)
Polling vs. Notification
An alternative to Observer is polling — each display repeatedly asks the station “has anything changed?” This is like refreshing a web page over and over instead of getting push notifications. It works, but wastes resources and introduces delays.
PRIMM Exercise: Predict, Run, Investigate, Modify
1. Predict
Look at pattern_preview.py. Before running it, predict:
- How many times will “Data changed!” print?
- Will
DashboardAalways print beforeDashboardB?
2. Run
Run it and compare the output to your prediction.
3. Investigate
Try these modifications and observe what changes:
- Remove the line
source.attach(DashboardB()). What happens to Dashboard B? Why? - Add a third
source.set_value(30)at the bottom. How many total lines print? - Move
source.attach(DashboardA())to after the firstset_value(10). Does Dashboard A see the value 10?
4. Modify
Add a DashboardC class that prints the value squared (e.g., "Dashboard C sees: 100 (squared)"). Attach it and verify it works. Then undo your changes — the tests check the original output.
# A tiny preview of the Observer pattern in action.
# PREDICT: What will this print? In what order?
class DataSource:
"""A simple Subject that notifies observers."""
def __init__(self) -> None:
self._observers: list = []
self._value: int = 0
def attach(self, observer) -> None:
self._observers.append(observer)
def set_value(self, new_value: int) -> None:
self._value = new_value
print(f"Data changed! New value: {self._value}")
for obs in self._observers:
obs.update(self._value)
class DashboardA:
def update(self, value: int) -> None:
print(f" Dashboard A sees: {value}")
class DashboardB:
def update(self, value: int) -> None:
print(f" Dashboard B sees: {value * 2} (doubled)")
source = DataSource()
source.attach(DashboardA())
source.attach(DashboardB())
source.set_value(10)
source.set_value(20)
Step 2 — Knowledge Check
Min. score: 80%1. In the Observer pattern, the Subject is:
The Subject holds state. When its state changes, it notifies all registered observers. In the YouTube analogy, it is the channel creator. In the UML diagram, the Subject has attach(), detach(), and notify() methods and maintains a list of Observer references.
2. Why is the Observer pattern better than polling (each display repeatedly checking the station for changes)?
Polling wastes CPU cycles checking for changes that have not happened yet. With Observer, the Subject pushes notifications exactly when state changes — no wasted work. This is like getting a push notification vs. manually refreshing your feed every 5 seconds.
3. A design pattern is:
Design patterns are not copy-paste code or libraries. They are reusable solution blueprints — named, documented approaches to recurring design problems. You adapt them to your specific context. The GoF catalog describes 23 such patterns.
4. In the UML diagram shown above, what does the dashed line with an open triangle from ConcreteObserverA to Observer mean?
In UML, a dashed arrow with an open triangle means “implements” or “realizes” an interface. ConcreteObserverA promises to provide the update() method declared in the Observer abstract class. A solid arrow with open triangle means direct inheritance.
5. Which category of GoF design patterns does the Observer belong to?
Observer is a behavioral pattern because it defines how objects communicate: the Subject broadcasts state changes to registered Observers. Creational patterns (Factory, Builder) handle object creation. Structural patterns (Adapter, Decorator) handle object composition.
Building Observer: Step by Step
Why this matters
Vocabulary alone won’t get you a working system — you need to wire the pieces together correctly the first time. The Subject’s role is subtle: it must hold a list of abstract observers, never naming the concrete classes that will plug in later. Building it from a faded worked example trains the muscle memory you’ll rely on every time you reach for this pattern in production.
🎯 You will learn to
- Apply the Observer pattern by implementing
Subject,Observer, and a concrete pair from scratch - Analyze each UML role in your own code and watch the class diagram build up live
Constructing the Observer Pattern
Quick Recall
Before we start coding, answer these in your head (from Step 2):
- What are the two main roles in the Observer pattern?
- What three methods does a Subject provide?
- What happens when a Subject’s state changes?
If you are unsure, revisit Step 2 before continuing.
Try First (Generation Challenge)
Before looking at the blueprint below, try this on paper or in your head for 2 minutes:
If you had to build an Observer pattern from scratch for a
TemperatureSensorthat notifies aTemperatureLogger, what classes would you create? What methods would each class need?
Jot down your answer, then compare it to the blueprint. The goal is not to get it right — the attempt itself deepens your understanding of the solution.
The Blueprint
The Observer pattern has four roles. Here is the UML for what you are about to build:
| Role | Class | Responsibility |
|---|---|---|
| Subject (base) | Subject |
Maintains observer list; provides attach(), detach(), notify() |
| ConcreteSubject | TemperatureSensor |
Holds temperature state; calls notify() when it changes |
| Observer (interface) | Observer |
Declares the update() method all observers must implement |
| ConcreteObserver | TemperatureLogger, TemperatureAlert |
Implement update() to react to changes |
Your Task — Faded Implementation
Open observer_basics.py. The Subject and Observer base classes are fully implemented for you. Your job:
-
Complete
TemperatureSensor(TODO 1): Thetemperatureproperty setter should store the new value AND callself.notify() -
Complete
TemperatureLogger(TODO 2): Itsupdate()should readsubject.temperature, append it toself.readings, and print it -
Complete
TemperatureAlert(TODO 3): Itsupdate()should check if temperature exceeds the threshold -
Complete
main()(TODOs 4-7): Create a sensor, attach both observers, then set the temperature to 72, 95, and 68
Watch the UML
As you add classes and methods, check the UML Diagram tab — the class diagram updates live to show your inheritance hierarchy and relationships. Compare it to the target diagram above.
from abc import ABC, abstractmethod
# ── Sub-goal 1: Subject base class (COMPLETE — study this) ─────
class Subject:
"""Base class for observable objects."""
def __init__(self) -> None:
self._observers: list = []
def attach(self, observer: 'Observer') -> None:
"""Register an observer."""
if observer not in self._observers:
self._observers.append(observer)
print(f" Attached: {type(observer).__name__}")
def detach(self, observer: 'Observer') -> None:
"""Unregister an observer."""
self._observers.remove(observer)
print(f" Detached: {type(observer).__name__}")
def notify(self) -> None:
"""Notify all registered observers."""
for observer in self._observers:
observer.update(self)
# ── Sub-goal 2: Observer interface (COMPLETE — study this) ─────
class Observer(ABC):
"""Interface that all observers must implement."""
@abstractmethod
def update(self, subject: Subject) -> None:
"""Called by the Subject when state changes."""
pass
# ── Sub-goal 3: ConcreteSubject — YOUR TURN ───────────────────
class TemperatureSensor(Subject):
"""A sensor that tracks temperature and notifies observers."""
def __init__(self) -> None:
super().__init__()
self._temperature: float = 0.0
@property
def temperature(self) -> float:
return self._temperature
@temperature.setter
def temperature(self, value: float) -> None:
self._temperature = value
print(f"\nSensor: Temperature set to {value}°F")
# TODO 1: Notify all observers that the state changed.
# (Hint: which inherited method broadcasts changes?)
# ── Sub-goal 4: ConcreteObservers — YOUR TURN ─────────────────
class TemperatureLogger(Observer):
"""Records every temperature reading."""
def __init__(self) -> None:
self.readings: list[float] = []
def update(self, subject: Subject) -> None:
# TODO 2: Read subject.temperature, append to self.readings,
# and print: f"Logger: Recorded {temp}°F"
pass
class TemperatureAlert(Observer):
"""Alerts when temperature exceeds a threshold."""
def __init__(self, threshold: float = 90.0) -> None:
self.threshold: float = threshold
def update(self, subject: Subject) -> None:
# TODO 3: Read subject.temperature.
# If temp > self.threshold:
# print(f"ALERT: Temperature {temp}°F exceeds {self.threshold}°F!")
# Else:
# print(f"Alert: {temp}°F is within safe range")
pass
# ── Sub-goal 5: Wire it up — YOUR TURN ────────────────────────
def main() -> None:
# TODO 4: Create a TemperatureSensor
# TODO 5: Create a TemperatureLogger and a TemperatureAlert(threshold=90)
# TODO 6: Attach both observers to the sensor
# TODO 7: Set sensor.temperature to 72, then 95, then 68
pass
if __name__ == "__main__":
main()
Step 3 — Knowledge Check
Min. score: 80%
1. In your implementation, TemperatureSensor never imports or references TemperatureLogger or TemperatureAlert by name. How does it communicate with them?
The sensor only knows the abstract Observer interface. It calls update() on each object in self._observers without knowing whether it is a logger, an alert, or something entirely new. In the UML diagram, the aggregation arrow goes from Subject to Observer (abstract) — not to any concrete class. This is loose coupling.
2. In the code you wrote, observers get data by calling subject.temperature inside their update() method. This is called the Pull model. What is the alternative?
In the Push model, the subject passes changed data directly: observer.update(temperature=72). In the Pull model (what you built), the subject passes itself: observer.update(self), and observers query what they need. Pull is more flexible when observers need different subsets of data.
3. What would happen if you call sensor.temperature = 80 but forget to add self.notify() in the setter?
Without self.notify(), the setter silently updates self._temperature but never tells observers. This is a common Observer bug: the state changes, but the world does not know. The @property decorator is just syntactic sugar for getters/setters — it does NOT trigger notifications automatically.
4. In the UML diagram for this step, TemperatureSensor --|> Subject uses a solid line with an open triangle. What does this relationship mean?
In UML, a solid line with an open triangle means inheritance (generalization). TemperatureSensor is a Subject — it inherits attach(), detach(), and notify(). Compare with the dashed line used by TemperatureLogger ..|> Observer, which means “implements interface.”
The Big Refactor: Observer-Powered Weather Station
Why this matters
Knowing the pattern in isolation (Step 3) is different from applying it to messy existing code — and existing code is what you’ll usually inherit on the job. This is where the payoff for Step 1’s pain shows up: adding the next display will require zero edits to WeatherStation, and you’ll see the architectural improvement made visually obvious in the UML diagram.
🎯 You will learn to
- Apply the Observer pattern to refactor the tightly coupled Step 1 code
- Evaluate before/after architectures by comparing both UML diagrams side-by-side
Applying Observer to the Weather Station
Quick Recall
Think back to Steps 1 and 3:
- In Step 1, how many places did you change in
WeatherStationto add one display? - In Step 3, what method did the
TemperatureSensorcall to broadcast changes? - What is the difference between
attach()anddetach()?
Before vs. After
Here is what the architecture looks like after the refactoring. Compare this to Step 1:
Notice: WeatherStation has zero arrows to any display. It only connects to Observer (abstract) through Subject. Any class that implements Observer can plug in — no changes to the station needed.
The Transformation
Open weather_observer.py. The WeatherStation has been refactored to inherit from Subject. Study it and notice:
- It calls
self.notify()innew_measurement()— that is it - It has zero references to any specific display class
CurrentDisplayandStatisticsDisplayare already implemented as Observers
Your Task
-
Complete
ForecastDisplay.update()(TODO 1): Readstation.pressure, compare toself._last_pressure, print the outlook. - Create
HeatIndexDisplay(TODO 2): A brand-new observer.- Formula:
heat_index = temp - 0.55 * (1 - humidity/100) * (temp - 58) - Print:
[Heat] Heat Index: {heat_index:.1f}°F
- Formula:
- Complete
main()(TODOs 3-6):- Attach all four displays to the station
- Run three measurements:
(80, 65, 1013.25),(82, 70, 1012.50),(78, 90, 1015.00) - Then detach the ForecastDisplay and run one more:
(75, 60, 1011.00) - Verify: the forecast should NOT appear in the last measurement
The Key Question
How many lines of WeatherStation did you change to add HeatIndexDisplay? Check the UML diagram — the station’s connections do not change.
Bug Hunt: Debug a Broken Observer
After completing the main task, open bug_hunt.py. This file contains an Observer implementation with three bugs — common mistakes that real developers make. Each bug has a comment marking it. Find and fix all three, then run the tests.
Hint: The three most common Observer bugs are:
- Notification timing (when does
notify()get called relative to the state change?) - Observer registration (is the observer actually attached?)
- Data flow (is the observer reading the right data?)
from abc import ABC, abstractmethod
class Subject:
def __init__(self) -> None:
self._observers: list = []
def attach(self, observer: 'Observer') -> None:
if observer not in self._observers:
self._observers.append(observer)
def detach(self, observer: 'Observer') -> None:
self._observers.remove(observer)
def notify(self) -> None:
for observer in self._observers:
observer.update(self)
class Observer(ABC):
@abstractmethod
def update(self, subject: Subject) -> None:
pass
class ScoreBoard(Subject):
"""Tracks a game score and notifies observers."""
def __init__(self) -> None:
super().__init__()
self._home: int = 0
self._away: int = 0
@property
def home(self) -> int:
return self._home
@property
def away(self) -> int:
return self._away
def update_score(self, home: int, away: int) -> None:
# BUG 1: Something is wrong with the notification timing.
self.notify()
self._home = home
self._away = away
class TVOverlay(Observer):
"""Shows the score on a TV broadcast overlay."""
def __init__(self) -> None:
self.last_display: str = ""
def update(self, subject: Subject) -> None:
self.last_display = f"HOME {subject.home} - {subject.away} AWAY"
print(f"[TV] {self.last_display}")
class MobileAlert(Observer):
"""Sends push notifications to mobile devices."""
def __init__(self) -> None:
self.alerts: list[str] = []
def update(self, subject: Subject) -> None:
# BUG 2: This observer reads hardcoded values instead
# of querying the subject.
msg = f"Score update: 0-0"
self.alerts.append(msg)
print(f"[Mobile] {msg}")
def main() -> None:
board = ScoreBoard()
tv = TVOverlay()
mobile = MobileAlert()
# BUG 3: One observer is not receiving updates.
# Is it attached?
board.attach(tv)
board.update_score(1, 0)
board.update_score(1, 1)
board.update_score(2, 1)
if __name__ == "__main__":
main()
from abc import ABC, abstractmethod
# ── Reusable Subject/Observer base classes ─────────────────────
class Subject:
"""Base class for any observable object."""
def __init__(self) -> None:
self._observers: list = []
def attach(self, observer: 'Observer') -> None:
if observer not in self._observers:
self._observers.append(observer)
def detach(self, observer: 'Observer') -> None:
self._observers.remove(observer)
def notify(self) -> None:
for observer in self._observers:
observer.update(self)
class Observer(ABC):
"""Interface for all observers."""
@abstractmethod
def update(self, subject: Subject) -> None:
pass
# ── The Refactored Weather Station (Subject) ───────────────────
class WeatherStation(Subject):
"""Collects weather data and notifies observers.
COMPARE TO STEP 1: This class knows NOTHING about displays.
It only knows it has observers with an update() method."""
def __init__(self) -> None:
super().__init__()
self._temperature: float = 0.0
self._humidity: float = 0.0
self._pressure: float = 0.0
def new_measurement(self, temp: float, humidity: float,
pressure: float) -> None:
print(f"\n--- Measurement: {temp}°F, {humidity}%, "
f"{pressure} hPa ---")
self._temperature = temp
self._humidity = humidity
self._pressure = pressure
self.notify()
@property
def temperature(self) -> float:
return self._temperature
@property
def humidity(self) -> float:
return self._humidity
@property
def pressure(self) -> float:
return self._pressure
# ── Concrete Observers (Displays) ──────────────────────────────
class CurrentDisplay(Observer):
"""Shows current conditions."""
def update(self, station: Subject) -> None:
print(f"[Current] {station.temperature}°F, "
f"{station.humidity}% humidity")
class StatisticsDisplay(Observer):
"""Shows temperature statistics."""
def __init__(self) -> None:
self._temps: list[float] = []
def update(self, station: Subject) -> None:
self._temps.append(station.temperature)
avg = sum(self._temps) / len(self._temps)
lo, hi = min(self._temps), max(self._temps)
print(f"[Stats] Avg={avg:.1f}°F Min={lo}°F Max={hi}°F")
class ForecastDisplay(Observer):
"""Shows forecast based on pressure changes."""
def __init__(self) -> None:
self._last_pressure: float = 0.0
def update(self, station: Subject) -> None:
# TODO 1: Read station.pressure. Compare to self._last_pressure.
# If pressure > self._last_pressure: outlook = "Improving weather!"
# Elif pressure < self._last_pressure: outlook = "Cooler, rainy weather"
# Else: outlook = "More of the same"
# Update self._last_pressure. Print: f"[Forecast] {outlook}"
pass
# TODO 2: Create HeatIndexDisplay(Observer) here.
# update() should compute:
# heat_index = temp - 0.55 * (1 - humidity/100) * (temp - 58)
# Print: f"[Heat] Heat Index: {heat_index:.1f}°F"
def main() -> None:
station = WeatherStation()
# TODO 3: Create all four display instances
# TODO 4: Attach all four to the station
# TODO 5: Run three measurements:
# station.new_measurement(80, 65, 1013.25)
# station.new_measurement(82, 70, 1012.50)
# station.new_measurement(78, 90, 1015.00)
# TODO 6: Detach the ForecastDisplay, then run one more:
# print("\n>>> Detaching ForecastDisplay...")
# station.detach(forecast)
# station.new_measurement(75, 60, 1011.00)
# (The forecast should NOT appear in this last measurement!)
pass
if __name__ == "__main__":
main()
Step 4 — Knowledge Check
Min. score: 80%
1. How many lines of the WeatherStation class did you change to add HeatIndexDisplay?
Zero. That is the entire point. In Step 1, you had to modify WeatherStation in 2 places. With the Observer pattern, you create a new class that implements Observer and call station.attach(). The station never needs to know about HeatIndexDisplay by name. Check the UML: WeatherStation’s connections did not change.
2. Which of the following are advantages of the Observer pattern compared to the tightly coupled version? (Select all that apply) (select all that apply)
The first three are genuine advantages. However, the code is not always shorter — the Observer version has more classes and lines of code. The benefit is flexibility and maintainability, not brevity. Look at the UML diagrams: the Observer version has more boxes, but the arrows tell a cleaner story.
3. A teammate says: “Why do we need abstract classes? I can just call display methods directly — it is simpler.” What is the best response?
Both approaches have their place. For a simple program with 2-3 displays that never change, direct calls are simpler. But Observer shines when: (1) the set of observers changes at runtime, (2) new types are added frequently, or (3) the subject and observers evolve independently. Patterns are tools for specific situations, not universal requirements.
4. In the Observer UML diagram, Subject o--> "*" Observer uses a diamond arrow with a multiplicity label. What does this mean?
The open diamond represents aggregation: the Subject has a collection of Observers, but the Observers can exist independently of the Subject. The "*" multiplicity means “zero or more.” A filled diamond would be composition (stronger ownership). Observers are loosely associated — they can be attached to or detached from the Subject freely.
Design Judgment: When and When Not
Why this matters
Junior engineers who just learned a pattern apply it everywhere; senior engineers know when not to. Observer adds classes, indirection, and debugging pain — costs that only pay off when the relationship is genuinely one-to-many and dynamic. Without this judgment step you’d leave the tutorial with a hammer that makes everything look like a nail.
🎯 You will learn to
- Evaluate whether the Observer pattern is appropriate for a given scenario
- Analyze the Push vs. Pull trade-off and pick the model that fits the situation
- Recognize over-engineering as a real cost, not a stylistic preference
When Observer Is the Right Tool — and When It Is Not
Quick Recall
Before learning when to use Observer, recall what it costs:
- How many more classes did the Observer version have compared to Step 1?
- What was the hardest part of debugging in Step 4’s Bug Hunt?
- What is the main benefit that justified that extra complexity?
The Danger of Over-Engineering
Design patterns are powerful, but they are not free. Every pattern adds:
- More classes and interfaces (look at how many UML boxes we added)
- Indirection that makes debugging harder (you cannot “follow” a method call from Subject to Observer by reading the code linearly)
- Cognitive overhead for anyone reading the code
The worst mistake after learning design patterns is applying them everywhere:
“The best engineers focus on solving problems simply, not memorizing existing solutions.”
When Observer Is Worth It
| Use Observer when… | Skip Observer when… |
|---|---|
| One state change triggers updates in multiple objects | There is only one receiver that will never change |
| Observers need to be added/removed at runtime | The relationship is fixed at design time |
| Subject and observers should evolve independently | Debugging is critical and traceability matters most |
| You expect new observer types over time | A simple direct method call would suffice |
Push vs. Pull: A Design Decision
You have used the Pull model (observers call subject.temperature). Here is the alternative:
| Model | How it works | Pros | Cons |
|---|---|---|---|
| Pull | update(subject) — observer queries what it needs |
Flexible; observers choose their data | More coupling to subject API |
| Push | update(temp=80, humidity=65) — subject sends everything |
Observers are simpler and independent | Subject must guess what observers need |
Generate Your Own Heuristic First
Before looking at the scenarios, write down your own rule for when Observer is worth the complexity. Try to complete this sentence:
“Use the Observer pattern when __, but skip it when __.”
Hold onto your answer — you will test it against five scenarios and refine it.
Your Task — Scenario Analysis
Open design_judgment.py. For each scenario, apply your heuristic first, then set the answer to True or False and run to check. If your heuristic gets one wrong, revise it.
# For each scenario, set the answer to True or False.
# True = Observer IS a good fit
# False = Observer is OVERKILL or wrong for this case
# --- Scenario 1 ---
# A stock trading app shows live prices on a chart, a portfolio
# summary, a news ticker, and push notifications. New display
# widgets are added every sprint. Users configure their dashboard
# dynamically.
scenario_1 = None # TODO: True or False?
# --- Scenario 2 ---
# A simple calculator app has one display that shows the result.
# There will never be a second display. The display is created
# once when the app starts and never changes.
scenario_2 = None # TODO: True or False?
# --- Scenario 3 ---
# A game engine physics system detects collisions. Multiple
# systems react: sound effects, particles, damage, achievements.
# Game modders should add collision handlers without editing
# the physics engine source code.
scenario_3 = None # TODO: True or False?
# --- Scenario 4 ---
# A script reads a config file once at startup and passes the
# values to three functions. The config never changes during
# execution.
scenario_4 = None # TODO: True or False?
# --- Scenario 5 ---
# A chat application notifies all connected users when a new
# message arrives. Users join and leave the chat dynamically
# throughout the session.
scenario_5 = None # TODO: True or False?
# --- Verification (do not edit below) ---
answers = {
"Scenario 1 (Stock trading dashboard)": (scenario_1, True),
"Scenario 2 (Simple calculator)": (scenario_2, False),
"Scenario 3 (Game collision events)": (scenario_3, True),
"Scenario 4 (One-time config read)": (scenario_4, False),
"Scenario 5 (Chat notifications)": (scenario_5, True),
}
correct = 0
for name, (student, expected) in answers.items():
if student is None:
print(f" [ ] {name} -- not answered yet")
elif student == expected:
print(f" [+] {name} -- Correct!")
correct += 1
else:
why = "Observer fits here" if expected else "Too simple for Observer"
print(f" [X] {name} -- Incorrect. {why}.")
print(f"\nScore: {correct}/{len(answers)}")
Step 5 — Knowledge Check
Min. score: 80%1. A developer learns Observer and starts using it for every function call. What is this anti-pattern called?
This is over-engineering, one of the most common mistakes after learning design patterns. Patterns add complexity (more classes, more indirection, harder debugging). They are worth it when the problem genuinely needs flexibility — but using Observer for a simple one-to-one method call is like using a sledgehammer to hang a picture frame.
2. What is the main trade-off of the Observer pattern compared to direct method calls?
The Observer pattern trades traceability for flexibility. You gain loose coupling (subject and observers evolve independently) and dynamic attach/detach. But you lose the ability to “follow the function call” from subject to observer — the indirection through the observer list makes control flow implicit. This matters during debugging.
3. In the Pull model, update(subject) passes the entire subject. In the Push model, update(temp=80, humidity=65) passes data directly. Which statement is correct?
Both models have trade-offs. Pull is flexible (observers pick what they need) but couples them to the subject’s getters. Push decouples observers from the subject (they just receive data) but the subject must guess what observers need. The choice depends on observer diversity — see the UML diagram comparing both approaches.
4. You are debugging a bug where a UI widget shows stale data. The widget is an Observer of a data model. What is the MOST likely cause?
The most common Observer bug is forgetting to attach() the observer. If the widget was never registered, it never receives update() calls. Other common issues: (1) the widget was detach()-ed unintentionally, (2) the subject changed state but notify() was not called, or (3) the update() method has a bug.
5. Consider this code:
station.attach(current_display)
station.attach(stats_display)
station.new_measurement(80, 65, 1013)
current_display guaranteed to receive the update BEFORE stats_display?
Our implementation uses a Python list, which preserves insertion order. But the Observer pattern itself does not guarantee order — different implementations might use sets, priority queues, or async dispatch. Your code should never depend on notification order between observers, because a future refactoring could change it.
6. (Interleaved — Implementation recall) Trace this code mentally. What does it print?
sensor = TemperatureSensor()
logger = TemperatureLogger()
sensor.temperature = 50
sensor.attach(logger)
sensor.temperature = 70
The logger is attached after the first temperature change. Since notify() only fires during sensor.temperature = ..., and the logger was not yet in the observer list when 50 was set, it only sees 70. This is a common source of bugs: observers only receive notifications from the point of attachment onward — they do not get historical state.
7. Looking at the UML diagrams from this tutorial, what is the most significant structural difference between the Step 1 (tightly coupled) and Step 4 (Observer) architectures?
The critical UML difference: in Step 1, WeatherStation has a concrete dependency arrow to every display class. In Step 4, it connects only to the abstract Observer interface (via aggregation from Subject). The concrete displays implement Observer independently. This means you can add, remove, or change displays without touching the station. The UML makes this architectural improvement visually obvious.
Comprehensive Assessment
Why this matters
A cumulative, interleaved quiz is one of the highest-leverage learning interventions there is — it forces you to discriminate between concepts that felt distinct in isolation but blur together in practice. If you can answer questions that mix tight coupling, UML, implementation, refactoring, and trade-offs without warning, the pattern has actually transferred into long-term memory.
🎯 You will learn to
- Apply Observer concepts across all earlier steps without scaffolding
- Analyze your own gaps and revisit specific steps where retrieval fails
Test Your Understanding
This is a cumulative quiz. The questions are drawn from every step and require you to synthesize what you have learned. You need at least 80% to pass.
Take your time. If a question stumps you, think about which step covered that concept and what you learned there.
Step 6 — Knowledge Check
Min. score: 80%
1. (Tight Coupling — Step 1)
In the original tightly coupled WeatherStation, adding a new display required modifying the station in two places. Which design principle does this violate?
The Open/Closed Principle states that software entities should be open for extension but closed for modification. The tightly coupled station forces you to modify its code every time you extend the system with a new display. The Observer pattern fixes this: new displays extend the system by implementing the Observer interface — no station modification needed.
2. (Observer Concepts — Step 2) In the YouTube analogy, a subscriber clicks “Unsubscribe.” Which Observer pattern method does this correspond to?
Unsubscribing corresponds to detach() — removing the observer from the subject’s notification list. After detaching, the observer no longer receives update() calls when the subject’s state changes. This is one of the key advantages of Observer: subscriptions are dynamic.
3. (Implementation — Step 3)
You implement a TemperatureSensor that inherits from Subject. The temperature property setter stores the new value but you forget to call self.notify(). What symptom will you observe?
Without self.notify(), the state changes silently. Observers are never called, so they never learn about the new temperature. This is one of the most common Observer bugs. The @property decorator is just syntactic sugar for getters and setters — it has no notification mechanism built in.
4. (Refactoring — Step 4)
After refactoring the weather station to use Observer, you need to add a UVIndexDisplay. How many lines of WeatherStation do you change?
Zero. This is the core benefit. You create class UVIndexDisplay(Observer) with its own update() method, then call station.attach(uv_display) from client code. The station class is never touched. Compare this to Step 1 where every new display required editing the station in two places.
5. (Design Judgment — Step 5) A logging system writes every application event to a single log file. There is exactly one log writer, and it will never change. A teammate suggests using the Observer pattern. What is the best response?
Observer solves the problem of dynamic, one-to-many dependencies. With exactly one fixed log writer that never changes, a direct call is simpler, clearer, and easier to debug. Adding Observer here would be over-engineering — adding indirection and complexity without addressing a real architectural need.
6. (UML Synthesis)
In the Step 4 UML diagram, WeatherStation --|> Subject and CurrentDisplay ..|> Observer. Why are the arrow styles different?
In UML class diagrams, a solid line with hollow triangle denotes generalization (inheritance). A dashed line with hollow triangle denotes realization (implementing an interface or abstract class). WeatherStation inherits concrete behavior from Subject. CurrentDisplay implements the abstract Observer contract.
7. (Architecture Synthesis)
Your tutorial implementation uses the Pull model: update(self, subject) where observers call subject.temperature. A colleague proposes switching to Push: update(self, temp, humidity, pressure). What trade-off does this introduce?
Push decouples observers from the subject’s getter API (they don’t need a reference back). But it couples the notification signature to whatever the subject decides to push. If observers have diverse data needs, the subject either pushes too much data or must maintain multiple notification methods. The Hybrid model (push an event type, let observers pull selectively) is the most common real-world approach.
8. (Debugging Synthesis)
A weather display shows stale data. You verify the display is attached and update() looks correct. The new_measurement() method stores new values but the display still shows old data. What is the most likely cause?
If self.notify() is called before updating the internal state, observers pull stale values. The correct order is: (1) update state, (2) call self.notify(). This is a subtle but critical bug — the notification must happen after the state change, not before.
9. (Comprehensive review)
Rank these three architectures from most to least coupled:
(A) WeatherStation directly calls all display .show() methods
(B) WeatherStation uses Observer with Pull model (update(subject))
(C) WeatherStation uses Observer with Push model (update(temp, humidity, pressure))
(A) is the most coupled — the station knows every concrete display by name. (B) Pull is less coupled — the station only knows the Observer interface, but observers hold a reference back to the subject. (C) Push is the least coupled — observers receive data directly and need no reference to the subject at all. However, Push has its own trade-off: the subject must anticipate what data observers need.
10. (Comprehensive review) Which of the following are consequences of the Observer pattern described by the GoF? (Select all that apply) (select all that apply)
The first three are all documented consequences. Loose coupling is the primary benefit. Dynamic relationships enable runtime flexibility. Unexpected cascading updates are the main drawback — observer A’s update can trigger subject B, which notifies observer C, creating hard-to-trace chains. The pattern does not guarantee notification order — that is implementation-dependent.
Summary and Key Takeaways
Why this matters
A tutorial without a closing summary leaves the schema half-built. This step exists so you can see your six-step journey laid out as one mental model — tight coupling, vocabulary, construction, refactor, judgment, assessment — and walk away with a single UML snapshot you can recall later when you encounter the next one-to-many problem in real code.
🎯 You will learn to
- Evaluate your own learning by mapping each step’s takeaway onto a single coherent narrative
- Apply the Observer pattern with confidence the next time a one-to-many requirement appears
What You Learned
Congratulations — you have completed the Observer Design Pattern tutorial. Here is a summary of your journey:
Step-by-Step Recap
| Step | What You Did | Key Insight |
|---|---|---|
| 1. Tight Coupling | Added a display to a hard-coded weather station | Every new display forced changes to the station — violating the Open/Closed Principle |
| 2. Concepts | Learned pattern vocabulary and the YouTube analogy | Observer = Subject + Observers + dynamic subscribe/unsubscribe |
| 3. Build It | Implemented Observer from scratch with a temperature sensor | The Subject never imports or names concrete observers — it only knows the interface |
| 4. Refactor | Applied Observer to the original weather station | Adding HeatIndexDisplay required zero changes to WeatherStation |
| 5. Design Judgment | Evaluated when Observer helps and when it is overkill | Use Observer for dynamic, one-to-many relationships — not for every function call |
| 6. Assessment | Proved your understanding across all concepts | Synthesis across tight coupling, UML, implementation, refactoring, and trade-offs |
The Observer Pattern at a Glance
Observer Code in Common Languages
The interactive tutorial uses Python, but the same roles appear in Java, C++, Python, and JavaScript. The syntax changes; the design responsibility split does not.
Java
import java.util.ArrayList;
import java.util.List;
interface Observer {
void update(WeatherStation station);
}
final class WeatherStation {
private final List<Observer> observers = new ArrayList<>();
private float temperature;
void attach(Observer observer) {
observers.add(observer);
}
void setTemperature(float temperature) {
this.temperature = temperature;
observers.forEach(observer -> observer.update(this));
}
float getTemperature() {
return temperature;
}
}
final class CurrentDisplay implements Observer {
public void update(WeatherStation station) {
System.out.println("Current: " + station.getTemperature());
}
}
C++
#include <iostream>
#include <vector>
class WeatherStation;
struct Observer {
virtual ~Observer() = default;
virtual void update(const WeatherStation& station) = 0;
};
class WeatherStation {
public:
void attach(Observer& observer) {
observers_.push_back(&observer);
}
void setTemperature(float temperature) {
temperature_ = temperature;
for (auto* observer : observers_) {
observer->update(*this);
}
}
float temperature() const {
return temperature_;
}
private:
std::vector<Observer*> observers_;
float temperature_ = 0.0f;
};
class CurrentDisplay : public Observer {
public:
void update(const WeatherStation& station) override {
std::cout << "Current: " << station.temperature() << "\n";
}
};
Python
from abc import ABC, abstractmethod
class Observer(ABC):
@abstractmethod
def update(self, station: "WeatherStation") -> None:
pass
class WeatherStation:
def __init__(self) -> None:
self._observers: list[Observer] = []
self._temperature = 0.0
def attach(self, observer: Observer) -> None:
self._observers.append(observer)
def set_temperature(self, temperature: float) -> None:
self._temperature = temperature
for observer in self._observers:
observer.update(self)
@property
def temperature(self) -> float:
return self._temperature
class CurrentDisplay(Observer):
def update(self, station: WeatherStation) -> None:
print(f"Current: {station.temperature}")
JavaScript
class WeatherStation {
constructor() {
this.observers = [];
this.temperature = 0;
}
attach(observer) {
this.observers.push(observer);
}
setTemperature(temperature) {
this.temperature = temperature;
this.observers.forEach((observer) => observer.update(this));
}
}
class CurrentDisplay {
update(station) {
console.log(`Current: ${station.temperature}`);
}
}
Key Design Decisions
| Decision | Options | When to Use |
|---|---|---|
| Data flow | Pull (observers query subject) vs. Push (subject sends data) vs. Hybrid | Pull for diverse observers; Push for uniform data needs; Hybrid for real-world frameworks |
| Notification trigger | Automatic (in setter) vs. Client-triggered vs. Batched | Automatic for simplicity; Batched for performance with frequent updates |
| Observer lifecycle | Explicit detach vs. Weak references vs. Scoped subscriptions | Explicit for simple cases; Weak refs for long-lived subjects; Scoped for UI frameworks |
Common Pitfalls to Watch For
- Forgetting
notify()— State changes silently; observers show stale data - Forgetting
attach()— Observer exists but never receives updates - Notification before state update — Observers pull old values
- Cascading notifications — Observer A triggers Subject B, which notifies Observer C — hard to debug
- Lapsed listener problem — Observers that are never detached cause memory leaks
- Over-engineering — Using Observer where a simple direct call would suffice
What to Explore Next
- Strategy Pattern — Another behavioral pattern that uses interfaces to swap algorithms at runtime
- Mediator Pattern — Centralizes complex communication between multiple objects (reduces many-to-many to many-to-one)
- Event-driven frameworks — Python’s
asyncio, JavaScript’sEventEmitter, and reactive programming (RxPy) all build on Observer concepts - Real-world Observer — Study how MVC frameworks (Django, React) implement the Observer pattern under the hood
# You completed the Observer Design Pattern tutorial!
#
# Key takeaway: The Observer pattern creates loose coupling
# by letting objects subscribe to state changes through
# a common interface — without the subject knowing the
# concrete types of its observers.
#
# Run this file to see your summary.
print("=" * 55)
print(" Observer Design Pattern — Tutorial Complete!")
print("=" * 55)
print()
print(" You can now:")
print(" [x] Identify tight coupling and its maintenance cost")
print(" [x] Explain the Observer pattern using real-world analogies")
print(" [x] Implement Subject, Observer, attach, detach, notify")
print(" [x] Refactor tightly coupled code into Observer architecture")
print(" [x] Judge when Observer is appropriate vs. overkill")
print(" [x] Distinguish Push vs. Pull notification models")
print(" [x] Read Observer relationships in UML class diagrams")
print()
print(" Next patterns to explore: Strategy, Mediator, Decorator")
print("=" * 55)