C++ 是一种强类型的编程语言,于是最近一直在头疼如何实现一个比较优雅的观察者模式。
如果一个事件附带的参数不同的时候,是否可以使用统一的界面来注册?比如我定义了一个事件,A,参数是 int,而事件B的参数是 string,如果采用统一的注册接口?最先想到的办法是事件用int 定义,采用 boost::any 来传递参数,而结束函数统一写成 void hand( boost::any a ) 这样的形式,问题是这样的回调函数只能通过文档来约束参数值,很难知道 a 其实是什么类型的。这毕竟没有 void handle( int a ) 这样直观。
还有个方案是给每个事件写一个注册函数,但是太累人了,如果事件比较多,代码会很乱,事件的定义和对象写在一起,也导致代码而无法重用,耦合太高。
最近灵感一闪,于是有下面这个自我感觉比较优雅的东东:
/* 这是一个观察者模式的简化实现,有助于代码的解耦。
* 你可以预先定义一些事件,事件的定义使用OBSERVER_EVENT宏:
* OBSERVER_EVENT( Name, <Params> )
* Name 是事件的名称
* Params 是参数表
* 比如
* OBSERVER_EVENT( MyEvent, int, std::string, long )
*
* 有必要的时候,就可以将一个回调函数绑定到这个事件(订阅)
* observer a;
* a.subscribe<Name>( Handle );
* Name 是事件的名称
* Handle 是回调函数,它的参数,应该和事件的参数表匹配。
* 当然也可以撤销订阅
* a.unsubscribe<Name>();
*
* 当事件发生时,可以通过 observer 对象来发送事件
* a.shot<Name>( <Params> );
*
* 这个对象可以作为基类使用,以帮助对象解耦,这样设计的优点在于,事件的定义、回调的参数表
* 必须严格匹配,否则就会发生编译错误,以防止代码错误。
* 特别的,参数可以定义为引用类型,以便让回调函数可以修改它,这时发送事件时,要注意使用 ref() 来包装参数。
*/
当然,库里用了 大量宏和类型推导,代码比较难以阅读,一般人就别去看了。
但是我的原则是,库的代码再乱都没关系,但是接口、使用一定要简单。所以这个只有一个文件的库,使用上还是相当简单的:
这里采用类作为事件,实现都是编译器的,你的代码对不对,编译一下就行了。
你很难写出错误的代码,比如回调函数的参数写错了,编译就无法通过。
贴一个完整的测试程序:
#include "observer.hpp"
/// 事件定义
OBSERVER_EVENT(protocol_data_event, std::string, int )
void handle( const std::string& v, int& x )
{
std::cout << v << " " << x << std::endl;
x=20;
}
int main(int argc, char* argv[])
{
using namespace lugce;
observer a;
a.subscribe<protocol_data_event>( &handle );
int i=15;
a.shot<protocol_data_event>("hello", boost::ref(i) );
return 0;
}