C++ Design Patterns: Observer – Building a Publish-Subscribe Weather Forecast System
Decouple your notification mechanism to keep objects “automatically” in sync
Introduction: Isolated Information Updates
Hello, C++ developers.
Imagine you are building a weather monitoring system. The central weather station (<span>WeatherStation</span>) is responsible for collecting the latest meteorological data (temperature, humidity, pressure). At the same time, you have multiple different display panels that need to show this data:
-
A Current Conditions Panel (
<span>CurrentConditionsDisplay</span>), displaying real-time data. -
A Statistics Panel (
<span>StatisticsDisplay</span>), showing maximum/minimum/average temperature. -
A Forecast Panel (
<span>ForecastDisplay</span>), predicting the weather based on pressure changes.
A straightforward, “hard-coded” design might look like this:
class WeatherStation {
public:
void dataChanged() {
// When data is updated, WeatherStation must explicitly call the update method of each display panel
m_currentDisplay.update(m_temperature, m_humidity);
m_statsDisplay.update(m_temperature);
m_forecastDisplay.update(m_pressure);
}
private:
float m_temperature, m_humidity, m_pressure;
// WeatherStation must hold instances of all display panels
CurrentConditionsDisplay m_currentDisplay;
StatisticsDisplay m_statsDisplay;
ForecastDisplay m_forecastDisplay;
};
What are the issues with this design?
-
High Coupling:
<span>WeatherStation</span>(subject) and all display panels (observers) are tightly bound together. The weather station must “know” every display panel that needs to be notified. -
Violating the Open-Closed Principle: If you want to add a new display panel (like a “Disaster Warning Panel”), you must modify
<span>WeatherStation</span>class’s<span>dataChanged</span>method to add new call code. -
Lack of Flexibility: If you want to dynamically add or remove a display panel at runtime, it will be very cumbersome to implement.
This design is like a “control freak” boss who must personally call each employee to notify them of a meeting. What we really want is a “publish-subscribe” system: the boss only needs to post a notice on the bulletin board, and all subscribed employees will automatically receive the message.
This elegant “publish-subscribe” model is the Observer Pattern.
Act One: Core Roles of the Observer Pattern
The Observer Pattern defines a one-to-many dependency relationship between objects. When the state of one object changes, all objects that depend on it are notified and automatically updated. This pattern includes four core roles:
-
Subject:
<span>ISubject</span>interface. It provides methods for attaching, detaching, and notifying observers. It is the abstract template of our “bulletin board”. -
Concrete Subject:
<span>WeatherStation</span>. It implements the<span>ISubject</span>interface, maintains a list of observers, and calls the<span>notify</span>method to notify all registered observers when its state changes. -
Observer:
<span>IObserver</span>interface. It defines an<span>update</span>method for the subject to call when the state changes. -
Concrete Observer:
<span>CurrentConditionsDisplay</span>etc. It implements the<span>IObserver</span>interface and defines the specific logic to execute upon receiving a notification in the<span>update</span>method.
Relationship Diagram: <span>ConcreteSubject</span> holds a list of <span>IObserver</span> -> When <span>ConcreteSubject</span> changes state, it iterates through the list and calls each <span>IObserver</span>‘s <span>update()</span> method -> <span>ConcreteObserver</span> executes its own update logic in <span>update()</span>.
Act Two: Code Implementation – Building Our Weather Forecast System
Now, let’s implement this decoupled weather system using C++ code.
Step 1: Define Subject and Observer Interfaces
This is the contract layer of the pattern, defining the interaction rules between both parties.
#include <iostream>
#include <vector>
#include <memory>
#include <algorithm>
// Observer interface
class IObserver {
public:
virtual ~IObserver() = default;
virtual void update(float temp, float humidity, float pressure) = 0;
};
// Subject interface
class ISubject {
public:
virtual ~ISubject() = default;
virtual void attach(std::shared_ptr<IObserver> observer) = 0;
virtual void detach(std::shared_ptr<IObserver> observer) = 0;
virtual void notify() = 0;
};
Step 2: Implement Concrete Subject (WeatherStation)
<span>WeatherStation</span> is now only responsible for maintaining the observer list and issuing notifications when data changes.
// Concrete Subject
class WeatherStation : public ISubject {
public:
void attach(std::shared_ptr<IObserver> observer) override {
m_observers.push_back(observer);
}
void detach(std::shared_ptr<IObserver> observer) override {
// C++20's std::erase_if would be cleaner
auto it = std::remove(m_observers.begin(), m_observers.end(), observer);
m_observers.erase(it, m_observers.end());
}
void notify() override {
for (const auto& observer : m_observers) {
observer->update(m_temperature, m_humidity, m_pressure);
}
}
// When the weather station data is updated, call notify
void setMeasurements(float temp, float humidity, float pressure) {
m_temperature = temp;
m_humidity = humidity;
m_pressure = pressure;
measurementsChanged();
}
private:
void measurementsChanged() {
notify();
}
std::vector<std::shared_ptr<IObserver>> m_observers;
float m_temperature = 0.0f;
float m_humidity = 0.0f;
float m_pressure = 0.0f;
};
Decoupling Manifestation: <span>WeatherStation</span>’s code does not contain any specific display panel (like <span>CurrentConditionsDisplay</span>) references! It only interacts with the abstract <span>IObserver</span> interface.
Step 3: Implement Concrete Observers (Display Panels)
Each display panel implements the <span>IObserver</span> interface and defines its own update logic.
// Concrete Observer 1: Current Conditions Panel
class CurrentConditionsDisplay : public IObserver {
public:
void update(float temp, float humidity, float pressure) override {
m_temperature = temp;
m_humidity = humidity;
display();
}
private:
void display() {
std::cout << "[Current Conditions] Temp: " << m_temperature
<< " F, Humidity: " << m_humidity << "%\n";
}
float m_temperature;
float m_humidity;
};
// Concrete Observer 2: Statistics Panel
class StatisticsDisplay : public IObserver {
public:
void update(float temp, float humidity, float pressure) override {
m_tempSum += temp;
m_numReadings++;
if (temp > m_maxTemp) m_maxTemp = temp;
if (temp < m_minTemp) m_minTemp = temp;
display();
}
private:
void display() {
std::cout << "[Statistics] Avg/Max/Min temperature = " << (m_tempSum / m_numReadings)
<< "/" << m_maxTemp << "/" << m_minTemp << "\n";
}
float m_maxTemp = 0.0f;
float m_minTemp = 200.0f;
float m_tempSum = 0.0f;
int m_numReadings = 0;
};
Step 4: Client – Assembling Everything
The client is responsible for creating the subject and observers and “subscribing” them together.
int main() {
// 1. Create the subject (weather station)
auto weatherStation = std::make_shared<WeatherStation>();
// 2. Create multiple observers (display panels)
auto currentDisplay = std::make_shared<CurrentConditionsDisplay>();
auto statsDisplay = std::make_shared<StatisticsDisplay>();
// 3. Register (subscribe) observers to the subject
weatherStation->attach(currentDisplay);
weatherStation->attach(statsDisplay);
// 4. Simulate weather data changes
std::cout << "--- Weather data update 1 ---\n";
weatherStation->setMeasurements(80, 65, 30.4f);
std::cout << "\n--- Weather data update 2 ---\n";
weatherStation->setMeasurements(82, 70, 29.2f);
// 5. Dynamically remove an observer
std::cout << "\n--- Statistics display is detached ---\n";
weatherStation->detach(statsDisplay);
std::cout << "\n--- Weather data update 3 ---\n";
weatherStation->setMeasurements(78, 90, 29.2f);
return 0;
}
Output:
--- Weather data update 1 ---
[Current Conditions] Temp: 80 F, Humidity: 65%
[Statistics] Avg/Max/Min temperature = 80/80/80
--- Weather data update 2 ---
[Current Conditions] Temp: 82 F, Humidity: 70%
[Statistics] Avg/Max/Min temperature = 81/82/80
--- Statistics display is detached ---
--- Weather data update 3 ---
[Current Conditions] Temp: 78 F, Humidity: 90%
From the output, we can clearly see:
-
Each time
<span>setMeasurements</span>is called, all registered observers are automatically updated. -
When
<span>statsDisplay</span>is<span>detach</span>ed, subsequent updates will no longer notify it.
Conclusion: The Value of the Observer Pattern
The core value of the Observer Pattern lies in Loose Coupling.
-
Decoupling Subject and Observer: The subject only knows it has a series of objects implementing the
<span>IObserver</span>interface, but does not know their specific types. This allows both parties to change and reuse independently. -
Broadcast Communication: The subject can broadcast notifications to any number of observers without caring who they are or how many there are.
-
Dynamic Relationships: Observers can be added and removed dynamically at runtime, providing great flexibility.
When to Use the Observer Pattern?
- When a change in one object requires changes in other objects, and you do not know how many objects need to change.
- When one object must notify other objects, and you want to minimize tight coupling between them.
-
When building any event-driven system, such as user interfaces (click events), message queues, game logic (character state change notifications), etc.
The Observer Pattern is a fundamental building block for constructing responsive, scalable systems. Mastering it gives you a powerful tool for elegantly handling inter-object relationships in complex systems.