[譯者按] 本文根據(jù)發(fā)表在CUJ Expert Forum上的兩篇文章編譯而成。C/C++ User's Journal是目前最出色的C/C++語(yǔ)言專(zhuān)業(yè)雜志,特別是在C++ Report閉刊之後,CUJ的地位更加突出。CUJ Expert Forum是CUJ主辦的網(wǎng)上技術(shù)專(zhuān)欄,匯集2000年10月以來(lái)C++社群中頂尖專(zhuān)家的技術(shù)短文,并免費(fèi)公開(kāi)發(fā)布,精彩紛呈,是每一個(gè)C/C++學(xué)習(xí)者不可錯(cuò)過(guò)的資料。由Jim Hyslop和Herb Sutter主持的Conversation系列,是CUJ Expert Forum每期必備的精品專(zhuān)欄,以風(fēng)趣幽默的對(duì)話形式講解C++高級(jí)技術(shù),在C++社群內(nèi)得到廣泛贊譽(yù)。譯者特別挑選兩篇設(shè)計(jì)模式方面的文章,介紹給大家。設(shè)計(jì)模式方面的經(jīng)典著作是GoF的Design Patterns。但是那本書(shū)有一個(gè)缺點(diǎn),不好懂。從風(fēng)格上講,該書(shū)與其說(shuō)是為學(xué)習(xí)者而寫(xiě)作的教程范本,還不如說(shuō)是給學(xué)術(shù)界人士看的學(xué)術(shù)報(bào)告,嚴(yán)謹(jǐn)有馀,生動(dòng)不足。這一點(diǎn)包括該書(shū)作者和象Bjarne Stroustrup這樣的大師都從不諱言。實(shí)際上Design Pattern并非一定是晦澀難懂的,通過(guò)生動(dòng)的例子,一個(gè)中等水平的C++學(xué)習(xí)者完全可以掌握基本用法,在自己的編程實(shí)踐中使用,得到立竿見(jiàn)影的功效。這兩篇文章就是很好的例證。本文翻譯在保證技術(shù)完整性的前提下作了不少刪節(jié)和修改,以便使文章顯得更緊湊。
----------------------------------------------------------
人物介紹:
我 --- 一個(gè)追求上進(jìn)的C++程序員,尚在試用期,聰明但是經(jīng)驗(yàn)不足。
Wendy --- 公司里的技術(shù)大拿,就坐在我旁邊的隔間里,C++大蝦,最了不起的是,她是個(gè)女的 她什麼都好,就是有點(diǎn)刻薄,
我對(duì)她真是又崇拜又嫉妒。
----------------------------------------------------------
I. Virtually Yours?-- Template Method模式
我在研究Wendy寫(xiě)的一個(gè)類(lèi)。那是她為這個(gè)項(xiàng)目寫(xiě)的一個(gè)抽象基類(lèi),而我的工作就是從中派生出一個(gè)具象類(lèi)(concrete class)。這個(gè)類(lèi)的public部份是這樣的:
class Mountie {
public:
void read( std::istream & );
void write( std::ostream & ) const;
virtual ~Mountie();
很正常,virtual destructor表明這個(gè)類(lèi)打算被繼承。那麼再看看其protected部份:
protected:
virtual void do_read( std::istream & );
virtual void do_write( std::ostream & ) const;
也不過(guò)就是一會(huì)兒的功夫,我識(shí)破了Wendy的把戲:她在使用template method模式。public成員函數(shù)read和write是非虛擬的,它們肯定是調(diào)用protected部份do_read/do_write虛擬成員函數(shù)來(lái)完成實(shí)際的工作。啊,我簡(jiǎn)直為自己的進(jìn)步而飄飄然了 哈,Wendy,這回你可難不住我,還有什麼招數(shù)?盡管放馬過(guò)來(lái)... 突然,笑容在我臉上凝固,因?yàn)槲铱吹搅似鋚rivate部份:
private:
virtual std::string classID() const = 0;
這是什麼?一個(gè)private純序函數(shù),能工作麼?我站了起來(lái),
“Wendy,你的Mountie類(lèi)好像不能工作耶,它有一個(gè)private virtual function。”
“你試過(guò)了?”她連頭都不抬。
“嗯,那倒是沒(méi)有啦,可是想想也不行???我的派生類(lèi)怎麼能override你的private函數(shù)呢?” 我嘟囔 。
“ ,你倒是很確定啊 ”Wendy的聲音很輕柔,“你怎麼老是這也不行,那也不行的,這幾個(gè)月跟 我你就沒(méi)學(xué)到什麼東西嗎?小菜鳥(niǎo)。”
真是可惡啊...
“小菜鳥(niǎo),你全都忘了,訪問(wèn)控制級(jí)別跟一個(gè)函數(shù)是不是虛擬的根本沒(méi)關(guān)系。判斷一個(gè)函數(shù)是動(dòng)態(tài)綁定還是靜態(tài)綁定是函數(shù)調(diào)用解析的最後一個(gè)步驟。好好讀讀標(biāo)準(zhǔn)的3.4和5.2.2節(jié)吧。”
我完全處?kù)断嘛L(fēng),只好采取干擾戰(zhàn)術(shù)?!昂冒?,就算你說(shuō)的不錯(cuò),我也還是不明白,何必把它設(shè)為private?”
“我且問(wèn)你,倘若你不想讓一個(gè)類(lèi)中的成員函數(shù)被其他的類(lèi)調(diào)用,應(yīng)當(dāng)如何處理?”
“當(dāng)然是把它設(shè)置為private的,” 我回答道。
“那麼你去看看我的Mountie類(lèi)實(shí)現(xiàn),特別是write()函數(shù)的實(shí)現(xiàn)?!?/P>
我正巴不得逃開(kāi)Wendy那刺人的目光,便轉(zhuǎn)過(guò)頭去在我的屏幕上搜索,很快,我找到了:
void Mountie::write(std::ostream &Dudley) const
{
Dudley << classID() << std::endl;
do_write(Dudley);
}
嗨,最近卡通片真是看得太多了,居然犯這樣的低級(jí)失誤。還是老是承認(rèn)吧:“好了,我明白了。classID()是一個(gè)實(shí)現(xiàn)細(xì)節(jié),用來(lái)在保存對(duì)象時(shí)指示具象類(lèi)的類(lèi)型,派生類(lèi)必須覆蓋它,所以必須是純虛的。但是既然是實(shí)現(xiàn)細(xì)節(jié),就應(yīng)該設(shè)為private的?!?/P>
“這還差不多,小菜鳥(niǎo)?!贝笪r點(diǎn)了點(diǎn)頭,“現(xiàn)在給我解釋一下為什麼do_read()和do_write()是protected的?”
這個(gè)問(wèn)題并不難,我組織了一下就回答:“因?yàn)榕缮?lèi)對(duì)象需要調(diào)用這兩個(gè)函數(shù)的實(shí)現(xiàn)來(lái)讀寫(xiě)其中的基類(lèi)對(duì)象?!?/P>
“很好很好,”大蝦差不多滿意了,“不過(guò),你再解釋解釋為什麼我不把它們?cè)O(shè)為public的?”
現(xiàn)在我感覺(jué)好多了:“因?yàn)檎{(diào)用它們的時(shí)候必須以一種特定的方式進(jìn)行。比如do_write()函數(shù),必須先把類(lèi)型信息寫(xiě)入,再把對(duì)象信息寫(xiě)入,這樣讀取的時(shí)候,負(fù)責(zé)生成對(duì)象的模塊首先能夠知道要讀出來(lái)的對(duì)象是什麼類(lèi)型的,然後才能正確地從流中讀取對(duì)象信息。”
“聰明啊,我的小菜鳥(niǎo) ”Wendy停頓了一下,“就跟學(xué)習(xí)外國(guó)口語(yǔ)一樣,學(xué)習(xí)C++也不光是掌握語(yǔ)法而已,還必須要掌握大量的慣用法。”
“是啊是啊,我正打算讀Coplien的書(shū)...”
[譯者注:就是James Coplien 1992年的經(jīng)典著作Advanced C++ Programming Style and Idioms]
大蝦揮了揮她的手,“冷靜,小菜鳥(niǎo),我不是指先知Coplien的那本書(shū),我是指某種結(jié)構(gòu)背後隱含的慣用法。比如一個(gè)類(lèi)有virtual destructor,相當(dāng)于告訴你說(shuō):‘嗨,我是一個(gè)多態(tài)基類(lèi),來(lái)繼承我吧 ' 而如果一個(gè)類(lèi)的destructor不是虛擬的,則相當(dāng)於是在說(shuō):‘我不能作為多態(tài)基類(lèi),看在老天的份上,別繼承我。'”
“同樣的,virtual函數(shù)的訪問(wèn)控制級(jí)別也具有隱含的意義。一個(gè)protected virtual function告訴你:‘你寫(xiě)的派生類(lèi)應(yīng)該,哦,可是說(shuō)是必須調(diào)用我的實(shí)現(xiàn)。'而一個(gè)private virtual function是在說(shuō):‘派生類(lèi)可以覆蓋,也可以不覆蓋我,隨你的便。但是你不可以調(diào)用我的實(shí)現(xiàn)。'”
我點(diǎn)點(diǎn)頭,告訴她我懂了,然後追問(wèn)道:“那麼public virtual function呢?”
“盡可能不要使用public virtual function。”她拿起一支筆寫(xiě)下了以下代碼:
class HardToExtend
{
public:
virtual void f();
};
void HardToExtend::f()
{
// Perform a specific action
}
“假設(shè)你發(fā)布了這個(gè)類(lèi)。在寫(xiě)第二版時(shí),需求有所變化,你必須改用Template Method??墒沁@根本不可能,你知道為什麼?”
“呃,這個(gè)...,不知道?!?/P>
“由兩種可能的辦法。其一,將f()的實(shí)現(xiàn)代碼轉(zhuǎn)移到一個(gè)新的函數(shù)中,然後將f()本身設(shè)為non-virtual的:
class HardToExtend
{
// possibly protected
virtual void do_f();
public:
void f();
};
void HardToExtend::f()
{
// pre-processing
do_f();
// post-processing
}
void HardToExtend::do_f()
{
// Perform a specific action
}
然而你原來(lái)寫(xiě)的派生類(lèi)都是企圖override函數(shù)f()而不是do_f()的,你必須改變所有的派生類(lèi)實(shí)現(xiàn),只要你錯(cuò)過(guò)了一個(gè)類(lèi),你的類(lèi)層次就會(huì)染上先知Meyers所說(shuō)的‘精神分裂的行徑'。”[譯者注:叁見(jiàn)Scott Meyers,Effective C++, Item 37,絕對(duì)不要重新定義繼承而來(lái)的非虛擬函數(shù)]
“另一種辦法是將f()移到private區(qū)域,引入一個(gè)新的non-virtual函數(shù):”
class HardToExtend
{
// possibly protected
virtual void f();
public:
void call_f();
};
“這會(huì)導(dǎo)致無(wú)數(shù)令人頭痛的問(wèn)題。首先,所有的客戶(hù)都企圖調(diào)用f()而不是call_f(),現(xiàn)在它們的代碼都不能編譯了。更有甚者,大部份派生類(lèi)都回把f()放在public區(qū)域中,這樣直接使用派生類(lèi)的用戶(hù)可以訪問(wèn)到你本來(lái)想保護(hù)的細(xì)節(jié)?!?/P>
“對(duì)待虛函數(shù)要象對(duì)待數(shù)據(jù)成員一樣,把它們?cè)O(shè)為private的,直到設(shè)計(jì)上要求使用更寬松的訪問(wèn)控制再來(lái)調(diào)整。要知道由private入public易,由public入private難啊 ”
[譯者注:這篇文章所表達(dá)的思想具有一定的顛覆性,因?yàn)槲覀兲菀自诨?lèi)中設(shè)置public virtual function了,Java中甚至專(zhuān)門(mén)為這種做法建立了interface機(jī)制,現(xiàn)在竟然說(shuō)這不好 一時(shí)間真是接受不了。但是仔細(xì)體會(huì)作者的意思,他并不是一般地反對(duì)public virtual function,只是在template method大背景下給出上述原則。雖然這個(gè)原則在一般的設(shè)計(jì)中也是值得考慮的,但是主要的應(yīng)用領(lǐng)域還是在template method模式中。當(dāng)然,template method是一種非常有用和常用的模式,因此也決定了本文提出的原則具有廣泛的意義。]
----------------------------------------------------------------
II. Visitor模式
我正在為一個(gè)設(shè)計(jì)問(wèn)題苦惱。試用期快結(jié)束了,我希望自己解決這個(gè)問(wèn)題,來(lái)證明自己的進(jìn)步。每個(gè)人都記得自己的第一份工作吧,也都應(yīng)該知道在這個(gè)時(shí)候把活兒做好是多麼的重要 我親眼看到其他的新雇員沒(méi)有過(guò)完試用期就被炒了魷魚(yú),就是因?yàn)樗麄儾欢萌绾螌?duì)付那個(gè)大蝦...,別誤會(huì),我不是說(shuō)她不好,她是我見(jiàn)過(guò)最棒的程序員,可就是有點(diǎn)刻薄古怪...?,F(xiàn)在我拜她為師,不為別的,就是因?yàn)槲沂窒M?FONT color=#8080ff>達(dá)到她那個(gè)高度。
我想在一個(gè)類(lèi)層次(class hierarchy)中增加一個(gè)新的虛函數(shù),但是這個(gè)類(lèi)層次是由另外一幫人維護(hù)的,其他人碰都不能碰:
class Personnel
{
public:
virtual void Pay ( /*...*/ ) = 0;
virtual void Promote( /*...*/ ) = 0;
virtual void Accept ( PersonnelV& ) = 0;
// ... other functions ...
};
class Officer : public Personnel { /* override virtuals */ };
class Captain : public Officer { /* override virtuals */ };
class First : public Officer { /* override virtuals */ };
我想要一個(gè)函數(shù),如果對(duì)象是船長(zhǎng)(Captain)就這麼做,如果是大副(First Officer)就那麼做。Virtual function正是解決之道,在Personnel或者Officer中聲明它,而在Captain和First覆蓋(override)它。
糟糕的是,我不能增加這麼一個(gè)虛函數(shù)。我知道可以用RTTI給出一個(gè)解決方案:
void f( Officer &o )
{
if( dynamic_cast<Captain*>(&o) )
/* do one thing */
else if( dynamic_cast<First*>(&o) )
/* do another thing */
}
int main()
{
Captain k;
First s;
f( k );
f( s );
}
但是我知道使用RTTI是公司編碼標(biāo)準(zhǔn)所排斥的行為,我對(duì)自己說(shuō):“是的,雖然我以前不喜歡RTTI,但是這回我得改變對(duì)它的看法了。很顯然,除了使用RTTI,別無(wú)它法?!?/P>
“任何問(wèn)題都可以通過(guò)增加間接層次的方法解決?!?/STRONG>
我噌地一下跳起來(lái),那是大蝦的聲音,她不知道什麼時(shí)候跑到我背後,“啊喲,您嚇了我一跳...您剛才說(shuō)什麼?”
“任何問(wèn)...”
“是的,我聽(tīng)清楚了,”我也不知道哪來(lái)的勇氣,居然敢打斷她,“我只是不知道您從哪冒出來(lái)的?!逼鋵?shí)這話只不過(guò)是掩飾我內(nèi)心的慌張。
“哈,算了吧,小菜鳥(niǎo),”大蝦斜 眼看 我,“你以為我不知道你心里想什麼 ”她把聲音提高了八度,直盯 我,“那些可憐的C語(yǔ)言門(mén)徒才會(huì)使用switch語(yǔ)句處理不同的對(duì)象類(lèi)型。你看:”
/* A not-atypical C program */
void f(struct someStruct *s)
{
switch(s->type) {
case APPLE:
/* do one thing */
break;
case ORANGE:
/* do another thing */
break;
/* ... etc. ... */
}
}
“這些人學(xué)習(xí)Stroustrup教主的C++語(yǔ)言時(shí),最重要的事情就是學(xué)習(xí)如何設(shè)計(jì)好的類(lèi)層次。”
“沒(méi)錯(cuò),”我又一次打斷她,迫不及待地想讓W(xué)endy明白,我還是有兩下子的,“他們應(yīng)該設(shè)計(jì)一個(gè)Fruit基類(lèi),派生出Apple和Orange,用virtual function來(lái)作具體的事情。
“很好,小菜鳥(niǎo)。C語(yǔ)言門(mén)徒通常老習(xí)慣改不掉。但是,你應(yīng)該知道,通過(guò)使用virtual function,你增加了一個(gè)間接層次?!彼畔鹿P,“你所需要的不就是一個(gè)新的虛函數(shù)嗎?”
“是的。可是我沒(méi)有權(quán)力這麼干?!?/P>
“因?yàn)槟銦o(wú)權(quán)修改類(lèi)層次,對(duì)吧 ”
“您終於了解了情況,我們沒(méi)法動(dòng)它。也不知道這個(gè)該死的類(lèi)層次是哪個(gè)家伙設(shè)計(jì)的...” 我嘀嘀咕咕 。
“是我設(shè)計(jì)的?!?/P>
“啊...,真的? 這個(gè),嘿嘿...”,我極為尷尬。
“這個(gè)類(lèi)層次必須非常穩(wěn)定,因?yàn)橛锌缙脚_(tái)的問(wèn)題。但是它的設(shè)計(jì)允許你增加新的virtual function,而不必?zé)﹦赗TTI。你可以通過(guò)增加一個(gè)間接層次的辦法解決這個(gè)問(wèn)題。請(qǐng)問(wèn),Personnel::Accept是什麼?”
”嗯,這個(gè)...”
“這個(gè)類(lèi)實(shí)現(xiàn)了一個(gè)模式,可惜這個(gè)模式的名字起得不太好,是個(gè)PNP,叫Visitor模式。”
[譯者注:PNP,Poor-Named Pattern, 沒(méi)起好名字的模式]
“啊,我剛剛讀過(guò)Visitor模式。但是那只不過(guò)是允許若干對(duì)象之間相互迭代訪問(wèn)的模式,不是嗎?”
她嘆了一口氣,“這是流行的錯(cuò)誤理解。那個(gè)V,我覺(jué)得毋寧說(shuō)是Visitor,還不如說(shuō)是Virtual更好。這個(gè)PNP最重要的用途是允許在不改變類(lèi)層次的前提下,向已經(jīng)存在的類(lèi)層次中增加新的虛函數(shù)。首先來(lái)看看Personnel及其派生類(lèi)的Accept實(shí)現(xiàn)細(xì)節(jié)?!彼闷鸸P寫(xiě)下:
void Personnel::Accept( PersonnelV& v )
{ v.Visit( *this ); }
void Officer::Accept ( PersonnelV& v )
{ v.Visit( *this ); }
void Captain::Accept ( PersonnelV& v )
{ v.Visit( *this ); }
void First::Accept ( PersonnelV& v )
{ v.Visit( *this ); }
“Visitor的基類(lèi)如下:”
class PersonnelV/*isitor*/
{
public:
virtual void Visit( Personnel& ) = 0;
virtual void Visit( Officer& ) = 0;
virtual void Visit( Captain& ) = 0;
virtual void Visit( First& ) = 0;
};
“啊,我記起來(lái)了。當(dāng)我要利用Personnel類(lèi)層次的多態(tài)性時(shí),我只要調(diào)用Personnel::Accept(myVisitorObject)。由於Accept是虛函數(shù),我的myVisitorObject.Visit()會(huì)針對(duì)正確的對(duì)象類(lèi)型調(diào)用,根據(jù)重載法則,編譯器會(huì)挑選最貼切的那個(gè)Visit來(lái)調(diào)用。這不相當(dāng)于增加了一個(gè)新的虛擬函數(shù)了嗎?”
“沒(méi)錯(cuò),小菜鳥(niǎo)。只要類(lèi)層次支持Accept,我們就可以在不改動(dòng)類(lèi)層次的情況下增加新的虛函數(shù)了?!?/P>
“好了,我現(xiàn)在知道該怎麼辦了”,我寫(xiě)道:
class DoSomething : public PersonnelV
{
public:
virtual void Visit( Personnel& );
virtual void Visit( Officer& );
virtual void Visit( Captain& );
virtual void Visit( First& );
};
void DoSomething::Visit( Captain& c )
{
if( femaleGuestStarIsPresent )
c.TurnOnCharm();
else
c.StartFight();
}
void DoSomething::Visit( First& f )
{
f.RaiseEyebrowAtCaptainsBehavior();
}
void f( Personnel& p )
{
p.Accept( DoSomething() ); // 相當(dāng)于 p.DoSomething()
}
int main()
{
Captain k;
First s;
f( k );
f( s );
}
大蝦滿意地笑了,“也許這個(gè)模式換一個(gè)名字會(huì)更好理解,可惜世事往往不遂人意...”。
[譯者注:這篇文章我作了一定的刪節(jié),原文中有稍微多一些的論述,而且提供了兩篇技術(shù)文章的link。]
from: http://jjhou.csdn.net/myan-design-patterns-big5.htm