谈|谈 C++17 里的 Observer 模式 - 补
上一回的 谈 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
class observer {
public:
virtual ~observer() {}
using subject_t = S;
virtual void observe(subject_t const &e) = 0;
};
/**
* @brief
* @tparam S
* @tparam Observer
* @tparam AutoLockthread-safe even if modifying observers chain dynamically
* @tparam CNSuse Copy-and-Swap to shorten locking time.
*/
template
class observable {
public:
virtual ~observable() { clear();
}
using subject_t = S;
using observer_t_nacked = Observer;
using observer_t = std::weak_ptr;
using observer_t_shared = std::shared_ptr;
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 _observers{};
std::mutex _m{};
};
} // namespace hicc::util
如果你知道观察者不多,例如不过数个乃至数百个,那么可以使用默认的 CNS = true 的算法。这是一种先复制再交换(Copy-and-Swap)的方法,用一定的内存代价来换取更短的加锁时间。但如果你会有成千上百万的观察者(真的会吗?),请不要这么做,使用 CNS - false 的工作模态,这不必消耗额外的内存,只不过锁定的时间可能相对略长。
此外,启用了加锁特性的 observable 不能解决 emit 过程中的长时间锁定问题,尤其是要注意若是某个观察者太坏,则副作用会影响到整个 emit 乃至父级调用者。
辅助 RAII 类 为了帮助你临时注册观察者,这里也提供一个支持 RAII 特性的辅助类:
namespace hicc::util {template
struct registerer {
using _Observable = observable;
_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 {};
class Customer : public hicc::util::observer {
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().data());
}
};
} // namespace hicc::dp::observer::basicvoid test_observer_basic() {
using namespace hicc::dp::observer::basic;
Store store;
Store::observer_t_shared c = std::make_shared();
// uses Store::observer_t_shared rather than 'auto'store += c;
store.emit(event{});
store -= c;
{hicc::util::registerer __r(store, c);
store.emit(event{});
}
}
后记 这次补充之后,总算是看得过去了,也稍微具有了点实用价值。
不过还存在一些遗憾,它们的一部分不应该由 observable observer pattern 负责解决,另一部分呢要留待其它解决思路去完成(例如 Rx 类似的异步手段)。
另外,使用一个 observer 类有时候可能太傻了。这也是为什么会有新的声音发出来说不要有 observer。这个问题不算困难,只是风格不同。但今天没力量完成了,下次看看是不是有兴趣弄的话大概就不得不再次补充了。
也或许不。
【谈|谈 C++17 里的 Observer 模式 - 补】
推荐阅读
- 2019-02-13——今天谈梦想()
- 村里的故事|村里的故事 --赵大头
- 你眼里的不公平,其实很公平
- (七)谈条件
- 山香|山香 善思 智学访谈
- 从我的第一张健身卡谈传统健身房
- 时光,是行走在文字里的眷恋
- 我在一条路上走了5年
- FBI怎么和恐怖分子谈判
- 跟身体谈恋爱