loop_in_codes

低调做技术__欢迎移步我的独立博客 codemaro.com 微博 kevinlynx

低耦合模块间的通信组件:两个模板

用途

在一个UI与逻辑模块交互比较多的程序中,因为并不想让两个模块发生太大的耦合,基本目标是
可以完全不改代码地换一个UI。逻辑模块需要在产生一些事件后通知到UI模块,并且在这个通知
里携带足够多的信息(数据)给接收通知的模块,例如UI模块。逻辑模块还可能被放置于与UI模
块不同的线程里。

最初的结构

最开始我直接采用最简单的方法,逻辑模块保存一个UI模块传过来的listener。当有事件发生时,
就回调相应的接口将此通知传出去。大致结构如下:

 /// Logic
 class EventNotify
 
{
 
public:
  
virtual void OnEnterRgn( Player *player, long rgn_id );
 }
;

 
/// UI
 class EventNotifyImpl : public EventNotify
 
{
 }
;

 
/// Logic
 GetEventNotify()->OnEnterRgn( player, rgn_id );

 

但是,在代码越写越多之后,逻辑模块需要通知的事件越来越多之后,EventNotify这个类开始
膨胀:接口变多了、不同接口定义的参数看起来也越来越恶心了。

改进

于是我决定将各种事件通知统一化:

 

struct Event
{
 
long type; // 事件类型
  // 附属参数
}
;

 

这样,逻辑模块只需要创建事件结构,两个模块间的通信就只需要一个接口即可:

void OnNotify( const Event &event );

但是问题又来了,不同的事件类型携带的附属参数(数据)不一样。也许,可以使用一个序列化
的组件,将各种数据先序列化,然后在事件处理模块对应地取数据出来。这样做总感觉有点大动
干戈了。当然,也可以使用C语言里的不定参数去解决,如:

void OnNotify( long event_type, ... )

其实,我需要的就是一个可以表面上类型一样,但其内部保存的数据却多样的东西。这样一想,
模块就能让事情简单化:

 

template <typename P1, typename P2>
class Param
{
public:
 Param( P1 p1, P2 p2 ) : _p1( p1 ), _p2( p2 )
 
{
 }

 
 P1 _p1;
 P2 _p2;
}
;

template 
<typename P1, typename P2>
void OnNotify( long event_type, Param<P1, P2> param );

GetNotify()
->OnNotify( ET_ENTER_RGN, Param<Player*long>( player, rgn_id ) );
GetNotify()
->OnNotify( ET_MOVE, Param<longlong>( x, y ) );

 

在上面这个例子中,虽然通过Param的包装,逻辑模块可以在事件通知里放置任意类型的数据,但
毕竟只支持2个参数。实际上为了实现支持多个参数(起码得有15个),还是免不了自己实现多个
参数的Param。

幸亏我以前写过宏递归产生代码的东西,可以自动地生成这种情况下诸如Param1、Param2的代码。
如:

 

#define CREATE_PARAM( n ) \
 template 
<DEF_PARAM( n )> \
 
struct Param##n \
 
{ \
  DEF_PARAM_TYPE( n ); \
  Param##n( DEF_FUNC_PARAM( n ) ) \
  
{ \
   DEF_MEM_VAR_ASSIGN( n ); \
  }
 \
  DEF_VAR_DEF( n ); \
 }


 CREATE_PARAM( 
1 );
 CREATE_PARAM( 
2 );

 

即可生成Param1和Param2的版本。其实这样定义了Param1、Param2的东西之后,又使得OnNotify
的参数不是特定的了。虽然可以把Param也泛化,但是在逻辑层写过多的模板代码,总感觉不好。

于是又想到以前写的一个东西,可以把各种类型包装成一种类型---对于外界而言:any。any在
boost中有提到,我只是实现了个简单的版本。any的大致实现手法就是在内部通过多态机制将各
种类型在某种程度上隐藏,如:

 

        class base_type
        
{
        
public:
            
virtual ~base_type()
            
{
            }

            
virtual base_type *clone() const = 0;
        }
;
        
        template 
<typename _Tp>
        
class var_holder : public base_type
        
{
        
public:
            typedef _Tp type;
            typedef var_holder
<type> self;
        
public:
            var_holder( 
const type &t ) : _t( t )
            
{
            }


            base_type 
*clone() const
            
{
                
return new self( _t );
            }

        
public:
            type _t;
        }


这样,any类通过一个base_type类,利用C++多态机制即可将类型隐藏于var_holder里。那么,
最终的事件通知接口成为下面的样子:

void OnNotify( long type, any data );

OnNotify( ET_ENTER_RGN, any( create_param( player, rgn_id ) ) );其中,create_param
是一个辅助函数,用于创建各种Param对象。

事实上,实现各种ParamN版本,让其名字不一样其实有点不妥。还有一种方法可以让Param的名字
只有一个,那就是模板偏特化。例如:

 

template <typename _Tp>
struct Param;

template 
<>
struct Param<void()>;

template 
<typename P1>
struct Param<void(P1)>

template 
<typename P1, typename P2>
struct Param<void(P1,P2)>

 

这种方法主要是通过组合出一种函数类型,来实现偏特化。因为我觉得构造一个函数类型给主模版,
并不是一种合情理的事情。但是,即使使用偏特化来让Param名字看起来只有一个,但对于不同的
实例化版本,还是不同的类型,所以还是需要any来包装。

实际使用

实际使用起来让我觉得非常赏心悦目。上面做的这些事情,实际上是做了一个不同模块间零耦合
通信的通道(零耦合似乎有点过激)。现在逻辑模块通知UI模块,只需要定义新的事件类型,在
两边分别写通知和处理通知的代码即可。

PS:
针对一些评论,我再解释下。其实any只是用于包装Param列表而已,这里也可以用void*,再转成
Param*。在这里过多地关注是用any*还是用void*其实偏离了本文的重点。本文的重点其实是Param:

 

OnNotify( NT_ENTER_RGN, ang( create_param( player, rgn_id ) ) );

->
void OnNotify( long type, any data )
{
 Param2
<Player*long> ParamType;
 ParamType 
*= any_cast<ParamType>&data );
 Player 
*player = p->p1;
 
long rgn_id = p->p2;
}




下载相关代码

posted on 2009-08-23 09:55 Kevin Lynx 阅读(6277) 评论(18)  编辑 收藏 引用 所属分类: 模块架构

评论

# re: 低耦合模块间的通信组件:两个模板[未登录] 2009-08-23 10:19 Davy.xu

很好的设计,收下了  回复  更多评论   

# re: 低耦合模块间的通信组件:两个模板[未登录] 2009-08-23 10:58 megax

唉,我还是用void*吧  回复  更多评论   

# re: 低耦合模块间的通信组件:两个模板 2009-08-23 13:27 OwnWaterloo

@megax
同意。 一旦any_cast出错,就是一个不能逃避,必须得修改掉的错误。
相对于void*, any能起到的作用只是开发时的debug?
发布时再改回无检查的void* ? 还要注意释放的问题……
  回复  更多评论   

# re: 低耦合模块间的通信组件:两个模板 2009-08-23 13:35 OwnWaterloo

嘿嘿嘿嘿~~~
设计来、设计去,回到了经典的WndProc模式:
typedef HRESULT (CALLBACK* WndProc)(HWND,UINT,WPARAM,LPARAM);


因为现在有模板这个高级货, 所以HWND,WPARAM,LPARAM可以塞到一起,变成:
typedef R (*OnNotify)(long type, xxx data);


然后呢, 为了更方便的将各种东西塞入data, lz创造了一个 Param.
试试boost.tuple?

为了让一个签名接受不同类型的tuple/Param, 再将它们塞入一个any/void*。


旧瓶新酒~~~  回复  更多评论   

# re: 低耦合模块间的通信组件:两个模板 2009-08-23 13:38 Kevin Lynx

@OwnWaterloo
对,就是你说的这个意思。我也记得有个tuple这个东西。但是我一般不用boost,太大,太多。可以用来学习。:)
  回复  更多评论   

# re: 低耦合模块间的通信组件:两个模板 2009-08-23 13:53 OwnWaterloo

@Kevin Lynx
嗯,boost确实有点重……
那么我们抛弃boost::tuple, 使用std::tr1::tuple吧~~~

不过any…… 这可怜的家伙没被采纳…… 得自己实现……  回复  更多评论   

# re: 低耦合模块间的通信组件:两个模板 2009-08-23 14:58 OwnWaterloo

光顾着说笑了…… 说点正事。
这样做算不上"降低耦合"吧?

void OnE1(tuple<e11,...> );
void OnE2(tuple<e21,...> );
void OnE3(tuple<e31,...> );

和:

void OnEvent(int e, any );

的耦合性不是完全一样么?
log和UI依然必须"协商"每个事件的参数是怎样的。


只是后一种方式, 将通信的接口纳入一个之中, 而不是每产生一个新事件就添加一个, 也就是说, 这个接口可以完全固定下来了, 不会再发生变化。

  回复  更多评论   

# re: 低耦合模块间的通信组件:两个模板 2009-08-23 15:31 Kevin Lynx

@OwnWaterloo
是啊。严格来说,不算降低耦合。因为UI和逻辑层确实得协商设置哪些通知事件。不过,反正这个逻辑模块本身就是要通知UI事件触发的。这样做之后,逻辑模块不需要管UI是否处理,只需要通知。在任何地方想怎样通知就通知。整个程序换了UI后,逻辑层也不需要改,只改UI的通知处理即可。
另一方面,如果使用
void OnE1(tuple<e11,...> );
void OnE2(tuple<e21,...> );
void OnE3(tuple<e31,...> );
这种方式,就会涉及到添加很多通知接口。这个负责通信的中间层就会越来越庞大。虽然,OnEvent(int e, any );这个方式会导致添加越来越多的事件类型定义,但总比添加一个OnEn好吧?:)
很少做这种UI比较复杂的应用程序,不知道其他人有没有好的模块架构方法。实在不想把UI和逻辑揉得那么紧。看过一些人直接在MFC的OnXXX里写一堆逻辑代码就觉得受不了。 我现在做的东西里就包含MFC和控制台两套UI,甚至在GUI方面差点换到bcb。
  回复  更多评论   

# re: 低耦合模块间的通信组件:两个模板 2009-08-23 15:50 OwnWaterloo

@Kevin Lynx
我说说我的看法啊, 你看看是否合理。

将:
void OnE1(tuple<e11,...> );
void OnE2(tuple<e21,...> );
void OnE3(tuple<e31,...> );

转化为:
OnEvent(int e, any );

是合理的,因为这样做可以很好的适应"增加event种类"这一变化。
对"需要增加很多event"或者"event暂时未知"的情况, 是很有用的。
也算是降低了耦合度吧, 因为
>逻辑层也不需要改,只改UI的通知处理即可。
嗯……

相比一些其他的很严格的抽象方式, 比如将每个OnE1设置为纯虚…… 那简直是自讨苦吃……
一旦增加一个OnEx的纯虚, ui代码就需要改。
如果不是纯虚, 或者使用OnEvent(int e, any );就有一个"缓合"的机会, 可以慢慢来~~~



但是, 我有一个猜测, 你看看对不对:
OnEvent(int e, any );
添加这一层, 总工作量不会有丝毫减少, 甚至会增加。
有可能ui端会:
switch (e)

如果e的种类继续增加, 还可能在ui端继续转变为:
ReplyE1(tuple<E11, ... > );
ReplyE2(tuple<E21, ... > );
ReplyE3(tuple<E31, ... > );


再次申明啊, 我觉得OnEvent(int e, any );是有好处的。
logic和ui通过这个通信。
logic内部是否NotifyE1,NotifyE2的形式? UI是否采用RE1,RE2的形式?
这个是完全解耦了。
只是数据格式始终解不了。

>看过一些人直接在MFC的OnXXX里写一堆逻辑代码就觉得受不了。
嗯…… 我曾经就写过让你受不了的代码~~~
当然, 现在我也受不了那代码了……
但是呢, 现在又没机会去做这种logic,ui分离的事, 只是猜测OnEvent(int e, any );会使得总代码量增加。
希望能得到你的经验之谈~~~

  回复  更多评论   

# re: 低耦合模块间的通信组件:两个模板 2009-08-23 16:13 Kevin Lynx

@OwnWaterloo
其实工作量这事,本来也就没有被减少。就工作量(代码量)这个角度来比较两者的话。OnE1 OnE2和OnEvent(以后讨论就说前者后者)比较而言,在增加新的事件通知时,前者需要增加通信层接口声明(也就是那个被UI继承的基类);后者需要从Param逐个取数据,前者是直接在参数里,如果前者也使用Param或者tuple来组织参数,也免不了解参数。

我觉得后者较前者让人爽的好处就是:永远不需要改这个中间通信层。

说下我现在是怎么派发通知的,这个其实也被放在这个通信中间层:
class OpNotify
{
typedef void (*HandleNotifyFnT)( any *data );
public:
AddNotifyHandler( long event_id, HandleNotifyFnT fn );
void Notify( long event_id, any data );
private:
std::map<long, HandleNotifyFnT> handleFuncTable;
};
UI层需要注册处理事件通知的函数到handleFuncTable里。逻辑层每次派发事件通知时,调用OpNotify::Notify,这个函数简单地从handleFuncTable里找对应的处理函数。
这个样子之后,避开了switch...case。谁都知道,随着事件类型的增加,switch...case也将急速膨胀。进一步地,通信中间层永远不需要修改了。

现在逻辑层派发事件通知时:
OP_NOTIFY( NT_ENTER_RGN, any( create_param( player, rgn_id ) ) );
// OP_NOTIFY被定义为OpNotify::Notify

UI层只需要定义NT_ENTER_RGN的处理函数,并注册到OpNotify,该处理函数大致为:
void HandleEnterRgn( any *data )
{
typedef Param2<Player, long> ParamType;
ParamType *p = any_cast<ParamType>( data );
Player *player = p->p1;
long rgn_id = p->p2;
}

  回复  更多评论   

# re: 低耦合模块间的通信组件:两个模板 2009-08-24 01:54 qinqing

很好,受益了  回复  更多评论   

# re: 低耦合模块间的通信组件:两个模板 2009-08-24 01:55 qinqing

这种代码的设计是没有最好方法的,依赖于语言的特性  回复  更多评论   

# re: 低耦合模块间的通信组件:两个模板 2009-08-24 09:03 欲三更

1 这个东西能实现逻辑层和UI层在不同线程里的事件机制? 没看出来.

2 这么执着的降低耦合,应该是比较大的程序里才有用吧? 但是模板机制只能在一个模块里使用,这个限制还是比较大的.

3 把耦合降到最小,似乎需要加一层"事件解释机制". 逻辑和UI的最大耦合不在于回调的形式上,而在于事件解释机制的分散.逻辑层发生的事件是诸如"底层数据更新"的事件,UI层的响应是"ListView重载入" 这样的, 把这些之间的解释和映射放到一起,无论什么形式的回调机制都可以.

纯个人感觉, 不要相信:)  回复  更多评论   

# re: 低耦合模块间的通信组件:两个模板 2009-08-25 15:48 expter

太花哨了。。

先看看,在慢慢消化  回复  更多评论   

# re: 低耦合模块间的通信组件:两个模板 2009-08-31 12:27 codespy

boost不是有信号槽机制吗,这个机制不就是用于解决此问题的吗?  回复  更多评论   

# re: 低耦合模块间的通信组件:两个模板 2009-10-09 14:14 starwing

这个问题,其实不一定非要是“协商”。可以是ui规定好了事件,让logic来遵守(WinProc,或者gtk的模式),又或者是logic规定好了接口,让ui来调用。总归有个主次。当然,也可以把这个主次去掉,把协商实体化,做成controler,也就是MVC了。

针对这个问题来说,其实可以将这个“通道”抽象出来。由ui在其中注册事件,由logic查询可用的事件,然后hook,然后由ui来调用hook,这样恐怕能更模块化一点,不过这个就是纯C的方案了。如果是要面向对象,可以让通道保留ui和logic类,然后ui和logic各保留通道类,然后就跟刚才一样,由ui注册事件,由logic来注册hook了。优势在于,可以一个logic管理多个ui,可以一个ui由多个logic注册hook(因为hook顺次执行),也可以多个logic对多个ui协商。并且因为logic是要查询以后才能够hook,扩展性会变强,而且也是真正减少了耦合。

熟悉gtk的人可能看出来了,这就是gtk的signal。我觉得,这种设计才是比较低耦合的。

优势:低耦合,signal做好了则所有的ui和logic都只需要注册-查询-hook,十分方便,减少了工作量(因为不再需要一个超大的switch了)
劣势:signal可能需要一定的代码量。它本身需要一个类型系统作为基础(gtk使用自己的GType和不定参数解决的这个问题),还有最致命的:它有比较严重的效率问题(使用字符串注册event,虽然可以使用GQuark技术加速,么但是速度仍然比不上直接用int。为什么不用int呢?因为既然是注册-查询,那么真正的事件号就是可变的,因此没办法直接将事件号写死进程序里面)。  回复  更多评论   

# re: 低耦合模块间的通信组件:两个模板 2010-07-17 11:51 li song

这种方法主要是通过组合出一种函数类型,来实现偏特化。

是模板特化。。。

哥子来看你了,老哥们  回复  更多评论   

# re: 低耦合模块间的通信组件:两个模板 2013-03-02 18:23 yzm

哥们你太牛了啊~!  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理