Focus on ACE

订阅 ace-china
电子邮件:
浏览存于 groups.google.com 上的所有帖子

C++博客 首页 新随笔 联系 聚合 管理
  64 Posts :: 3 Stories :: 22 Comments :: 0 Trackbacks

避免依赖的消息处理方式

Anthony Williams
url: http://www.ddj.com/dept/cpp/184429055
译者: Stone Jiang
译者说明:本人还在学习英文的过程中,有些句子很难译,这里给出原文的链接,欢迎就其中译得不准确的地方与我交换意见。

在您维护安全类型和避免集成电路般函数时,你可以使用C++的强大的力量进行消息传递。

Anthony是Just Software Solution有限公司的一位软件开发者和执行管理者。可以通过anthony@justsoftwaresolutions.co.uk与之联系。

使用通用的消息传递方式传递数据在C++程序中很普遍。这种技术经常用于在线程间以及从/到GUI组件间传递数据。但是消息传递仍然很难实现得良好,这是因为在常见的消息传递方式中,暴露出了过多的藕合、缺少类型安全和集成电路般的消息处理函数。

在本文中,我提出了一种技术,这种技术利用C++的强大力量来避免上述缺陷——在消息传递中避免不适当的藕合,维护类型安全,以及消除集成电路般的消息处理函。( The only translation units that need to known the details of a message are those containning the source and handler functions for that specific message type.) 需要转换的单元,即需要知道的消息详细内容是包含了特定消息的类型的源代码和处理函数。

传统技术


大概应用得最为广泛的消息传递技术是使用一个带有特殊成员来表示消息类型的结构体,该消息类型是消息的标识。这种方式被广泛应用归咎于使用了基于C的API,比如X11和Microsoft Windows。在这种方法中,消息结构体中要么有一个通用的字体用于区别不同消息的意义,这个字段可被所有消息重用,或者它是更大结构的第一个成员,它的类型由类型代码来确定。Windows API使用前面的技术,而X11使用后面的方法。无论用哪种方式,处理消息的代码都须检查类型编码,用以决定怎么处理该消息。

这些技术的问题是:缺乏类型安全,集成电路般的处理函数,需要管理类型编码来确保消息唯一性的适当层次。特别的,缺乏类型安全意味着使用之前,使用代码必须把消息数据转换成适当的类型。这一步是极易出错的,尤其在当复制和粘贴代码时(这种非常的手段常发生在为处理相似消息编写代码的时候),编译器不会在这种错误给出任何警告。

缺乏类型安全还有一个额外的问题——即它不可能简单有效的通过消息系统传递资源或变长的数据, 这是因为消息的发送方总是不能知道何时(或是否)该消息已被处理过了。

在这部分,集成电路般的消息处理函数是必须用于确定消息类型的产物,通过已接收的消息来消息类型,然后得到如何处理它的方式。这种处理函数往往实现为一个很大的switch语句或是一串if eles if。一些框架,如MFC,提供一些宏来减弱这种问题的影响,它这不能完全消除这个问题。

最后的问题是管理类型代码。它必须要求接收消息代码清楚地知道是哪一个消息,以便于正确的处理它。所以,类型代码需要在处理它的相关代码中确保唯一性。比如,在Windows API中,指定范围的消息类型在不同的应用程序中代表不同的意义,并且,在同一个应就用程序中,其它范围的消息类型在不同窗口或GUI组件中代表不同的意义。 通常,需要所有类型代码的列表,该列表要求在给定的范围中保持唯一,以便于检查它们的唯一性。列表常常是以头文件的形式给出,头文件中定义了类型代码,包含在需要知道消息类型的所有地方。这种方式容易导致应用程序不同部分之间的藕合,而这些部分之间却没有任何关系。由于这种过度的藕,简单的变更导致过多的重新编译。

面向对象技术

对象技术的一个常见特征是所有相关消息类派生自一个通用的基类。该特征用编译器能认识的真实类型代替了显式的类型代码。不仅如此,它还有了一个重要的,超越C风格技术的优点——类型安全。它提供的通用基类的析构函数是虚函数,所以派生的消息类能自由地管理资源,如变长的数据,这些数据可以在析构函数中释放。仅有的需求是接受消息的代码能正确地销毁消息对象,无论它们是否被处理。

管理类型代码现在被替换为管理类。这是一个更加简单的任务,由于可能的消息名字的范围是没有限制的,可能存在名字冲突,但这一点可以通过名字空间来解决。

保持简单

最简单的OOP技术就是用dynamic_cast检查实际的消息类型代替检查消息编码。然而,这依然面临着集成电路般地消息处理方式——现在通过包括dynamic_cast的比较链也优于通过类型编码字段比较链。如列表1:

void  handleMessage(Message *  message)
{
    
if (Message1 *  m = dynamic_cast < Message1 *> (message))
    
{
        handleMessage1(m);
    }

    
else   if (Message2 *  m = dynamic_cast < Message2 *> (message))
    
{
        handleMessage2(m);
    }

    
//  
}

[列表1]

一般而言,由于仅仅是消息的源代码和接受消息的源代码需求知道相关的消息,所以依赖得到降低。然后,集成电路般地处理函数现在需要知道消息的有关细节,所以dynamic_cast需要消息的完整定义——如果分派给另外的函数处理实际的消息,C风格技术的处理函数不需求知道消息的细节。

双重分派

(Direct testing of a class's type using dynamic_cast is generally indicative of a design problem;)类的类型用dynamic_cast的直测试一般可表示为设计问题;然而,简单地把虚函数放在消息类中起不到任何作用——它将把消息处理与消息缠绕在一起,这个消息使在第一个地方发送消息的目的失败。

双重分派的关键点是,在消息类中的虚函数带有一个作为参数的处理器,然后在处理器上把自已作为参数传递传递给另一个函数并完成调用。因为这里的第二次到处理器的回调已经在实际的派生类中完成,所以真实的消息类型已经知道,在处理器上能调用适当的函数,无论这个函数是通过重载的方式实现还是另外独立命名的函数来实现(列表2)。

class  Message
{
public :
    
virtual   void  dispatch(MessageHandler *  handler) = 0 ;
};
class  Message1:
    
public  Message
{
    
void  dispatch(MessageHandler *  handler)
    {
        handler
-> process( this );
    }
};
class  Message2:
    
public  Message
{
    
void  dispatch(MessageHandler *  handler)
    {
        handler
-> process( this );
    }
};
//  other message classes
class  MessageHandler
{
    
void  process(Message1 * );
    
void  process(Message2 * );
    
//  overloads of process for other messages
};

[列表2]

依赖于重载的方式来区别不同的消息有利于大多数平衡——现在在每个消息类中虚函数的实现方式是相同的,如果需要,可以通过宏来一致地包装,或通过从一个消息到另一个消息中直接复制,不会有出错的机会。

双重分派存在一个缺点——高度藕合。由于通过重载方式在处理器类中的选择处理函数,在消息类中虚函数的实现需要知道处理器类的定义的全部,因此必须注意到在系统中每个其它的类的名字。不光这些,如果要支持不同的处理器类,处理函数必须在通用的处理器的基类中声明为虚函数,所以每个处理器类必须在系统中注意到所有的消息类型(列表3)。增加或删除一个消息类型会引起应用程序大部分代码重新编译。

class  MessageHandler
{
    
virtual   void  process(Message1 * ) = 0 ;
    
virtual   void  process(Message2 * ) = 0 ;
    
virtual   void  process(Message3 * ) = 0 ;
    
virtual   void  process(Message4 * ) = 0 ;
    
//  overloads of process for other messages
}
;
class  SpecificMessageHandler:
    
public  MessageHandler
{
    
void  process(Message1 * );
    
void  process(Message2 * );
    
void  process(Message3 * );
    
void  process(Message4 * );
    
//  overloads of process for other messages
}
;
class  OtherSpecificMessageHandler:
    
public  MessageHandler
{
    
void  process(Message1 * );
    
void  process(Message2 * );
    
void  process(Message3 * );
    
void  process(Message4 * );
    
//  overloads of process for other messages
}
;

[列表3]

动态双重分派

(It was against this backdrop that I developed the technique I call "Dynamic Double Dispatch.")我开发了一种技术,我称其为“动态双重分派”,这种技术用于解决上述问题。尽管有基本的双重分派技术,但选择的消息处理函数使用的是在编译阶段确定的重载技术(尽管发现在正确的消息处理器类中的实现是使用虚函数机制),而动态双重分派是在运行时检查在处理器上适当的处理函数的。结论是动态双重分派消除了双重分派的依赖问题。消息类型不在需要注意到其它的消息类型,并且处理器类仅需要注意到它的它要处理的消息。

动态检查的关键点是:每一个消息类型有一个独立的基类——处理器类从适当的,设计为处理消息的基类派生。然后在每个消息类中的分派函数能用dynamic_cast来检查从正派基类派生的处理器类,因而实现了正确的处理函数。(列表4)

class  MessageHandlerBase
{};
class  Message1HandlerBase:
    
public   virtual  MessageHandlerBase
{
    
virtual   void  process(Message1 * ) = 0 ;
};
class  Message1
{
    
void  dispatch(MessageHandlerBase *  handler)
    {
        dynamic_cast
< Message1HandlerBase &> ( * handler).process( this );
    }
};
class  Message2HandlerBase:
    
public   virtual  MessageHandlerBase
{
    
virtual   void  process(Message2 * ) = 0 ;
};
class  Message2:
    
public  MessageBase
{
    
void  dispatch(MessageHandlerBase *  handler)
    {
        dynamic_cast
< Message2HandlerBase &> ( * handler).process( this );
    }
};
//  
class  SpecificMessageHandler:
    
public  Message1HandlerBase,
    
public  Message2HandlerBase
{
    
void  process(Message1 * );
    
void  process(Message2 * );
};
class  OtherSpecificMessageHandler:
    
public  Message3HandlerBase,
    
public  Message4HandlerBase
{
    
void  process(Message3 * );
    
void  process(Message4 * );
};

[列表4]

(Of course, having a completely separate handler base class for each message type would add excessive complication, as the dispatch function for each message type would now be specific to that message type, and the base classes would have to be written separately, despite being fundamentally the same, except for the message type they referenced.)
诚然,为每个消息类型分别编写的处理器基类将增加过多的复杂性,同样地,每个消息类型各自的分派函数现在需要特别指定,基类也需求分别编写,然后除了它们引用的消息类型外基础是相同的。消除这种重复的关键是使基类成为模板,用消息类型作为模板参数——分派函数引用到模板的实现好于指定类型;请看列表5。

 

template < typename MessageType >
class  MessageHandler:
    
public   virtual  MessageHandlerBase
{
    
virtual   void  process(MessageType * ) = 0 ;
};
class  Message1
{
    
void  dispatch(MessageHandlerBase *  handler)
    {
        dynamic_cast
< MessageHandler < Message1 >&> ( * handler).process( this );
    }
};
class  SpecificMessageHandler:
    
public  MessageHandler < Message1 > ,
    
public  MessageHandler < Message2 >
{
    
void  process(Message1 * );
    
void  process(Message2 * );
};

[列表5]
出于简化原因,在消息类中的分派函数几乎相同,但也不是完全相同——它们必须明确的指定属于它们的指定消息类,以便于转换为适当的处理器基类。像软件中许多事情一样,这个问题可以增加一个额外的层来解决——分派函数可以委托给单个模板函数,这个模板函数使用模板参数类型来确定消息类型和把处理器转换到适当的类型上。(列表6)

 

class  Message
{
protected :
    template
< typename MessageType >
    
void  dynamicDispatch(MessageHandlerBase *  handler,MessageType *  self)
    {
        dynamic_cast
< MessageHandler < MessageType >&> ( * handler).process(self);
    }
};
class  Message1:
    
public  MessageBase
{
    
void  dispatch(MessageHandlerBase *  handler)
    {
        dynamicDispatch(handler,
this );
    }
};

[列表6]

通过进一步抽象在消息对象中分派函数的不同之处,我们把工作集中到一个地方——模板函数的定义;它提供了为修改行为的单一点。在消息类中剩下的分派函数都是相同的,这足以把它们简化到隐藏细节的宏中或在消息类之间中逐字复制。



未处理的消息

迄今为止,我们展示的 dynamicDispach模板函数的代码假定处理的类是从适当的SpecificMessageHandler是派生的;如是不是这样, dynamic_cast将抛出std::bad_cast异常。有时这就足够了,但是有的时候,有更适当的行为——也许更好的做法是抛弃消息,这不能被接受消息的代理处理或调用catch-all处理器。举例来说,dynamicDispatch 函数能被调整,用基于指针的转换代替基于引用的转换,所以结果值可以与NULL进行测试。


缺点(Trade-Off)在哪里?
有如此多的优点,一定存在它的缺点,那它的缺点在哪里呢?在这里,有两个缺点。第一个是:额外的动态转换,两个虚函数调用会影响性能。如果性能上是一个问题,这就是一个疑问,但是,在很多情况下,花销在这里的额外的时间是不值得关注的。可以使用相应的工具来签定到底哪里才是真正的性能瓶颈所在。

第二个缺点是:需要为每个消息处理从指定的基类派生消息处理器。因为处理新的消息类型需要修改两个地方——适当的基类列表入口和处理函数,所以这可能成为错误的来源,遗失处理函数容易被发现,因为这是全局点,但是遗失基类在代码运行时只产生不易查觉的缺陷。因为没有处理函数的时候仅仅是不调用它。这些错误在单元测试的时候是很容易被抓出来的,所以所实话,这些不便之处都成不了大问题。

 

posted on 2006-05-04 20:52 Stone Jiang 阅读(1212) 评论(0)  编辑 收藏 引用 所属分类: C++&OOPMiscellaneous

只有注册用户登录后才能发表评论。
【推荐】超50万行VC++源码: 大型组态工控、电力仿真CAD与GIS源码库
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理