避免依賴的消息處理方式
Anthony Williams
url: http://www.ddj.com/dept/cpp/184429055
譯者: Stone Jiang
譯者說明:本人還在學(xué)習(xí)英文的過程中,有些句子很難譯,這里給出原文的鏈接,歡迎就其中譯得不準(zhǔn)確的地方與我交換意見。
在您維護(hù)安全類型和避免集成電路般函數(shù)時(shí),你可以使用C++的強(qiáng)大的力量進(jìn)行消息傳遞。
Anthony是Just Software Solution有限公司的一位軟件開發(fā)者和執(zhí)行管理者。可以通過anthony@justsoftwaresolutions.co.uk與之聯(lián)系。
使用通用的消息傳遞方式傳遞數(shù)據(jù)在C++程序中很普遍。這種技術(shù)經(jīng)常用于在線程間以及從/到GUI組件間傳遞數(shù)據(jù)。但是消息傳遞仍然很難實(shí)現(xiàn)得良好,這是因?yàn)樵诔R姷南鬟f方式中,暴露出了過多的藕合、缺少類型安全和集成電路般的消息處理函數(shù)。
在本文中,我提出了一種技術(shù),這種技術(shù)利用C++的強(qiáng)大力量來避免上述缺陷——在消息傳遞中避免不適當(dāng)?shù)呐汉希S護(hù)類型安全,以及消除集成電路般的消息處理函。( 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.) 需要轉(zhuǎn)換的單元,即需要知道的消息詳細(xì)內(nèi)容是包含了特定消息的類型的源代碼和處理函數(shù)。
傳統(tǒng)技術(shù)
大概應(yīng)用得最為廣泛的消息傳遞技術(shù)是使用一個(gè)帶有特殊成員來表示消息類型的結(jié)構(gòu)體,該消息類型是消息的標(biāo)識(shí)。這種方式被廣泛應(yīng)用歸咎于使用了基于C的API,比如X11和Microsoft Windows。在這種方法中,消息結(jié)構(gòu)體中要么有一個(gè)通用的字體用于區(qū)別不同消息的意義,這個(gè)字段可被所有消息重用,或者它是更大結(jié)構(gòu)的第一個(gè)成員,它的類型由類型代碼來確定。Windows API使用前面的技術(shù),而X11使用后面的方法。無論用哪種方式,處理消息的代碼都須檢查類型編碼,用以決定怎么處理該消息。
這些技術(shù)的問題是:缺乏類型安全,集成電路般的處理函數(shù),需要管理類型編碼來確保消息唯一性的適當(dāng)層次。特別的,缺乏類型安全意味著使用之前,使用代碼必須把消息數(shù)據(jù)轉(zhuǎn)換成適當(dāng)?shù)念愋汀_@一步是極易出錯(cuò)的,尤其在當(dāng)復(fù)制和粘貼代碼時(shí)(這種非常的手段常發(fā)生在為處理相似消息編寫代碼的時(shí)候),編譯器不會(huì)在這種錯(cuò)誤給出任何警告。
缺乏類型安全還有一個(gè)額外的問題——即它不可能簡(jiǎn)單有效的通過消息系統(tǒng)傳遞資源或變長(zhǎng)的數(shù)據(jù), 這是因?yàn)橄⒌陌l(fā)送方總是不能知道何時(shí)(或是否)該消息已被處理過了。
在這部分,集成電路般的消息處理函數(shù)是必須用于確定消息類型的產(chǎn)物,通過已接收的消息來消息類型,然后得到如何處理它的方式。這種處理函數(shù)往往實(shí)現(xiàn)為一個(gè)很大的switch語句或是一串if eles if。一些框架,如MFC,提供一些宏來減弱這種問題的影響,它這不能完全消除這個(gè)問題。
最后的問題是管理類型代碼。它必須要求接收消息代碼清楚地知道是哪一個(gè)消息,以便于正確的處理它。所以,類型代碼需要在處理它的相關(guān)代碼中確保唯一性。比如,在Windows API中,指定范圍的消息類型在不同的應(yīng)用程序中代表不同的意義,并且,在同一個(gè)應(yīng)就用程序中,其它范圍的消息類型在不同窗口或GUI組件中代表不同的意義。 通常,需要所有類型代碼的列表,該列表要求在給定的范圍中保持唯一,以便于檢查它們的唯一性。列表常常是以頭文件的形式給出,頭文件中定義了類型代碼,包含在需要知道消息類型的所有地方。這種方式容易導(dǎo)致應(yīng)用程序不同部分之間的藕合,而這些部分之間卻沒有任何關(guān)系。由于這種過度的藕,簡(jiǎn)單的變更導(dǎo)致過多的重新編譯。
面向?qū)ο蠹夹g(shù)
對(duì)象技術(shù)的一個(gè)常見特征是所有相關(guān)消息類派生自一個(gè)通用的基類。該特征用編譯器能認(rèn)識(shí)的真實(shí)類型代替了顯式的類型代碼。不僅如此,它還有了一個(gè)重要的,超越C風(fēng)格技術(shù)的優(yōu)點(diǎn)——類型安全。它提供的通用基類的析構(gòu)函數(shù)是虛函數(shù),所以派生的消息類能自由地管理資源,如變長(zhǎng)的數(shù)據(jù),這些數(shù)據(jù)可以在析構(gòu)函數(shù)中釋放。僅有的需求是接受消息的代碼能正確地銷毀消息對(duì)象,無論它們是否被處理。
管理類型代碼現(xiàn)在被替換為管理類。這是一個(gè)更加簡(jiǎn)單的任務(wù),由于可能的消息名字的范圍是沒有限制的,可能存在名字沖突,但這一點(diǎn)可以通過名字空間來解決。
保持簡(jiǎn)單
最簡(jiǎn)單的OOP技術(shù)就是用dynamic_cast檢查實(shí)際的消息類型代替檢查消息編碼。然而,這依然面臨著集成電路般地消息處理方式——現(xiàn)在通過包括dynamic_cast的比較鏈也優(yōu)于通過類型編碼字段比較鏈。如列表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]
一般而言,由于僅僅是消息的源代碼和接受消息的源代碼需求知道相關(guān)的消息,所以依賴得到降低。然后,集成電路般地處理函數(shù)現(xiàn)在需要知道消息的有關(guān)細(xì)節(jié),所以dynamic_cast需要消息的完整定義——如果分派給另外的函數(shù)處理實(shí)際的消息,C風(fēng)格技術(shù)的處理函數(shù)不需求知道消息的細(xì)節(jié)。
雙重分派
(Direct testing of a class's type using dynamic_cast is generally indicative of a design problem;)類的類型用dynamic_cast的直測(cè)試一般可表示為設(shè)計(jì)問題;然而,簡(jiǎn)單地把虛函數(shù)放在消息類中起不到任何作用——它將把消息處理與消息纏繞在一起,這個(gè)消息使在第一個(gè)地方發(fā)送消息的目的失敗。
雙重分派的關(guān)鍵點(diǎn)是,在消息類中的虛函數(shù)帶有一個(gè)作為參數(shù)的處理器,然后在處理器上把自已作為參數(shù)傳遞傳遞給另一個(gè)函數(shù)并完成調(diào)用。因?yàn)檫@里的第二次到處理器的回調(diào)已經(jīng)在實(shí)際的派生類中完成,所以真實(shí)的消息類型已經(jīng)知道,在處理器上能調(diào)用適當(dāng)?shù)暮瘮?shù),無論這個(gè)函數(shù)是通過重載的方式實(shí)現(xiàn)還是另外獨(dú)立命名的函數(shù)來實(shí)現(xiàn)(列表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]
依賴于重載的方式來區(qū)別不同的消息有利于大多數(shù)平衡——現(xiàn)在在每個(gè)消息類中虛函數(shù)的實(shí)現(xiàn)方式是相同的,如果需要,可以通過宏來一致地包裝,或通過從一個(gè)消息到另一個(gè)消息中直接復(fù)制,不會(huì)有出錯(cuò)的機(jī)會(huì)。
雙重分派存在一個(gè)缺點(diǎn)——高度藕合。由于通過重載方式在處理器類中的選擇處理函數(shù),在消息類中虛函數(shù)的實(shí)現(xiàn)需要知道處理器類的定義的全部,因此必須注意到在系統(tǒng)中每個(gè)其它的類的名字。不光這些,如果要支持不同的處理器類,處理函數(shù)必須在通用的處理器的基類中聲明為虛函數(shù),所以每個(gè)處理器類必須在系統(tǒng)中注意到所有的消息類型(列表3)。增加或刪除一個(gè)消息類型會(huì)引起應(yīng)用程序大部分代碼重新編譯。
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]
動(dòng)態(tài)雙重分派
(It was against this backdrop that I developed the technique I call "Dynamic Double Dispatch.")我開發(fā)了一種技術(shù),我稱其為“動(dòng)態(tài)雙重分派”,這種技術(shù)用于解決上述問題。盡管有基本的雙重分派技術(shù),但選擇的消息處理函數(shù)使用的是在編譯階段確定的重載技術(shù)(盡管發(fā)現(xiàn)在正確的消息處理器類中的實(shí)現(xiàn)是使用虛函數(shù)機(jī)制),而動(dòng)態(tài)雙重分派是在運(yùn)行時(shí)檢查在處理器上適當(dāng)?shù)奶幚砗瘮?shù)的。結(jié)論是動(dòng)態(tài)雙重分派消除了雙重分派的依賴問題。消息類型不在需要注意到其它的消息類型,并且處理器類僅需要注意到它的它要處理的消息。
動(dòng)態(tài)檢查的關(guān)鍵點(diǎn)是:每一個(gè)消息類型有一個(gè)獨(dú)立的基類——處理器類從適當(dāng)?shù)模O(shè)計(jì)為處理消息的基類派生。然后在每個(gè)消息類中的分派函數(shù)能用dynamic_cast來檢查從正派基類派生的處理器類,因而實(shí)現(xiàn)了正確的處理函數(shù)。(列表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.)
誠然,為每個(gè)消息類型分別編寫的處理器基類將增加過多的復(fù)雜性,同樣地,每個(gè)消息類型各自的分派函數(shù)現(xiàn)在需要特別指定,基類也需求分別編寫,然后除了它們引用的消息類型外基礎(chǔ)是相同的。消除這種重復(fù)的關(guān)鍵是使基類成為模板,用消息類型作為模板參數(shù)——分派函數(shù)引用到模板的實(shí)現(xiàn)好于指定類型;請(qǐng)看列表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]
出于簡(jiǎn)化原因,在消息類中的分派函數(shù)幾乎相同,但也不是完全相同——它們必須明確的指定屬于它們的指定消息類,以便于轉(zhuǎn)換為適當(dāng)?shù)奶幚砥骰悺O褴浖性S多事情一樣,這個(gè)問題可以增加一個(gè)額外的層來解決——分派函數(shù)可以委托給單個(gè)模板函數(shù),這個(gè)模板函數(shù)使用模板參數(shù)類型來確定消息類型和把處理器轉(zhuǎn)換到適當(dāng)?shù)念愋蜕稀?列表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]
通過進(jìn)一步抽象在消息對(duì)象中分派函數(shù)的不同之處,我們把工作集中到一個(gè)地方——模板函數(shù)的定義;它提供了為修改行為的單一點(diǎn)。在消息類中剩下的分派函數(shù)都是相同的,這足以把它們簡(jiǎn)化到隱藏細(xì)節(jié)的宏中或在消息類之間中逐字復(fù)制。
未處理的消息
迄今為止,我們展示的 dynamicDispach模板函數(shù)的代碼假定處理的類是從適當(dāng)?shù)腟pecificMessageHandler是派生的;如是不是這樣, dynamic_cast將拋出std::bad_cast異常。有時(shí)這就足夠了,但是有的時(shí)候,有更適當(dāng)?shù)男袨椤苍S更好的做法是拋棄消息,這不能被接受消息的代理處理或調(diào)用catch-all處理器。舉例來說,dynamicDispatch 函數(shù)能被調(diào)整,用基于指針的轉(zhuǎn)換代替基于引用的轉(zhuǎn)換,所以結(jié)果值可以與NULL進(jìn)行測(cè)試。
缺點(diǎn)(Trade-Off)在哪里?
有如此多的優(yōu)點(diǎn),一定存在它的缺點(diǎn),那它的缺點(diǎn)在哪里呢?在這里,有兩個(gè)缺點(diǎn)。第一個(gè)是:額外的動(dòng)態(tài)轉(zhuǎn)換,兩個(gè)虛函數(shù)調(diào)用會(huì)影響性能。如果性能上是一個(gè)問題,這就是一個(gè)疑問,但是,在很多情況下,花銷在這里的額外的時(shí)間是不值得關(guān)注的。可以使用相應(yīng)的工具來簽定到底哪里才是真正的性能瓶頸所在。
第二個(gè)缺點(diǎn)是:需要為每個(gè)消息處理從指定的基類派生消息處理器。因?yàn)樘幚硇碌南㈩愋托枰薷膬蓚€(gè)地方——適當(dāng)?shù)幕惲斜砣肟诤吞幚砗瘮?shù),所以這可能成為錯(cuò)誤的來源,遺失處理函數(shù)容易被發(fā)現(xiàn),因?yàn)檫@是全局點(diǎn),但是遺失基類在代碼運(yùn)行時(shí)只產(chǎn)生不易查覺的缺陷。因?yàn)闆]有處理函數(shù)的時(shí)候僅僅是不調(diào)用它。這些錯(cuò)誤在單元測(cè)試的時(shí)候是很容易被抓出來的,所以所實(shí)話,這些不便之處都成不了大問題。
?