上一回的 談 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。這個問題不算困難,只是風格不同。但今天沒力量完成了,下次看看是不是有興趣弄的話大概就不得不再次補充了。
也或許不。