19 KiB
Observer
One of the most popular design patterns is an observer. Almost every project uses at least one observer. This popular pattern is used to notify other classes about some changes. There are two types of observers:
- Pull observer: We inform a class about change, but this class needs to find out what changed
- Push observer: We send all necessary information to class about what changes
A first approach is more flexible, but forcing a class to get info on what changed, may take a long time to get this info, which may cause an observer slower
A second approach is less flexible because we need to create a method per change, to be sure, that the other class will know what happens and can use changed values without explicitly taking arguments or checking the state from the observed class
The observer design pattern is a behavioural pattern listed among the 23 well-known "Gang of Four" design patterns that address recurring design challenges in order to design flexible and reusable object-oriented software, yielding objects that are easier to implement, change, test and reuse. Observer has the following traits:
- A one-to-many dependency between objects should be defined without making the objects tightly coupled.
- When one object changes state, an open-ended number of dependent objects should be updated automatically.
- An object can notify multiple other objects
Simple example
We want to inform other classes when the browser will change the focus or will be closed.
struct Observer {
virtual ~Observer() = default;
virtual void browserAboutToQuit() = 0;
virtual void browserFocusChanged() = 0;
}
class Browser {
public:
struct Observer { /* implementation */ };
~Browser() {
std::cout << "Browser is closing\n";
notifyOnQuit();
std::cout << "Browser closed\n";
}
// Rule of 5 :)
void attach(Observer* observer) { _observers.push_back(observer); }
void detach(Observer* observer) { std::erase(_observers, observer); }
private:
void notifyOnQuit() const {
std::ranges::for_each(_observers, std::mem_fn(&Observer::browserAboutToQuit));
}
void notifyOnFocusChanfed() const {
std::ranges::for_each(_observers, std::mem_fn(&Observer::browserFocusChanged));
}
std::vector<Observer*> _observers;
};
class TabStripManager : public Browser::Observer {
public:
TabStripManager(Browser* browser): _browser(browser) {
browser->attach(this);
}
~TabStripManager() {
_browser->detach(this);
}
// Rule of 5 :)
void browserAboutToQuit() override {
std::cout << "TabStripManager will close tabs\n";
}
void browserFocusChanged() override {
std::cout << "TabStripManager lost focus\n";
}
private:
Browser* _browser;
};
int main() {
std::unique_ptr<Browser> browser = std::make_unique<Browser>();
TabStripManager manager(browser.get());
browser = nullptr;
}
Browser is closing
TabStripManager will close tabs
Browser closed
Problem with observer
Let's check once more the class TabStripManager, did you spot a problem here?
class TabStripManager : public Browser::Observer {
public:
TabStripManager(Browser* browser): _browser(browser) {
browser->attach(this);
}
~TabStripManager() {
_browser->detach(this);
}
// Rule of 5 :)
void browserAboutToQuit() override {
std::cout << "TabStripManager will close tabs\n";
}
void browserFocusChanged() override {
std::cout << "TabStripManager lost focus\n";
}
private:
Browser* _browser;
};
About to quit
Observer is a useful mechanism to inform other classes that hold a pointer to our class to invalidate this pointer because we are destroying it. If we forget to invalidate the observer we will call detach in destructor which will cause a UB because we hold a dangling pointer.
class TabStripManager : public Browser::Observer {
public:
TabStripManager(Browser* browser): _browser(browser) {
assert(browser);
browser->attach(this);
}
~TabStripManager() {
if (_browser) {
_browser->detach(this);
}
}
// Rule of 5 :)
void browserAboutToQuit() override {
std::cout << "TabStripManager will close tabs\n";
_browser = nullptr;
}
void browserFocusChanged() override {
std::cout << "TabStripManager lost focus\n";
}
private:
Browser* _browser;
};
Template observer
Do we always need to copy and paste the Observer class to every other class that wants to use the pattern? And do we always need to add a method for every state? Can we do this without duplicating the code?
Spoiler was in the topic, yes we can!
template <typename State>
struct Observer {
virtual ~Observer() = default;
virtual void update(State state) = 0;
};
class Browser {
public:
enum class State {
Closing,
FocusChanged
};
using BrowserObserver = Observer<State>;
~Browser() {
std::cout << "Browser is closing\n";
notify(State::Closing);
std::cout << "Browser closed\n";
}
// Rule of 5 :)
void attach(BrowserObserver* observer) { _observers.push_back(observer); }
void detach(BrowserObserver* observer) { std::erase(_observers, observer); }
private:
void notify(State state) const {
std::ranges::for_each(_observers, [state](auto* observer){
observer->update(state);
});
}
std::vector<BrowserObserver*> _observers;
};
class TabStripManager : public Browser::BrowserObserver {
public:
TabStripManager(Browser* browser): _browser(browser) {
assert(browser);
browser->attach(this);
}
~TabStripManager() {
if (_browser) {
_browser->detach(this);
}
}
// Rule of 5 :)
// dispatch message
void update(Browser::State state) override {
switch (state) {
case Browser::State::Closing: {
browserAboutToQuit();
return;
}
case Browser::State::FocusChanged: {
browserFocusChanged();
return;
}
}
}
private:
void browserAboutToQuit() {
std::cout << "TabStripManager will close tabs\n";
_browser = nullptr;
}
void browserFocusChanged() {
std::cout << "TabStripManager lost focus\n";
}
Browser* _browser;
};
Still we have the same behavior
int main() {
std::unique_ptr<Browser> browser = std::make_unique<Browser>();
TabStripManager manager(browser.get());
browser = nullptr;
}
Browser is closing
TabStripManager will close tabs
Browser closed
Problem with observer part 2
Let's check once more the class Browser do you spot any problem here?
class Browser {
public:
enum class State {
Closing,
FocusChanged
};
using BrowserObserver = Observer<State>;
~Browser() {
std::cout << "Browser is closing\n";
notify(State::Closing);
std::cout << "Browser closed\n";
}
// Rule of 5 :)
void attach(BrowserObserver* observer) { _observers.push_back(observer); }
void detach(BrowserObserver* observer) { std::erase(_observers, observer); }
private:
void notify(State state) const {
std::ranges::for_each(_observers, [state](auto* observer){
observer->update(state);
});
}
std::vector<BrowserObserver*> _observers;
};
Attach / dettach
Question1: How do we bahve, when someone will attach twice the same observer?
-
- Register it twice, and than notify twice? Like `std::recursive_mutex`?
-
- Throw an exception?
-
- Return an error?
-
- Ignore it, and only print some warning logs about that?
The answer is not that easy, as usual, it depends, on what we want to achieve
Question2: How do we bahve, when someone will detach twice the same observer?
-
- Check if someone attach it twice?
-
- Throw an exception?
-
- Return an error?
-
- Ignore it, and only print some warning logs about that?
I guess you know the answer :) it depens.
Problem with observer part 3
This is not all the problems that we may have with the observer, let's check this snippet of code. Do you spot any problem here?
class Browser {
public:
enum class State {
Closing,
FocusChanged
};
using BrowserObserver = Observer<State>;
void attach(BrowserObserver* observer) { _observers.emplace(observer); }
void detach(BrowserObserver* observer) { _observers.erase(observer); }
void setFocus(bool focus) {
if (std::exchange(_hasFocus, focus) != focus) {
notify(State::FocusChanged);
}
}
bool hasFocus() const { return _hasFocus; }
private:
void notify(State state) const {
std::ranges::for_each(_observers, [state](auto* observer){
observer->update(state);
});
}
bool _hasFocus{false};
std::set<BrowserObserver*> _observers;
};
Maybe now?
class Extension : public Browser::BrowserObserver {
public:
Extension(Browser* browser, const std::string& name): _browser(browser), _name(name) { browser->attach(this); }
// Rule of 5 :)
~Extension() { if (_browser) { _browser->detach(this); } }
// dispatch message
void update(Browser::State state) override {
switch (state) {
case Browser::State::Closing: return;
case Browser::State::FocusChanged: {
if (_browser->hasFocus()) { displayExtension(); }
return;
}
}
}
private:
void displayExtension() {
std::cout << "This is an extension: " << _name << '\n';
}
Browser* _browser;
std::string _name;
};
Order of observer
What will be printed?
int main() {
std::unique_ptr<Browser> browser = std::make_unique<Browser>();
Extension extA(browser.get(), "A");
Extension extB(browser.get(), "B");
Extension extC(browser.get(), "C");
browser->setFocus(true);
}
The order is unknown because we use std::set. Generally, we should never depend on the order of calling an observer. Because in the future someone can change the implementation and the program will stop working!
Exercise
-
Go into directory
Coreand implementTimeandTimeObserverclass -
Class
Timeshould have 4 methods:- void attach(TimeObserver* observer); -> attach observer
- void detach(TimeObserver* observer); -> detach observer
- Time& operator++(); -> increment day and notify observers
- size_t day() const; -> return current day
-
Go into directory
Shipand makeShipclass inherit fromTimeObserver - Each time the observer will be triggered, you should give the crew food and drink
- Each crew member should consume 1 rum and 1 banana (I know they usually eat biscuits)
- If you don't have enough cargo crew should rebel
- Each day of a rebel you should subtract 5 members of the crew
- If the number of crew drops below 0, throw an exception "Game Over"
Exercise 2
-
Go into directory
Storeand makeStoreclass inherit from *TimeObserver* -
Class
Storeshould don't know anything about classTime - We should attach and detach observer outside of class. Think about how you can make it exception-safe (RAII)
- Each time the observer will be triggered, you should change the number of available cargo in the store
- Each time draw a number between -50 and 50 and add it to the amount of cargo
- Print everyday cargo from the Shop and verify if the amount changes
Moder apporach
In modern C++ we try to avoid too many references or pointers because they are problematic. We will try to implement an observer pattern by using value semantics. First let's get rid of the virtual function, and forcing classes to inherit from the observer class.
template <typename Subject, typename State>
struct Observer {
// First, we don't need to use polymorphism, so also virtual D'tor is redundant
using OnUpdate = std::function<void(const Subject&. State)>;
explciit Observer(OnUpdate fun): _onUpdate(std::move(fun)) {}
void update(const Subject& subject, State state) {
std::invoke(_onUpdate, subject, state);
}
private:
OnUpdate _onUpdate;
};
Now move out of the Browser class BorwserObserver so the files that want to use BrowserObserver don't need to include the whole Browser file in the header. Only in the source file we will need a Browser library, so every time the Browser class changes we don't need to recompile a lot of files.
class Browser;
enum class BrowserState {
Closing,
FocusChanged
};
using BrowserObserver = Observer<Browser, BrowserState>;
We will still use pointer as Observer because they are easy to use (we know that each address is unique) but we can change it to some uuid or sth similar to get rid of the pointer also in a Borwser class.
class Browser {
public:
void attach(BrowserObserver* observer) { _observers.emplace(observer); }
void detach(BrowserObserver* observer) { _observers.erase(observer); }
void setFocus(bool focus) {
if (std::exchange(_hasFocus, focus) != focus) {
notify(BrowserState::FocusChanged);
}
}
bool hasFocus() const { return _hasFocus; }
private:
void notify(BrowserState state) const {
std::ranges::for_each(_observers, [state, this](auto* observer){
observer->update(*this, state);
});
}
bool _hasFocus{false};
std::set<BrowserObserver*> _observers;
};
Extension class doesn't need to inherit from Observer so we get rid of destructor and vtable. Another advantage is that we separate observers from extension classes. Now it will be a separate object.
class Extension {
public:
explicit Extension(const std::string& name): _name(name) { }
// D'tor is not needed neither the rule of 5
// dispatch message
void update(const Browser& browser, BrowserState state) {
switch (state) {
case BrowserState::Closing:
return;
case BrowserState::FocusChanged: {
if (browser.hasFocus()) {
displayExtension();
}
return;
}
}
}
private:
void displayExtension() {
std::cout << "This is an extension: " << _name << '\n';
}
std::string _name;
};
We have three separate objects, one is a Browser the second is an Extension and the third is a BrowserObserver. We still can use traditional observer patterns, where we use inheritance, but in a lot of cases, we can avoid virtual and make our code faster.
int main() {
std::unique_ptr<Browser> browser = std::make_unique<Browser>();
Extension extension1("A");
Extension extension2("B");
Extension extension3("C");
BrowserObserver observer1([&extension1](const Browser& browser, BrowserState state){
extension1.update(browser, state);
});
BrowserObserver observer2([&extension2](const Browser& browser, BrowserState state){
extension2.update(browser, state);
});
BrowserObserver observer3([&extension3](const Browser& browser, BrowserState state){
extension3.update(browser, state);
});
browser->attach(&observer1);
browser->attach(&observer2);
browser->attach(&observer3);
browser->setFocus(true);
}
Benchmark
By using value semantics, we may also get around 10% faster code. This depends on what we do as an observer, it the action is long, we will not see any difference, but for quick actions we may improve a performance.
The last drawback of Oobserver
Please don't use it everywhere, because your code becomes more complicated when you will have hundreds of observers. Remember you can't relay on the order of notification, so if class A calls notify which triggers class B which also triggers a notification for class C which also triggers a notification for class D you will have really complicated structure and finding bugs and fixing it will be hard.
Exercise 3
- Rewrite Observer to use modern approach -> by value semantic.
- Rewrite class Ship
- Rewrite class Store
- Question: Which class was easier to rewrite?