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.
Learning objective: After this step you will be able to identify the symptoms of tightly coupled code and explain why direct dependencies create maintenance headaches.
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 |
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.”
Open weather_station.py and:
HeatIndexDisplay class with a show(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 WeatherStation so it gets called whenever new data arrives.
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.
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)
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.
Learning objective: After this step you will be able to define what a design pattern is and describe the Observer pattern using a real-world analogy and its UML structure.
Think about how YouTube notifications work:
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 |
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.
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:
--|>) = inheritance (“is-a”)..|>) = implements interfaceo-->) = aggregation (“has-a”, loosely owned)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.
Look at pattern_preview.py. Before running it, predict:
DashboardA always print before DashboardB?Run it to check your predictions.
# 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)
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.
Learning objective: After this step you will be able to implement the Observer pattern from scratch, identify each UML role in your code, and watch the class diagram update live.
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 |
Open observer_basics.py. The Subject and Observer base classes are fully implemented for you. Your job:
Complete TemperatureSensor (TODO 1): The temperature property setter should store the new value AND call self.notify()
Complete TemperatureLogger (TODO 2): Its update() should read subject.temperature, append it to self.readings, and print it
Complete TemperatureAlert (TODO 3): Its update() 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
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()
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.”
Learning objective: After this step you will be able to refactor tightly coupled code into the Observer pattern, demonstrate dynamic attach/detach, and compare the two architectures using UML diagrams.
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.
Open weather_observer.py. The WeatherStation has been refactored to inherit from Subject. Study it and notice:
self.notify() in new_measurement() — that is itCurrentDisplay and StatisticsDisplay are already implemented as ObserversComplete ForecastDisplay.update() (TODO 1): Read station.pressure, compare to self._last_pressure, print the outlook.
HeatIndexDisplay (TODO 2): A brand-new observer.
heat_index = temp - 0.55 * (1 - humidity/100) * (temp - 58)[Heat] Heat Index: {heat_index:.1f}°Fmain() (TODOs 3-6):
(80, 65, 1013.25), (82, 70, 1012.50), (78, 90, 1015.00)(75, 60, 1011.00)How many lines of WeatherStation did you change to add HeatIndexDisplay? Check the UML diagram — the station’s connections do not change.
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()
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--> "0..*" Observer uses a diamond arrow with a multiplicity label. What does this mean?
The open diamond (o-->) represents aggregation: the Subject has a collection of Observers, but the Observers can exist independently of the Subject. The "0..*" 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.
Learning objective: After this step you will be able to evaluate whether the Observer pattern is appropriate for a given scenario, explain the Push vs. Pull trade-off, and recognize the danger of over-engineering.
Design patterns are powerful, but they are not free. Every pattern adds:
The worst mistake after learning design patterns is applying them everywhere:
“The best engineers focus on solving problems simply, not memorizing existing solutions.”
| 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 |
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 |
Open design_judgment.py. For each scenario, decide whether Observer is a good fit (True) or overkill (False). Set the variable and run to check.
# 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)}")
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. 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.