615 lines
19 KiB
Markdown
615 lines
19 KiB
Markdown
# 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:
|
|
|
|
* <!-- .element: class="fragment fade-in" --> Pull observer: We inform a class about change, but this class needs to find out what changed
|
|
* <!-- .element: class="fragment fade-in" --> 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
|
|
<!-- .element: class="fragment fade-in" -->
|
|
|
|
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
|
|
<!-- .element: class="fragment fade-in" -->
|
|
___
|
|
|
|
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:
|
|
|
|
* <!-- .element: class="fragment fade-in" --> A one-to-many dependency between objects should be defined without making the objects tightly coupled.
|
|
* <!-- .element: class="fragment fade-in" --> When one object changes state, an open-ended number of dependent objects should be updated automatically.
|
|
* <!-- .element: class="fragment fade-in" --> An object can notify multiple other objects
|
|
|
|
<img data-src="images/observer.png" alt="Observer UML">
|
|
<!-- .element: class="fragment fade-in" -->
|
|
___
|
|
|
|
## Simple example
|
|
|
|
We want to inform other classes when the browser will change the focus or will be closed.
|
|
|
|
```C++
|
|
struct Observer {
|
|
virtual ~Observer() = default;
|
|
virtual void browserAboutToQuit() = 0;
|
|
virtual void browserFocusChanged() = 0;
|
|
}
|
|
```
|
|
___
|
|
|
|
```C++
|
|
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;
|
|
};
|
|
```
|
|
<!-- .slide: style="font-size: 0.80em" -->
|
|
___
|
|
|
|
```C++
|
|
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;
|
|
};
|
|
|
|
```
|
|
<!-- .slide: style="font-size: 0.83em" -->
|
|
|
|
___
|
|
|
|
```C++
|
|
int main() {
|
|
std::unique_ptr<Browser> browser = std::make_unique<Browser>();
|
|
TabStripManager manager(browser.get());
|
|
browser = nullptr;
|
|
}
|
|
```
|
|
|
|
```bash
|
|
Browser is closing
|
|
TabStripManager will close tabs
|
|
Browser closed
|
|
```
|
|
<!-- .element: class="fragment fade-in" -->
|
|
___
|
|
|
|
## Problem with observer
|
|
|
|
Let's check once more the class `TabStripManager`, did you spot a problem here?
|
|
|
|
```C++
|
|
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;
|
|
};
|
|
|
|
```
|
|
<!-- .element: class="fragment fade-in" -->
|
|
<!-- .slide: style="font-size: 0.83em" -->
|
|
___
|
|
|
|
## 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.
|
|
|
|
```C++
|
|
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;
|
|
};
|
|
```
|
|
<!-- .element: class="fragment fade-in" -->
|
|
<!-- .slide: style="font-size: 0.68em" -->
|
|
|
|
___
|
|
|
|
## 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?
|
|
<!-- .element: class="fragment fade-in" -->
|
|
|
|
Spoiler was in the topic, yes we can!
|
|
<!-- .element: class="fragment fade-in" -->
|
|
|
|
```C++
|
|
template <typename State>
|
|
struct Observer {
|
|
virtual ~Observer() = default;
|
|
virtual void update(State state) = 0;
|
|
};
|
|
```
|
|
<!-- .element: class="fragment fade-in" -->
|
|
___
|
|
|
|
```C++
|
|
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;
|
|
};
|
|
```
|
|
<!-- .slide: style="font-size: 0.72em" -->
|
|
|
|
___
|
|
|
|
```C++
|
|
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;
|
|
};
|
|
```
|
|
<!-- .slide: style="font-size: 0.74em" -->
|
|
|
|
___
|
|
|
|
## Still we have the same behavior
|
|
|
|
```C++
|
|
int main() {
|
|
std::unique_ptr<Browser> browser = std::make_unique<Browser>();
|
|
TabStripManager manager(browser.get());
|
|
browser = nullptr;
|
|
}
|
|
```
|
|
|
|
```bash
|
|
Browser is closing
|
|
TabStripManager will close tabs
|
|
Browser closed
|
|
```
|
|
<!-- .element: class="fragment fade-in" -->
|
|
___
|
|
|
|
## Problem with observer part 2
|
|
|
|
Let's check once more the class `Browser` do you spot any problem here?
|
|
|
|
```C++
|
|
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;
|
|
};
|
|
```
|
|
<!-- .slide: style="font-size: 0.72em" -->
|
|
___
|
|
|
|
## Attach / dettach
|
|
|
|
Question1: How do we bahve, when someone will attach twice the same observer?
|
|
|
|
- 1) <!-- .element: class="fragment fade-in" --> Register it twice, and than notify twice? Like `std::recursive_mutex`?
|
|
- 2) <!-- .element: class="fragment fade-in" --> Throw an exception?
|
|
- 3) <!-- .element: class="fragment fade-in" --> Return an error?
|
|
- 4) <!-- .element: class="fragment fade-in" --> 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
|
|
<!-- .element: class="fragment fade-in" -->
|
|
|
|
Question2: How do we bahve, when someone will detach twice the same observer?
|
|
<!-- .element: class="fragment fade-in" -->
|
|
|
|
- 1) <!-- .element: class="fragment fade-in" --> Check if someone attach it twice?
|
|
- 2) <!-- .element: class="fragment fade-in" --> Throw an exception?
|
|
- 3) <!-- .element: class="fragment fade-in" --> Return an error?
|
|
- 4) <!-- .element: class="fragment fade-in" --> Ignore it, and only print some warning logs about that?
|
|
|
|
I guess you know the answer :) it depens.
|
|
<!-- .element: class="fragment fade-in" -->
|
|
|
|
___
|
|
|
|
## 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?
|
|
|
|
```C++
|
|
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;
|
|
};
|
|
```
|
|
<!-- .slide: style="font-size: 0.72em" -->
|
|
___
|
|
|
|
## Maybe now?
|
|
|
|
```C++
|
|
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;
|
|
};
|
|
```
|
|
<!-- .slide: style="font-size: 0.72em" -->
|
|
___
|
|
|
|
## Order of observer
|
|
|
|
What will be printed?
|
|
|
|
```C++
|
|
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!
|
|
<!-- .element: class="fragment fade-in" -->
|
|
|
|
___
|
|
|
|
## Exercise
|
|
|
|
* <!-- .element: class="fragment fade-in" --> Go into directory <code>Core</code> and implement <code>Time</code> and <code>TimeObserver</code> class
|
|
* <!-- .element: class="fragment fade-in" --> Class <code>Time</code> should have 4 methods:
|
|
* <!-- .element: class="fragment fade-in" --> void attach(TimeObserver* observer); -> attach observer
|
|
* <!-- .element: class="fragment fade-in" --> void detach(TimeObserver* observer); -> detach observer
|
|
* <!-- .element: class="fragment fade-in" --> Time& operator++(); -> increment day and notify observers
|
|
* <!-- .element: class="fragment fade-in" --> size_t day() const; -> return current day
|
|
* <!-- .element: class="fragment fade-in" --> Go into directory <code>Ship</code> and make <code>Ship</code> class inherit from <code>TimeObserver</code>
|
|
* <!-- .element: class="fragment fade-in" --> Each time the observer will be triggered, you should give the crew food and drink
|
|
* <!-- .element: class="fragment fade-in" --> Each crew member should consume <b>1</b> rum and <b>1</b> banana (I know they usually eat biscuits)
|
|
* <!-- .element: class="fragment fade-in" --> If you don't have enough cargo crew should rebel
|
|
* <!-- .element: class="fragment fade-in" --> Each day of a rebel you should subtract <b>5</b> members of the crew
|
|
* <!-- .element: class="fragment fade-in" --> If the number of crew drops below <b>0</b>, throw an exception "Game Over"
|
|
|
|
___
|
|
|
|
## Exercise 2
|
|
|
|
* <!-- .element: class="fragment fade-in" --> Go into directory <code>Store</code> and make <code>Store</code> class inherit from *TimeObserver*
|
|
* <!-- .element: class="fragment fade-in" --> Class <code>Store</code> should don't know anything about class <code>Time</code>
|
|
* <!-- .element: class="fragment fade-in" --> We should attach and detach observer outside of class. Think about how you can make it exception-safe (RAII)
|
|
* <!-- .element: class="fragment fade-in" --> Each time the observer will be triggered, you should change the number of available cargo in the store
|
|
* <!-- .element: class="fragment fade-in" --> Each time draw a number between <b>-50</b> and <b>50</b> and add it to the amount of cargo
|
|
* <!-- .element: class="fragment fade-in" --> 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.
|
|
|
|
```C++
|
|
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;
|
|
};
|
|
```
|
|
<!-- .element: class="fragment fade-in" -->
|
|
|
|
___
|
|
|
|
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.
|
|
|
|
```C++
|
|
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.
|
|
|
|
```C++
|
|
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;
|
|
};
|
|
```
|
|
<!-- .slide: style="font-size: 0.84em" -->
|
|
___
|
|
|
|
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.
|
|
|
|
```C++
|
|
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;
|
|
};
|
|
```
|
|
<!-- .slide: style="font-size: 0.74em" -->
|
|
___
|
|
|
|
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.
|
|
|
|
```C++
|
|
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);
|
|
}
|
|
```
|
|
<!-- .slide: style="font-size: 0.74em" -->
|
|
___
|
|
|
|
## 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 <a href="https://quick-bench.com/q/tdXuY7d_d-QJ5Qry5HiN8LntYlM">performance</a>.
|
|
|
|
|
|
|
|
<img data-src="images/benchmark_observer.png" alt="Benchamrk Observer">
|
|
|
|
___
|
|
|
|
## 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
|
|
|
|
* <!-- .element: class="fragment fade-in" --> Rewrite Observer to use modern approach -> by value semantic.
|
|
* <!-- .element: class="fragment fade-in" --> Rewrite class Ship
|
|
* <!-- .element: class="fragment fade-in" --> Rewrite class Store
|
|
* <!-- .element: class="fragment fade-in" --> <b>Question: Which class was easier to rewrite?</b>
|