談 C++17 裡的 Observer 模式 - 補

hedzr 發表於 2021-09-25
C++
上一回的 談 C++17 裡的 Observer 模式 還是有點慌張,所以需要補充完善一下下

Observer Pattern - Part II

多種 event (types) 問題

我們已經解釋過,如果你需要很多不同的 event 物件,那麼你應該擴充套件 event 結構成員:

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

這就好像設計一份通訊協議一般的做法,當然,後面的 body 部分應該是相對一致的資料型別才比較好,或者,採用 union 的解決方案。

進一步地,如果你的事件族非常龐大複雜,你可以採用派生類體系的方案:

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{});

放心,我們的 observable 具有足夠的容納能力。

在觀察者中修改被觀察者

請不要那麼做

這不是觀察者模式原本要承擔的責任。因此我們根本就不會 emit observable 本身,也正因如此正常情況下你並不能修改它——除非你用不道德的手段持有了一個被觀察者的例項參考,但這樣做真的是太壞了:用觀察者模式就是為了解耦的,你拿住目標的引用參考你禮貌嗎。

如果你真的想這麼做,也不是不行,但你需要自行 async 一下。c++ 的 async 關鍵字提供了一種簡便的非同步能力(其實就是隱含了一個新執行緒而已)。在非同步的上下文中修改被觀察者,你知道修改被觀察者本身可能會觸發新的事件,所以非同步的目的在於防止事件觀察的無限迴圈與死鎖。

如果某個事件的被觀察是無副作用的,那麼也可以直接做修改操作。這種情況在 DOS 時代叫做可重入的中斷程式。對的,那時候的中斷程式實際上就是一種觀察者模式,只不過它是以組合語言的形式組織的。

生命週期問題

採用 weak_ptr 保證了即使 observer 被提前釋放,也不會影響到 observable 的 emit 動作。反過來呢,observable 如果提前釋放了,則毫無任何可能的副作用。

動態修改觀察者鏈問題 - 改進後的新版本

上一版中的 observable 實現沒有做鎖定,因此若是在多執行緒環境動態修改觀察者鏈且發生 emit 時,會有競態問題。

因此針對這種可能,我們提供改進之後的、可託管的版本實現:

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

如果你知道觀察者不多,例如不過數個乃至數百個,那麼可以使用預設的 CNS = true 的演算法。這是一種先複製再交換(Copy-and-Swap)的方法,用一定的記憶體代價來換取更短的加鎖時間。但如果你會有成千上百萬的觀察者(真的會嗎?),請不要這麼做,使用 CNS - false 的工作模態,這不必消耗額外的記憶體,只不過鎖定的時間可能相對略長。

此外,啟用了加鎖特性的 observable 不能解決 emit 過程中的長時間鎖定問題,尤其是要注意若是某個觀察者太壞,則副作用會影響到整個 emit 乃至父級呼叫者。

輔助 RAII 類

為了幫助你臨時註冊觀察者,這裡也提供一個支援 RAII 特性的輔助類:

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

新的測試程式碼

所以測試程式碼也有所調整:

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{});}
}

後記

這次補充之後,總算是看得過去了,也稍微具有了點實用價值。

不過還存在一些遺憾,它們的一部分不應該由 observable observer pattern 負責解決,另一部分呢要留待其它解決思路去完成(例如 Rx 類似的非同步手段)。

另外,使用一個 observer 類有時候可能太傻了。這也是為什麼會有新的聲音發出來說不要有 observer。這個問題不算困難,只是風格不同。但今天沒力量完成了,下次看看是不是有興趣弄的話大概就不得不再次補充了。

也或許不。