上上回的 谈 C++17 里的 Observer 模式 介绍了该模式的根本结构。起初在 谈 C++17 里的 Observer 模式 - 补 外面提供了改良版本,次要聚焦于针对多线程环境的暴力应用的场景。
也能够返回 博客原文
Observer Pattern - Part III
而后咱们提到了,对于观察者模式来说,GoF 的原生定义当然是采纳一个 observer class 的形式,但对于差不多 15 年后的 C++11 来说,观察者应用一个 class 定义的形式有点掉队了。特地是到了简直 23 年后的 C++14/17 之后,lambda 以及 std::function 的反对力度变得较为稳固,无需太多“高级”手法也能轻松地包装闭包或者函数对象,在加上折叠表达式对变参模板的加成能力。所以当初是有一种呼声认为,间接在被观察者上绑定匿名函数对象、或者函数对象,才是观察者模式的正确打开方式。
那么是不是如此呢?
咱们首先要做的是实现这样的想法,而后用一段测试用例来展现这种模态下编码的可能性。再而后才来看看它的优缺点。
根本实现
这一次的外围类模板咱们将其命名为 observable_bindable
,因为这在你批改本人的实现代码时无利——只须要增加后缀就能够。这个模板类依然应用一个繁多的构造 S
作为事件/信号实体:
namespace hicc::util { /** * @brief an observable object, which allows a lambda or a function to be bound as the observer. * @tparam S subject or event will be emitted to all bound observers. * */ template<typename S> class observable_bindable { public: virtual ~observable_bindable() { clear(); } using subject_t = S; using FN = std::function<void(subject_t const &)>; template<typename _Callable, typename... _Args> observable_bindable &add_callback(_Callable &&f, _Args &&...args) { FN fn = std::bind(std::forward<_Callable>(f), std::forward<_Args>(args)...); _callbacks.push_back(fn); return (*this); } template<typename _Callable, typename... _Args> observable_bindable &on(_Callable &&f, _Args &&...args) { return add_callback(f, args...); } /** * @brief fire an event along the observers chain. * @param event_or_subject */ void emit(subject_t const &event_or_subject) { for (auto &fn : _callbacks) fn(event_or_subject); } private: void clear() {} private: std::vector<FN> _callbacks{}; };}
首先,咱们不提供解除 observer 绑定的成员函数。咱们感觉这是不必要的,因为这一设计的指标原本就是冲着 lambda 函数去的,解除 lambda 函数的绑定,没有多大意义。当然你能够实现一份 remove_observer,这并没有什么技术性难度,甚至你都不用如许懂 c++,照猫画虎也能弄一份。
而后借助于 std::bind 函数(整个绑定乃至于类型推导等等至多 c++14,倡议 c++17)的推导能力,咱们提供了一个强力无效的 add_callback 实现,另外,on() 是它的同义词。
所谓强力无效,是指,咱们在这个繁多的函数签名上实现了各种各样的函数对象的通用绑定,匿名函数也好、成员函数也好、或者一般函数等,都能够借助于 add_callback() 被绑定到 observable_bindable 对象中去。
新的测试代码
到底有如许无力,还是要看测试代码:
namespace hicc::dp::observer::cb { struct event { std::string to_string() const { return "event"; } }; struct mouse_move_event : public event {}; class Store : public hicc::util::observable_bindable<event> {};} // namespace hicc::dp::observer::cbvoid fntest(hicc::dp::observer::cb::event const &e) { hicc_print("event CB regular function: %s", e.to_string().c_str());}void test_observer_cb() { using namespace hicc::dp::observer::cb; using namespace std::placeholders; Store store; store.add_callback([](event const &e) { hicc_print("event CB lamdba: %s", e.to_string().c_str()); }, _1); struct eh1 { void cb(event const &e) { hicc_print("event CB member fn: %s", e.to_string().c_str()); } void operator()(event const &e) { hicc_print("event CB member operator() fn: %s", e.to_string().c_str()); } }; store.on(&eh1::cb, eh1{}, _1); store.on(&eh1::operator(), eh1{}, _1); store.on(fntest, _1); store.emit(mouse_move_event{});}
留神,这个 on()/add_callback() 的绑定语法相似于 std::bind
,你可能须要 std::placeholder::_1, _2, ..
等等占位符。你还须要留神到这个绑定语法齐全因循了 std::bind 的各种非凡能力,例如提前绑定技术。不过这些能力有的在 on() 中无奈间接体现,有的尽管有用然而却又用不到,此外这里毕竟还在议论 observer pattern,现有的展现曾经足够了。
输入后果相似于如此:
--- BEGIN OF test_observer_cb ----------------------09/19/21 08:38:02 [debug]: event CB lamdba: event ...09/19/21 08:38:02 [debug]: event CB member fn: event ...09/19/21 08:38:02 [debug]: event CB member operator() fn: event 09/19/21 08:38:02 [debug]: event CB regular function: event --- END OF test_observer_cb ----------------------It took 465.238us
所以没有什么意外,甚至于无论是 observable_bindable 还是用例代码都是出其不意的简洁明快,合乎直觉。
这个例子可能揭示咱们,GoF 当然是经典里的经典(没方法,它出的早,它出的时候咱们的脑子基本没往这种总结方向下来转,只好是它经典了,我那时候在干啥哩,哦,我在贵州一个鸟不生蛋的深山里的变电所搞什么 scada 调试吧,陷入细节之中),但也未必就要祖宗之法不可易。
改良
下面显得颇为完满了,然而还有一个小小的问题。康康测试代码的绑定语法,例如:
store.on(fntest, _1);
那个俊俏的 _1
很是扎眼。能不能毁灭它呢?
因为咱们约定的回调函数的接口为:
using FN = std::function<void(subject_t const &)>;
所以这个 _1
是对应于 subject_t const &
,这是 std::bind 的调用约定。留神到回调函数的签名是固定的,所以咱们的确有一个办法可能打消 _1
,即批改 add_callback()
的代码:
template<typename _Callable, typename... _Args>observable_bindable &add_callback(_Callable &&f, _Args &&...args) { FN fn = std::bind(std::forward<_Callable>(f), std::forward<_Args>(args)..., std::placeholders::_1); _callbacks.push_back(fn); return (*this);}
咱们加上它,用户代码中就不用写它了对吗。
这法子有点点无赖,但它管用。而后测试代码中是这样:
store.add_callback([](event const &e) { hicc_print("event CB lamdba: %s", e.to_string().c_str()); }); store.on(&eh1::cb, eh1{}); store.on(&eh1::operator(), eh1{}); store.on(fntest);
是不是难看多了?
这个例子通知咱们,写 C++ 有时候基本不须要那些所谓的精美的、粗劣的、绝妙的、奇思妙想的奇技淫巧。
善战者赫赫无功,说的是就是平平无奇。
优缺点
总之,咱们曾经实现了这个主打函数式格调的观察者模式。你依然须要从 observable_bindable
派生进去你的被观察者,但你能够随时随地就地建设对其的察看,你能够应用匿名的 lambda,也能够应用各种格调的具名的函数对象。
至于毛病,不算太多,或者,采纳 lambda 的时候就不那么容易 remove_callback 了,这对于有的须要重入的场景可能有点不妥,但这能够用显式具名的形式轻松解决。
有两种显式具名的办法,一是将 lambda 付给一个变量:
auto my_observer = [](event const &e) { hicc_print("event CB lamdba: %s", e.to_string().c_str());};store.on(my_observer);store.emit(event{});store.remove(my_observer);
remove() 须要你自行实现
另一种办法是一般函数对象、或者类成员函数对象、或者类动态成员函数对象,这些都是人造具名的。例子略过。
后记
整个过程很简略,甚至比我预想的还简略,当然我还是遇到了点麻烦的。
次要的麻烦在于函数签名中的形参列表须要被正确地传导到 emit 中的调用局部去。然而你也看到了,最终的代码其实齐全借助了主动推导的能力,出乎意料地间接解决了该问题。我当然遇到了麻烦,走了些弯路,设法想要求助于变参模板能力以及 traits 的力量,事实证明不用那么累,也没有那么简单的形参表的必要。而且 auto 经常能够打天下。
Callable 技术,我在 pool,ticker 等之中也有同样的利用,然而那些时候有一点点不同,那时候待绑定的函数对象是没有参数表的。而在 cmdr-cxx 中做函数绑定时,我面临的是确定格局的参数表。
不过这些也都无所谓。