头图
The last time talked about the Observer mode in C++17. is still a bit panicked, so I need to add and improve it.

Observer Pattern - Part II

Multiple event (types) issues

We have already explained that if you need many different event objects, then you should extend the event structure members:

struct event {
  enum EventType type;
  ... // extras body
};

This is like the general practice of designing a communication protocol. Of course, the latter body part should be a relatively consistent data type, or it is better to adopt a union solution.

Further, if your event family is very large and complex, you can use the solution of the derived class system:

struct event {
  enum EventType type;
  ... // extras body
};

struct mouse_move_event : public event {
  int x, y;
  int modifiers;
};

struct kb_event : public event {
  int key_code, scan_code;
  int modifiers;
  bool pressed_or_released_or_holding;
};

// ...
store.emit(mouse_move_event{});

Rest assured, our observable has enough capacity.

Modify the observed person in the observer

Please do not do .

This is not the original responsibility of the observer model. Therefore, we will not emit observable itself, and because of this, you can’t modify it under normal circumstances—unless you use unethical to hold a reference to an instance of the observed person, but this is really true. Too bad: the observer pattern is used for decoupling, are you polite to hold the reference of the target?

If you really want to do this, it's not impossible, but you need to async yourself. The async keyword of c++ provides a convenient asynchronous capability (in fact, it implies a new thread). Modifying the observer in the context of asynchronous, you know that modifying the observer itself may trigger new events, so the purpose of asynchrony is to prevent the infinite loop and deadlock of event observation.

If an event is observed without side effects, then it can also be modified directly. This situation was called a reentrant interrupt program in the DOS era. Yes, the interrupt program at that time was actually an observer mode, but it was organized in assembly language.

Life cycle issues

The use of weak_ptr ensures that even if the observer is released early, it will not affect the observable's emit action. Conversely, if the observable is released early, there will be no possible side effects.

Dynamically modify the observer chain problem-the new and improved version

The observable implementation in the previous version did not lock, so if the observer chain is dynamically modified in a multi-threaded environment and emit occurs, there will be race conditions.

Therefore, in response to this possibility, we provide an improved, managed version implementation:

namespace hicc::util {

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

  /**
   * @brief 
   * @tparam S 
   * @tparam Observer 
   * @tparam AutoLock  thread-safe even if modifying observers chain dynamically
   * @tparam CNS       use Copy-and-Swap to shorten locking time.
   */
  template<typename S, bool AutoLock = false, bool CNS = true, typename Observer = observer<S>>
  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) {
      if (AutoLock) {
        if (CNS) {
          auto copy = _observers;
          copy.push_back(o);
          std::lock_guard _l(_m);
          _observers.swap(copy);
        } else {
          std::lock_guard _l(_m);
          _observers.push_back(o);
        }
      } else
        _observers.push_back(o);
      return (*this);
    }
    observable &add_observer(observer_t_shared &o) {
      observer_t wp = o;
      if (AutoLock) {
        if (CNS) {
          auto copy = _observers;
          copy.push_back(wp);
          std::lock_guard _l(_m);
          _observers.swap(copy);
        } else {
          std::lock_guard _l(_m);
          _observers.push_back(wp);
        }
      } else
        _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) {
      if (AutoLock) {
        if (CNS) {
          auto copy = _observers;
          copy.erase(std::remove_if(copy.begin(), copy.end(), [o](observer_t const &rhs) {
            if (auto spt = rhs.lock())
              return spt.get() == o;
            return false;
          }),
                     copy.end());
          std::lock_guard _l(_m);
          _observers.swap(copy);
        } else {
          std::lock_guard _l(_m);
          _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());
        }
      } else
        _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);
    }
    friend observable &operator+(observable &lhs, observer_t_shared &o) { return lhs.add_observer(o); }
    friend observable &operator+(observable &lhs, observer_t const &o) { return lhs.add_observer(o); }
    friend observable &operator-(observable &lhs, observer_t_shared &o) { return lhs.remove_observer(o); }
    friend observable &operator-(observable &lhs, observer_t_nacked *o) { return lhs.remove_observer(o); }
    observable &operator+=(observer_t_shared &o) { return add_observer(o); }
    observable &operator+=(observer_t const &o) { return add_observer(o); }
    observable &operator-=(observer_t_shared &o) { return remove_observer(o); }
    observable &operator-=(observer_t_nacked *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) {
      if (AutoLock) {
        std::lock_guard _l(_m);
        for (auto const &wp : _observers)
          if (auto spt = wp.lock())
            spt->observe(event_or_subject);
      } else {
        for (auto const &wp : _observers)
          if (auto spt = wp.lock())
            spt->observe(event_or_subject);
      }
    }

    private:
    void clear() {
      if (AutoLock) {
        std::lock_guard _l(_m);
        _observers.clear();
      }
    }

    private:
    std::vector<observer_t> _observers{};
    std::mutex _m{};
  };

} // namespace hicc::util

If you know that there are not many observers, such as a few or even hundreds, you can use the default CNS = true algorithm. This is a copy-and-swap method that uses a certain memory cost in exchange for a shorter lock time. But if you have tens of millions of observers (really?), please don't do this, use the CNS-false working mode, which does not need to consume additional memory, but the lock time may be relatively small long.

In addition, the observable with the lock feature enabled cannot solve the long-term locking problem during the emit process. In particular, it should be noted that if an observer is too bad, the side effects will affect the entire emit and even the parent caller.

Auxiliary RAII category

To help you register observers temporarily, here is also an auxiliary class that supports RAII features:

namespace hicc::util {

  template<typename S, bool AutoLock = false, bool CNS = true, typename Observer = observer<S>>
  struct registerer {
    using _Observable = observable<S, AutoLock, CNS, Observer>;
    _Observable &_observable;
    typename _Observable::observer_t_shared &_observer;
    registerer(_Observable &observable, typename _Observable::observer_t_shared &observer)
      : _observable(observable)
        , _observer(observer) {
        _observable += _observer;
      }
    ~registerer() {
      _observable -= _observer;
    }
  };

} // namespace hicc::util

New test code

So the test code has also been adjusted:

namespace hicc::dp::observer::basic {

  struct event {};

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

  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;

  {hicc::util::registerer<event, true> __r(store, c);
  store.emit(event{});}
}

postscript

After this supplement, it is finally readable, and it has a little practical value.

However, there are still some regrets. Some of them should not be solved by the observable observer pattern, and the other part should be left to other solutions (such as asynchronous methods like Rx).

In addition, using an observer class can sometimes be too silly. This is why there is a new voice saying not to have an observer. This question is not difficult, but the style is different. But today I don't have the power to complete it. Next time I see if I am interested in doing it, I will probably have to add it again.

Or maybe not.

:end:


hedzr
95 声望19 粉丝