C++ Design Patterns: Observer – Building a Publish-Subscribe Weather Forecast System

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:

  1. Subject: <span>ISubject</span> interface. It provides methods for attaching, detaching, and notifying observers. It is the abstract template of our “bulletin board”.

  2. 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.

  3. Observer: <span>IObserver</span> interface. It defines an <span>update</span> method for the subject to call when the state changes.

  4. 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.

  1. 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.

  2. Broadcast Communication: The subject can broadcast notifications to any number of observers without caring who they are or how many there are.

  3. 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.

Leave a Comment