This article explores how to design a robust Observer pattern in C++ using modern features, with small code snippets to illustrate the concepts.
Why the Observer Pattern?
In an event-driven system, one component will react to other independent events. For example, a button click might trigger a change in view, display, etc. Without a proper Observer pattern, the calls that are made to perform these changes can lead to:
- Tight coupling: The subject must know about each observer, making the system harder to change.
- Low Reusability: Observers are specific to one subject and can’t be easily reused elsewhere.
The observer pattern addresses these issues by using the concept of indirect communication. The subject has a list of observers that it will notify, without knowing their types.
Core Components of the Observer Pattern
- Subject (Observable): The object that maintains a list of its observers and notifies them of state changes. It provides interfaces for attaching, detaching, and notifying observers.
- Observer: A basic setup that lets an object get updates when something changes.
- Concrete Subject: The actual object that holds data and tells observers when something changes.
- Concrete Observer: The object that responds when the subject sends an update.
Designing a Robust Observer Pattern in C++
Basic Interface design
Code Snippet
#include<iostream> #include<vector> #include <algorithm> using namespace std; class Observer { public: virtual void onNotify(const string& message) = 0; virtual ~Observer() = default; }; class Subject { vector<Observer*> observers; public: void attach(Observer* obs) { observers.push_back(obs); } void detach(Observer* obs) { observers.erase(remove(observers.begin(), observers.end(), obs), observers.end()); } protected: void notify(const string& msg) { for (auto* obs : observers) { obs->onNotify(msg); } } };
Explaination
Observer class:
- The code of the observer pattern is virtual functions and abstract classes.
- virtual void onNotify(const string& message) = 0: A pure virtual function which forces the observers’ subclass to implement a onNotify function.
- Destructor: A virtual destructor is critical for correct polymorphic deletion to prevent memory leaks.
Subject class:
- observers: A vector of Observer class objects.
- attach function: adds an Observer object to the observers vector.
- detach function: It uses a common C++ idiom to remove the specific ‘obs’ from the ‘observers’ vector with the help of ‘erase’ and ‘remove’ method.
- notify function: This function is critical, as during runtime it automatically determines which subclass the obs is from and invokes the correct ‘onNotify()’ method. This happens because inside the Observer class, the ‘onNotify()’ method was virtual.
Concrete Example: Event publisher and Listener
Code Snippet
class EventManager : public Subject { public: void triggerEvent(const string& event) { notify(event); } }; class Logger : public Observer { public: void onNotify(const string& message) override { cout << "[Logger] Event received: " << message << endl; } }; class UIHandler : public Observer { public: void onNotify(const string& message) override { cout << "[UI] Update triggered by: " << message << endl; } };
Explaination
EventManager class:
- The EventManager is our concrete Subject. Think of it as the “broadcaster” of various occurrences.
- It inherits publicly from ‘Subject’. This means it automatically gains the functionalities that ‘Subject’ provides.
- triggerEvent function: when some part of the code wants to trigger an even,t it will call upon this method.
Logger class:
- Logger is a concrete Observer. Its purpose is to react to events by logging them to the console.
- It inherits publicly from ‘Observer’, meaning it must provide an implementation for the pure virtual function.
- onNotify method: This will be called when the EventManager invokes it. The ‘override’ keyword is to tell the compiler that this method is intended to override a virtual function in a base class.
UIHandler class:
- This is also a concrete Observer.
- Same as Logger class.
Usage
Code Snippet
int main() { EventManager manager; Logger logger; UIHandler ui; manager.attach(&logger); manager.attach(&ui); manager.triggerEvent("DataLoaded"); manager.detach(&logger); manager.triggerEvent("UserClicked"); return 0; }
Output
[Logger] Event received: DataLoaded [UI] Update triggered by: DataLoaded [UI] Update triggered by: UserClicked
Explaination
- Creation of manager, logger and ui class object.
- manager uses the attach method to keep track of the logger and ui objects.
- when manager triggers the event “DataLoaded”, the ‘notify’ method goes through all the ‘obs’ and invokes their onNotify() method.
- This resulted in “[Logger] Event received: DataLoaded [UI] Update triggered by: DataLoaded”
- The Logger event is detached from the manager.
- Now, triggering the “UserClicked” event will only print out “[UI] Update triggered by: UserClicked”.
It is important to note that we have only done this for ‘Strings’. In order to trigger an event of other types, we have to add function overloading to achieve our desired result.
Conclusion
The observer pattern shows how it makes triggering an event in an event-driven system in C++ easier to implement. This makes our application easier to change, scale and fix. By using this architecture, we can manage our application more reliably.