Why Observer is so cool?
Managing complexity in large code base is all about decoupling. Observer is a perfect tool to do that. The Observed
object has no idea about its observers, no dependency injection is required at all, as a result a loose coupling is achieved between two classes of objects where the only common part is the interface they agree upon.
First iteration
It’s a classic design pattern and I’m sure majority (if not all) of engineers know the details behind it, most of us implement it in a slightly different way, depending on the requiremenets and the code base we are working with though. Here’s my flavour of this design pattern.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
class Observer {
public:
virtual ~Observer() = default;
virtual void update() = 0;
};
class Observed {
public:
virtual ~Observed() = default;
void addObserver(std::shared_ptr<Observer> observer) final {
observers.push_back(observer);
}
void removeObserver(std::shared_ptr<Observer> observer) final {
observers.erase(std::remove_if(observers.begin(), observers.end(),
[observer](const auto o){ return o.lock() == observer; }),
observers.end());
}
void notifyObservers() final {
for (const auto o : observers) {
if (auto strong = o.lock()) {
strong->update();
}
}
}
private:
std::vector<std::weak_ptr<Observer>> observers;
};
|
This implementation would be used in a following way:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class ConcreteObserved : public Observed {
public:
};
class ConcreteObserver : public Observer {
public:
void update() override {
std::cout << "Received an update" << std::endl;
}
};
...
ConcreteObserver observed;
auto concreteObserver = std::make_shared<ConcreteObserver>();
observed.addObserver(concreteObserver);
observed.notify();
|
That’s how the fundamentals usually look. The Observed
object generates update()
events to notify its observers that something has happened. There’s a couple of problems with this code though, preventing it from being a generic, library implementation that could be used independently. Let’s address these.
Generic Observer type
First of all, the implementation dictates the API for Observers
which is not good because, depending on the situation this for sure will be different. The simplest way is to introduce a template parameter, the following way:
1
2
3
4
5
6
7
|
template <typename ObserverT>
class Observed {
...
void notifyObservers() {
// how to implement this?
}
}
|
… and there’s a problem. Since the Observer
’s API is unknown up front it’s impossible to implement the notification functions in a generic way. This could be solved in a simple manner by delegating the problem to the clients:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
class ClientObserver {
public:
virtual void updateA(int a) = 0;
virtual void updateB(std::string b) = 0;
};
class ClientConcreteObserver : public ClientObserver {
public:
void updateA(int a) {
std::cout << "got update A with value: " << a << std::endl;
}
void updateB(std::string b) {
std::cout << "got update B with value: " << b << std::endl;
}
};
class ClientObserved : public Observed<ClientObserver> {
...
void notifyA(int i) {
for (const auto o : observers) {
if (auto strong = o.lock()) {
strong->updateA(i);
}
}
}
void notifyB(std::string s) {
for (const auto o : observers) {
if (auto strong = o.lock()) {
strong->updateB(s);
}
}
}
};
|
Sure, this will work but it leads to a terrible amount of code duplication. This problem can be solved in two ways.
Pointer to members
Firstly, I’m gonna generalise the implementation of the notify
function. It’ll now be a function template and it’ll look like so:
1
2
3
4
5
6
7
8
|
template <typename ReturnT, typename ... ArgsT>
void notify(ReturnT (ObserverT::*member)(ArgsT...), ArgsT&& ... args) {
for (const auto o : observers) {
if (auto strong = o.lock()) {
(strong.get()->*member)(std::forward<ArgsT>(args)...);
}
}
}
|
Granted, it looks a bit scarry but after a minute it becomes clear. I’m using a pointer to member here and a variadic template to handle function arguments which are perfectly forwarded. How to use it?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
class ClientObserved : public Observed<Observer> {
public:
};
class ClientConcreteObserver : public Observer {
public:
void updateA(int a) {
std::cout << "got update A with value: " << a << std::endl;
}
void updateB(std::string b) {
std::cout << "got update B with value: " << b << std::endl;
}
};
...
auto co = std::make_shared<ClientConcreteObserver>();
ClientObserved o;
o.addObserver(co);
o.notify(&Observer::updateA, 456);
o.notify(&Observer::updateB, std::string("hello"));
|
That looks good. The provided implementation is generic enough to exist independently and be reusable. Just for reference, I’m gonna show another way to implement notifications, using functors.
Functors
The implementation of notify
member function becomes simpler and more concise:
1
2
3
4
5
6
7
8
|
template <typename FunctorT>
void notify(FunctorT functor) {
for (const auto o : observers) {
if (auto strong = o.lock()) {
functor(strong.get());
}
}
}
|
In here I expect to receive a functor taking a pointer to Observer*
type explicitly. In other words, the function signature becomes as following:
1
|
void functor(Observer* o, ...);
|
The complexity is shifted to the client side. In order to use the notify
function in its current form, functors have to be created in place:
1
2
3
4
5
6
7
8
9
|
using namespace std::placeholders;
...
auto co = std::make_shared<ClientConcreteObserver>();
ClientObserved o;
o.addObserver(co);
o.notify(std::bind(&Observer::updateA, _1, 456));
o.notify(std::bind(&Observer::updateB, _1, std::string("hello")));
|
All details and a more concise reference can be found on gitlab.