Part of the Design Patterns series:
I have spent a good portion of my professional career working on native mobile applications, which largely rely on reactive programming to automatically update UI elements when there is a state change to relevant data objects. Frameworks like RxSwift and Combine make this pretty painless and provide modern, clean, and performant native code. This pattern also has significant value in the C++ development enviornment.
At a high level, the observer design pattern allows us to define a one-to-many dependency between objects so that when the state of one object changes, all its dependents are notified. This pattern targets a variation point and extracts it into an abstraction that helps decouple software entities.
Lets look at an implementation example:
C++ 1template<typename Subject, typename StateTag>
2class Observer {
3 public:
4 virtual ~Observer() = default;
5 virtual void update(Subject const& subject, StateTag property) = 0;
6};
7
8class Person {
9 public:
10 enum StateChange {
11 forenameChanged,
12 surnameChanged,
13 addressChanged
14 };
15 using PersonObserver = Observer<Person, StateChange>;
16
17 explicit Person(std::string forename, std::string surname) : forename_{std::move(forename)}, surname_{std::move(surname)} {}
18
19 bool attach(PersonObserver* observer);
20 bool detach(PersonObserver* observer);
21
22 void notify(StateChange property);
23
24 void forename(std::string newForename);
25 void surname(std::string newSurname);
26 void address(std::string newAddress);
27
28 std::string const& forename() const { return forename_;}
29 std::string const& surname() const { return surname_; }
30 std::string const& address() const {return address_;}
31 private:
32 std::string forename_;
33 std::string surname_;
34 std::string address_;
35
36 std::set<PersonObserver*> observers_;
37};
We have built an abstract templated Observer class that other classes can implement as part of this design pattern. In our example, we use a simplistic Person class made up of three data members representing the first name, last name, and address. The two things worth noting so far is:
- Using a templated Observer class allows us to easily customize the class to pass data specific to the use case of the class that utilizes it. In our case the Person class will be passing a StateChange enum to help observers monitor what exactly changed.
- We will be storing all observers of the Person class in a set of PersonObserver pointers. This might feel like an abandonment of modern C++, but it remains a valid and well accepted part of the language. In fact, it is recommended we take arguments as pointers when the class does not have ownership over the resource. We could of course utilize a weak pointer, but the second advantage to using a pointer is each value in the set is a unique memory address. Ok lets continue
1bool Person::attach(PersonObserver* observer) {
2 auto [pos,success] = observers_.insert(observer);
3 return success;
4}
5bool Person::detach(PersonObserver* observer) {
6 return (observers_.erase(observer) > 0U );
7}
8void Person::notify(StateChange property) {
9 for (auto iter = begin(observers_); iter != end(observers_);) {
10 auto const pos = iter++;
11 (*pos)->update(*this,property);
12 }
13}
The attach and detach functions are pretty self explanatory, we simply are adding and removing new observers to our set. For the notify function, all we technically need to do is call update() on all pointers in our observers_ set, passing a reference of self along with the state change. The implementation above is a little more complicated in order to handle a use case where a detach is called during the loop within notify(). But this does not handle use cases where attach() is called which can get quite a bit more complex.
C++ 1void Person::forename(std::string newForename) {
2 forename_ = std::move(newForename);
3 notify(forenameChanged);
4}
5void Person::surname(std::string newSurname) {
6 surname_ = std::move(newForename);
7 notify(surnameChanged);
8}
9void Person::address(std::string newAddress) {
10 address_ = std::move(newAddress);
11 notify(addressChanged);
12}
We are now notifying any interested party when the first name, last name, or address changes with any instance of a Person class.
C++ 1class NameObserver : public Observer<Person, Person::StateChange> {
2 public:
3 void update(Person const& person, Person::StateChange property) override;
4};
5
6void NameObserver::update(Person const& person, Person::StateChange property) {
7 if (property == Person::forenameChanged || property == Person::surnameChanged) {
8 // Respond to name change
9 }
10}
11
12class AddressObserver : public Observer<Person, Person::StateChange> {
13 public:
14 void update(Person const& person, Person::StateChange property) override;
15};
16
17void AddressObserver::update(Person const& person, Person::StateChange property) {
18 if (property == Person::addressChanged) {
19 // Respond to address change
20 }
21}
Equipped with these observers, we can now put our design pattern into action
C++ 1int main() {
2 NameObserver nameObserver;
3 AddressObserver addressObserver;
4
5 Person homer("Homer", "Simpson");
6 Person marge("Marge", "Simpson");
7 Person monty("Montgomery", "Burns");
8
9 homer.attach(&nameObserver);
10 marge.attach(&addressObserver);
11 monty.attach(&addressObserver);
12
13 home.forename("Homer Jay");
14 marge.address("123 Main St.");
15 monty.address("Springfield Nuclear Power Plant");
16
17 homer.detatch(&nameObserver);
18}
We create three persons and attach specific observers to them. Then, anytime the name changes for homer or the address changes for marge and monty we will see the update function called for the appropriate observer instance. If these observer classes are tied to the UI of our application, we can wire this up to quickly change our UI elements when the name and address likewise changes on our Person instances.
While the approach above works, a good modern C++ engineer should often look for ways to limit inheritance and reference semantics. To that end, here is a way to improve our Observer design pattern using std::function
C++ 1template<typename Subject, typename StateTag>
2class Observer {
3 public:
4 using OnUpdate = std::function<void(Subject const&, StateTag)>;
5
6 explicit Observer(OnUpdate onUpdate) : onUpdate_{ std::move(onUpdate)} {}
7
8 void update(subject, property);
9
10 private:
11 onUpdate onUpdate_;
12};
What we will see if we investiage the implementation in a new main() function is that we have simplified and expanded the flexibility of the Observer class
C++ 1void propertyChanged(Person const& person, Person::StateChange property) {
2 if (property == Person::forenameChanged || property == Person::surnameChanged) {
3 // Respond to name change
4 }
5}
6
7void main() {
8 using PersonObserver = Observer<Person, Person::StateChang>;
9
10 PersonObserver nameObserver(propertyChanged);
11
12 PersonObserver addressObserver([](Person const& person, Person::StateChange property) {
13 if (property == Person::addressChanged) {
14 // Respond to address change
15 }
16 });
17
18 Person homer("Homer", "Simpson");
19 Person marge("Marge", "Simpson");
20 Person monty("Montgomery", "Burns");
21
22 homer.attach(&nameObserver);
23 marge.attach(&addressObserver);
24 monty.attach(&addressObserver);
25
26 home.forename("Homer Jay");
27 marge.address("123 Main St.");
28 monty.address("Springfield Nuclear Power Plant");
29
30 homer.detatch(&nameObserver);
31}
We no longer need to define and construct classes that inherit from the Observer. Instead we can pass functions and llambdas into an instance of the templated observer class itself and have those handle changes as they occur.
There are three major types of observer patterns. The one given above as well as a push and a pull observer. With the push observer, our update function would need to send the updated state via a bunch of overloaded update functions:
C++1class Observer {
2 public:
3 virtual void update1(/* arguments for a specific state change */);
4 virtual void update2(/* arguments for a specific state change */);
5};
The other type is a pull observer which essentially sends the entire changed object with the update function and has the observer attempt to understand what specifically changed. The push observer has the advantage of potentially being more efficent since the observer does not need to query the observed class for a change, but increases code complexity with multiple update functions. The pull observer on the other hand simplifies the update function at the expense of more complexity in understanding what exactly changed in the observed class. The impelmentation provided above is the compromise between the two, providing only one update function but with an additional heuristic tag to help decipher what changed.
One final note.
Observer classes become complex very quickly. Especially in multithreaded enviornments. Properly handling attaching and detaching is a massive subject on its own. Security risks can also be problematic with this design pattern as a bad observer could lock up your program if it inproperly handles its callback. Using the observer design pattern is non-trivial and should be implemented with caution and further education.