Observer Pattern

The Observer pattern is a behavioral pattern that is a subscription-publishing mechanism. Objects are able to make announcements, and anyone who has registered observer status with the object will be able to receive notifications when such announcement events occur. Registering an identity means subscribing, and the event occurs means publishing.

There can be many observers doing subscriptions, at which point an observer chain is held in the observed object.

Engineering Difficulties

The usual C++ implementations focus on modeling the implementation of the pattern and not on practical qualities. So most of the implementations of the observer pattern you can see are case-based and do not support cross-threading, asynchronous, or non-blocking.

Due to the blocking one-way traversal nature of the observer chain, an unruly observer may hang up the notification chain of events that occur and furthermore hang up the whole thread.

However, solving this problem is not easy - for the most part we rely on the convention that observers must do their job in a disciplined manner and must finish their observations quickly. If you want to solve this problem completely, you will need a timeout interrupt mechanism, but this tends to make the entire code implementation of the observer pattern vague and complex, and in fact makes the technique infeasible.

This can be useful if you make your observer pattern implementations support asynchronous methods, but the problem with this is that the response latency of events is unpredictable (and left to the CPU thread scheduling feature). There are two ways to trigger non-blocking: either start a thread when the event occurs in order to traverse the observer chain and call back in turn, or facilitate the observer chain and call back in a new thread. Both approaches have their own characteristics, which you can evaluate in practice. In addition, using the standard library of concurrent threads provided by C++20 can help improve response latency.

Is it a misunderstanding?

I haven’t drawn a UML diagram for many years. In fact, I think this diagram is not very useful, it is not as straightforward intuition as reading the code directly, look at the diagram still have to translate it in the brain, look at the code, it seems that the brain when the CPU is very skilled ah, directly there.

Am I missing something, or, misunderstanding something.

Scenes

The Observer pattern is so easy to understand that there is no need to design a proper scenario to explain it.

A customer looks to see if a product has arrived in the store. I’m going to order a copy of the South China Morning Post. I order fresh milk from the dairy every morning. And so on.

Composition

Having said that (boring on uml), to quote a graph.

  1. 发布者 (Publisher) 会向其他对象发送值得关注的事件。 事件会在发布者自身状态改变或执行特定行为后发生。 发布者中包含一个允许新订阅者加入和当前订阅者离开列表的订阅构架。
  2. 当新事件发生时, 发送者会遍历订阅列表并调用每个订阅者对象的通知方法。 该方法是在订阅者接口中声明的。
  3. 订阅者 (Subscriber) 接口声明了通知接口。 在绝大多数情况下, 该接口仅包含一个 update 更新方法。 该方法可以拥有多个参数, 使发布者能在更新时传递事件的详细信息。
  4. 具体订阅者 (Concrete Subscribers) 可以执行一些操作来回应发布者的通知。 所有具体订阅者类都实现了同样的接口, 因此发布者不需要与具体类相耦合。
  5. 订阅者通常需要一些上下文信息来正确地处理更新。 因此, 发布者通常会将一些上下文数据作为通知方法的参数进行传递。 发布者也可将自身作为参数进行传递, 使订阅者直接获取所需的数据。
  6. 客户端 (Client) 会分别创建发布者和订阅者对象, 然后为订阅者注册发布者更新。

Realization

The new C++17 implementation of the observer pattern focuses on these aspects.

  • using smart pointers instead of the previous bare pointers, as well as fine-grained explicit management rights
  • Allowing different means of adding observers
  • Allowing custom Observer types
  • Preference for empty structs as event signals

Core templates observable and observer

A default recommended observer base class template provides you with the base constructor prototype. Your observer class should derive from this template. Unless you intend to define your own interface (though, to a large extent, the need to do so is infinitely close to zero, since the observable template requires an Observer to have an interface like observe(subject const&)).

As for the observable template itself, it contains ‘+=’ and ‘-=’ operator overloads, so you can use a more semantic coding approach.

The code is as follows (referenced in hz-common.hh).

 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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
namespace hicc::util {

  template<typename S>
  class observer {
    public:
    virtual ~observer() {}
    using subject_t = S;
    virtual void observe(subject_t const &e) = 0;
  };

  template<typename S, typename Observer = observer<S>, bool Managed = false>
  class observable {
    public:
    virtual ~observable() { clear(); }
    using subject_t = S;
    using observer_t_nacked = Observer;
    using observer_t = std::weak_ptr<observer_t_nacked>;
    using observer_t_shared = std::shared_ptr<observer_t_nacked>;
    observable &add_observer(observer_t const &o) {
      _observers.push_back(o);
      return (*this);
    }
    observable &add_observer(observer_t_shared &o) {
      observer_t wp = o;
      _observers.push_back(wp);
      return (*this);
    }
    observable &remove_observer(observer_t_shared &o) { return remove_observer(o.get()); }
    observable &remove_observer(observer_t_nacked *o) {
      _observers.erase(std::remove_if(_observers.begin(), _observers.end(), [o](observer_t const &rhs) {
        if (auto spt = rhs.lock())
          return spt.get() == o;
        return false;
      }), _observers.end());
      return (*this);
    }
    observable &operator+=(observer_t const &o) { return add_observer(o); }
    observable &operator+=(observer_t_shared &o) { return add_observer(o); }
    observable &operator-=(observer_t_nacked *o) { return remove_observer(o); }
    observable &operator-=(observer_t_shared &o) { return remove_observer(o); }

    public:
    /**
      * @brief fire an event along the observers chain.
      * @param event_or_subject 
      */
    void emit(subject_t const &event_or_subject) {
      for (auto const &wp : _observers)
        if (auto spt = wp.lock())
          spt->observe(event_or_subject);
    }

    private:
    void clear() {
      if (Managed) {
      }
    }

    private:
    std::vector<observer_t> _observers;
  };

} // namespace hicc::util

In the current implementation, the Managed template parameter of observable is useless, and the managed observer feature is not yet implemented, so you always have to manage each observer instance yourself. The observable only contains the weak_ptr of the observer, which sets the stage for adding asynchronous capabilities in the future, but for now its usefulness seems small.

There’s a lot of talk in the previous section, but that’s all the code in the core class template when it comes to concrete implementation, which isn’t too much.

test case

The method used is.

  • declaring event signals as structs, and you can include the necessary load in the structs, thus using a single struct to carry different event signals
  • but observable does not support you to provide multiple event signals of structure type
  • observable objects need to be derived from observable
  • The observable is created and registered to the observable using make_shareable

The sample code is as follows.

 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
namespace hicc::dp::observer::basic {

  struct event {};

  class Store : public hicc::util::observable<event> {};

  class Customer : public hicc::util::observer<event> {
    public:
    virtual ~Customer() {}
    bool operator==(const Customer &r) const { return this == &r; }
    void observe(const subject_t &) override {
      hicc_debug("event raised: %s", debug::type_name<subject_t>().data());
    }
  };

} // namespace hicc::dp::observer::basic

void test_observer_basic() {
  using namespace hicc::dp::observer::basic;

  Store store;
  Store::observer_t_shared c = std::make_shared<Customer>(); // uses Store::observer_t_shared rather than 'auto'
  store += c;
  store.emit(event{});
  store -= c;
}

Store is an observable object.

The Customer acts as an observer, registering with store += c and deregistering with store -= c.

Where appropriate, store.exit() fires an event signal out, which will then be received by all observers and interpreted however they see fit.

Note that smart pointer degradation.

  • must use Store::observer_t_shared c = std::make_shared<Customer>(); because the ‘+=’ and ‘-=’ operators recognize the hicc::util::observable<event>::observer_t_shared type
  • If you use auto c = std::make_shared<Customer>(), they cannot be derived by ‘+=’ or ‘-=’ and the compilation will not complete
  • could consider solving this problem with CRTP techniques, but the need is not really great - you can complain about it and I might get motivated

Remaining issues.

  • There is no mechanism to prevent duplicate observer registration. It’s not difficult to add it, but we don’t think you should write code for duplicate registration, so we don’t care if it’s duplicate or not, you do it ~

Epilogue

We have not been able to solve the hard problem. The quandary raised above can only be solved by a realistic and enhanced version of the Observer pattern, Rx/ReactiveX. ReactiveX, however, is not a pure subscription pattern at all, and the threshold is too high.

So, or, maybe, next time consider a simpler Rx, you know ReactiveX already has RxCpp, but we might get a simple version of Rx, with the main purpose of adding asynchronous capabilities to the observer pattern, and just drop the operators.

There is another famous implementation of the subscriber, or observer, pattern: Qt’s Signal-Slot mechanism. This relies on Qt’s QObject to provide a mechanism by which a connect can be triggered by a signal. It is almost equivalent to the observer pattern, but emphasizes the notion of sender and receiver, which may not be necessary for most implementations of the observer pattern. But the signal slot mechanism provides developers before C++11 with the ability to call back a slot function with parameters without association, which no one could do at the beginning, even after C++11, due to the template change in the perfect forwarding sometimes syntax is not perfect, but also until after C++14/17 to have a comprehensive beyond. So now it is, slot this mechanism has lost its allure, just in the stubborn, and the actual application, unless you are using qt, most or just do an observable is also very light and easy.