Skip to Content
Course content

210: Practice Exercise: Implementing a Simple Observer-Based Event System

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've noticed a recurring pattern when developers first try to build event systems: they treat the "Subject" (the thing being watched) as a manager that knows exactly who is listening and what those listeners actually do. They think of it as a coordinator rather than a broadcaster.

The Myth: The Subject should call specific methods on specific classes

If you're building a game, you might think, "Okay, when the Player takes damage, the Player class should call UI::updateHealthBar(), SoundEngine::playOuchSound(), and AchievementSystem::checkForDeath()." On the surface, this works. It's direct and easy to trace in a debugger. But here is where it falls apart: the moment you want to add a CameraShake effect or a LogSystem, you have to go back into your Player class and add yet another function call and another header include.

Your Player class—which should only care about gameplay logic—is now suddenly burdened with knowing about the UI, the audio engine, and the achievement system. You've created a "God Object" that is tightly coupled to every other system in your project. If you delete the SoundEngine class, your Player class won't even compile.

The Reality: Broadcasters don't care who is listening

The core of the Observer pattern is decoupling. The Subject shouldn't know who is observing it, only that the observer adheres to a specific "contract" (an interface). Instead of the Subject calling updateHealthBar(), it should simply tell anyone who is interested: "Hey, something happened, here is the data."

In C++, we achieve this by creating an abstract base class—an interface. Here is how I usually structure this to keep it clean:


#include <vector>
#include <algorithm>
#include <iostream>

// The "Contract"
class IObserver {
public:
    virtual ~IObserver() = default;
    virtual void onNotify(int value) = 0; 
};

// The Broadcaster
class Subject {
    std::vector<IObserver*> observers;

public:
    void addObserver(IObserver* observer) {
        observers.push_back(observer);
    }

    void removeObserver(IObserver* observer) {
        // Use the erase-remove idiom to keep the list clean
        observers.erase(std::remove(observers.begin(), observers.end(), observer), observers.end());
    }

    void notify(int value) {
        for (auto* observer : observers) {
            observer->onNotify(value);
        }
    }
};

// Concrete implementation 1: The UI
class HealthBar : public IObserver {
public:
    void onNotify(int value) override {
        std::cout < "UI: Updating health bar to " < value < "%" < std::endl;
    }
};

// Concrete implementation 2: The Sound System
class SoundEngine : public IObserver {
public:
    void onNotify(int value) override {
        if (value < 20) {
            std::cout < "Sound: Playing 'Low Health' warning beep!" < std::endl;
        }
    }
};

Now, look at the Subject class. Does it mention HealthBar? Does it include SoundEngine.h? No. It doesn't care. You can add a hundred different observers—logging systems, analytics trackers, particle effects—and you will never have to touch a single line of code inside the Subject class again. That is the power of this pattern.

One quick word of caution: in a real production environment, you have to be very careful about the lifetime of your observers. If a HealthBar object is destroyed but forgot to call removeObserver(), the Subject will eventually try to call onNotify() on a dangling pointer, and your program will crash. In a more advanced system, I'd suggest using std::weak_ptr, but for this exercise, we'll stick to raw pointers to keep the logic focused on the pattern itself.




📋 Practical Task

Implementation Exercise: Building a Stock Market Alert System

To put this into practice, you're going to build a simple Stock Market simulator. You need to implement an event system where a StockTicker (the Subject) notifies various Investor types (the Observers) whenever a stock price changes.

Requirements:

  • Create an IStockObserver interface with a method updatePrice(std::string symbol, double price).
  • Implement a StockTicker class that:
    • Maintains a list of observers.
    • Has methods to attach() and detach() observers.
    • Has a setPrice(std::string symbol, double price) method that triggers the notification to all attached observers.
  • Implement two concrete observers:
    • MobileApp: Prints a message like "Push Notification: [Symbol] is now $[Price]".
    • DayTrader: Only prints a message if the price drops below a certain threshold (e.g., $100.00), indicating a "Buy" opportunity.

Test Case: In your main(), create one StockTicker, attach one MobileApp and one DayTrader, and call setPrice("AAPL", 150.00) and then setPrice("AAPL", 95.00). Verify that the DayTrader only reacts to the second update.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.