I'm studying design patterns from Head First Design Patterns and, in order to get confident, I plan to implement the each pattern in C++ after studying the corresponding chapter.
Concerning the Observer pattern, I am really struggling to beyond the language-indpended main idea.
I've been skiming through the following:
- Implementation of Observer Pattern in C++14
- Observer pattern implementation
- this
- that
- and several others, actually.
but, as soon as I started coding in C++, some language-specific difficulties brought to light some misunderstanding of mine on the whole topic, which I've not been able to solve with the liks above. However, I am posting here since I've got a (seemingly) working code.
The sample code is the following, afterwards I list some of my concerns about my understanding and use of this pattern.
#include <algorithm>
#include <iostream>
#include <unordered_map>
#include <unordered_set>
class Observer {
public:
virtual void update() = 0;
};
class Observable {
protected:
std::unordered_set<Observer*> observers;
public:
virtual void addObserver(Observer&) = 0;
virtual void removeObserver(Observer&) = 0;
virtual void notifyObservers() = 0;
};
class Virus : public Observable {
public:
void addObserver(Observer& o) {
observers.insert(&o);
};
void removeObserver(Observer& o) {
std::erase_if(observers, [&o](auto const& io){ return &o == io; });
};
void notifyObservers() {
for (auto& o : observers) o->update();
};
void operator++() {
++spread;
std::cout << "\nLevel: " << spread
<< "\nSending notifications:\n";
notifyObservers();
}
int getSpread() { return spread; };
private:
int spread = 0;
};
class NormalCountry final : public Observer {
private:
void update() override {
if (obs.getSpread() < 2)
std::cout << "NormalCountry: What!? Coronavirus?\n";
else
std::cout << "NormalCountry: Ok, let's quarantine...\n";
};
private:
Virus& obs;
public:
void selfUnsubscribe() {
obs.removeObserver(*this);
};
void selfSubscribe() {
obs.addObserver(*this);
};
NormalCountry() = delete;
NormalCountry(Virus& o) : obs(o) {
selfSubscribe();
}
};
class BraveCountry final : public Observer {
public:
void update() override {
std::cout << "BraveCountry: No worries, people, our antibodies are cooler!\n";
};
};
int main() {
Virus cv;
NormalCountry it(cv);
BraveCountry uk;
++cv;
cv.addObserver(uk);
++cv;
it.selfUnsubscribe();
++cv;
}
Doubts about the pattern itself:
My understanding is that the observers only have to be able to be informed, by the
Observablethat they observe, that something changed in it, which makes easy to think of theObserveras an interface, which in C++ means an abstract class defining no more than a pure virtualupdatemethod, which forces the derived classes into overloading that method with that specific signature (this seems to be more or less the same in Java); however, this does not say anything about whether the observers should know more than the "binary" information (something/nothing changed); indeed, here it comes the pull or push design decision, which, on the one hand, looks like an implementation detail to me; on the other hand, the decision affects how theupdatepure virtual method should be declared (choosing the arguments list), as well as whether the concrete observers should hold a pointer/reference to the observed object. This is to say that if I have been given the two abstract classesObserverandObservable, an implementation choice has already been taken, and I cannot change.As regards the
Observableinterface, the book says it only has to provide declarations foraddObserver,removeObserver, andnotifyObservers. But then the collection ofObservers has to be a member of the concrete observer classes. I understand that the choice of the collection (std::vector,std::list, ...) is an implementation detail, but the fact that some kind of collection must be in theObservabledoes not look as a detail. However, choosing a collection in a concrete observable becomes necessary as soon as one tries to code the three member functions mentioned above. Maybe that's enough, I don't know.The
Observer::updatepure virtual method should be public in order for concrete observables to implementnotifyObservers. But the concrete observers can make their implementation ofupdateprivate. Doing so kinda makes sense to me: ifupdateandnotifyObserversare the two ends of each observer-observable relation, why should it be possible for the caller code to call an observer'supdateif the observable'snotifyObservershas not decided to do so? Well, maybe for the same reason why an observer can handle its own (un)subscription (in the case that it holds an handle to the observed object).
C++-specific doubts:
Should the observable class (the abstract base or the concrete derived) have a collection of raw pointers or smart pointers? What the implications of this choice could be?
addObserverandremoveObservershould take anObserverparameter. I don't think it should by passed by value, to avoid copying; maybe not even by pointer, otherwise at the call site we have to pass&objinstead ofobj. Then it's by reference; but which? Aconstlvalue reference would allow passing temporary observers, but does this make sense? If it does, then one should have two overloads for rvalues and lvalues, as having one template function with a universal reference is not allowed for virtual functions.In my example code I have stored a reference to the concrete observable object in one of the concrete onservers class, so that I can use some member specific to the concrete observable (
getSpread) in theupdateimplementation. I fear this might be bad.