條款43: 明智地使用多繼承
要看是誰來說,多繼承(MI)要么被認(rèn)為是神來之筆,要么被當(dāng)成是魔鬼的造物。支持者宣揚(yáng)說,它是對真實(shí)世界問題進(jìn)行自然模型化所必需的;而批評者爭論說,它太慢,難以實(shí)現(xiàn),功能卻不比單繼承強(qiáng)大。更讓人為難的是,面向?qū)ο缶幊陶Z言領(lǐng)域在這個問題上至今仍存在分歧:C++,Eiffel和the Common LISP Object System (CLOS)提供了MI;Smalltalk,Objective C和Object Pascal沒有提供;而Java只是提供有限的支持。可憐的程序員該相信誰呢?
在相信任何事情之前,首先得弄清事實(shí)。C++中,關(guān)于MI一條不容爭辯的事實(shí)是,MI的出現(xiàn)就象打開了潘朵拉的盒子,帶來了單繼承中絕對不會存在的復(fù)雜性。其中,最基本的一條是二義性(參見條款26)。如果一個派生類從多個基類繼承了一個成員名,所有對這個名字的訪問都是二義的;你必須明確地說出你所指的是哪個成員。下面的例子取自ARM(參見條款50)中的一個專題討論:
class Lottery {
public:
virtual int draw();
...
};
class GraphicalObject {
public:
virtual int draw();
...
};
class LotterySimulation: public Lottery,
/t/t/t public GraphicalObject {
.../t/t/t // 沒有聲明draw
};
LotterySimulation *pls = new LotterySimulation;
pls->draw();/t/t // 錯誤! ---- 二義
pls->Lottery::draw();/t // 正確
pls->GraphicalObject::draw(); // 正確
這段代碼看起來很笨拙,但起碼可以工作。遺憾的是,想避免這種笨拙很難。即使其中一個被繼承的draw函數(shù)是私有成員從而不能被訪問,二義還是存在。(對此有一個很好的理由來解釋,但完整的說明在條款26中提供,所以此處不再重復(fù)。)
顯式地限制修飾成員不僅很笨拙,而且還帶來限制。當(dāng)顯式地用一個類名來限制修飾一個虛函數(shù)時,函數(shù)的行為將不再具有虛擬的特征。相反,被調(diào)用的函數(shù)只能是你所指定的那個,即使調(diào)用是作用在派生類的對象上:
class SpecialLotterySimulation: public LotterySimulation {
public:
virtual int draw();
...
};
pls = new SpecialLotterySimulation;
pls->draw();/t/t // 錯誤! ---- 還是有二義
pls->Lottery::draw();/t // 調(diào)用Lottery::draw
pls->GraphicalObject::draw(); // 調(diào)用GraphicalObject::draw
注意,在這種情況下,即使pls指向的是SpecialLotterySimulation對象,也無法(沒有 "向下轉(zhuǎn)換" ---- 參見條款39)調(diào)用這個類中定義的draw函數(shù)。
沒完,還有呢。Lottery和GraphicalObject中的draw函數(shù)都被聲明為虛函數(shù),所以子類可以重新定義它們(見條款36),但如果LotterySimulation想對二者都重新定義那該怎么辦?令人沮喪的是,這不可能,因為一個類只允許有唯一一個沒有參數(shù)、名稱為draw的函數(shù)。(這個規(guī)則有個例外,即一個函數(shù)為const而另一個不是的時候 ---- 見條款21)
從某一方面來說,這個問題很嚴(yán)重,嚴(yán)重到足以成為修改C++語言的理由。ARM中就討論了一種可能,即,允許被繼承的虛函數(shù)可以 "改名" ;但后來又發(fā)現(xiàn),可以通過增加一對新類來巧妙地避開這個問題:
class AuxLottery: public Lottery {
public:
virtual int lotteryDraw() = 0;
virtual int draw() { return lotteryDraw(); }
};
class AuxGraphicalObject: public GraphicalObject {
public:
virtual int graphicalObjectDraw() = 0;
virtual int draw() { return graphicalObjectDraw(); }
};
class LotterySimulation: public AuxLottery,
/t/t/t public AuxGraphicalObject {
public:
virtual int lotteryDraw();
virtual int graphicalObjectDraw();
...
};
這兩個新類, AuxLottery和AuxGraphicalObject,本質(zhì)上為各自繼承的draw函數(shù)聲明了新的名字。新名字以純虛函數(shù)的形式提供,本例中即lotteryDraw和graphicalObjectDraw;函數(shù)是純虛擬的,所以具體的子類必須重新定義它們。另外,每個類都重新定義了繼承而來的draw函數(shù),讓它們調(diào)用新的純虛函數(shù)。最終效果是,在這個類體系結(jié)構(gòu)中,有二義的單個名字draw被有效地分成了無二義但功能等價的兩個名字:lotteryDraw和graphicalObjectDraw:
LotterySimulation *pls = new LotterySimulation;
Lottery *pl = pls;
GraphicalObject *pgo = pls;
// 調(diào)用LotterySimulation::lotteryDraw
pl->draw();
// 調(diào)用LotterySimulation::graphicalObjectDraw
pgo->draw();
這是一個集純虛函數(shù),簡單虛函數(shù)和內(nèi)聯(lián)函數(shù)(參見條款33)綜合應(yīng)用之大成的方法,值得牢記在心。首先,它解決了問題,這個問題說不定哪天你就會碰到。其次,它可以提醒你,使用多繼承會導(dǎo)致復(fù)雜性。是的,這個方法解決了問題,但僅僅為了重新定義一個虛函數(shù)而不得不去引入新的類,你真的愿意這樣做嗎?AuxLottery和AuxGraphicalObject類對于保證類層次結(jié)構(gòu)的正確運(yùn)轉(zhuǎn)是必需的,但它們既不對應(yīng)于問題范疇(problem domain )的某個抽象,也不對應(yīng)于實(shí)現(xiàn)范疇(implementation domain)的某個抽象。它們單純是作為一種實(shí)現(xiàn)設(shè)備而存在,再沒有別的用處。你一定知道,好的軟件是 "設(shè)備無關(guān)" 的,這條法則在此也適用。
將來使用MI還會面臨更多的問題,二義性問題(盡管有趣)只不過是剛開始。另一個問題基于這樣一個實(shí)踐經(jīng)驗:一個起初象下面這樣的繼承層次結(jié)構(gòu):
class B { ... };
class C { ... };
class D: public B, public C { ... };
/t/t B C
/t/t /
/t/t /
/t/t /
/t/t D
往往最后悲慘地發(fā)展成象下面這樣:
class A { ... };
class B : virtual public A { ... };
class C : virtual public A { ... };
class D: public B, public C { ... };
/t/t A
/t/t /
/t/t /
/t/t /
/t/t B C
/t/t /
/t/t /
/t/t /
/t/t D
鉆石可能是女孩最好的朋友,也許不是;但肯定的是,象這樣一種鉆石形狀的繼承結(jié)構(gòu)絕對不可能成為我們的朋友。如果創(chuàng)建了象這樣的層次結(jié)構(gòu),就會立即面臨這樣一個問題:是不是該讓A成為虛基類呢?即,從A的繼承是否應(yīng)該是虛擬的呢?現(xiàn)實(shí)中,答案幾乎總是 ---- 應(yīng)該;只有極少數(shù)情況下會想讓類型D的對象包含A的數(shù)據(jù)成員的多個拷貝。正是認(rèn)識到這一事實(shí),上面的B和C將A聲明為虛基類。
遺憾的是,在定義B和C的時候,你可能不知道將來是否會有類去同時繼承它們,而且知不知道這一點(diǎn)實(shí)際上對正確地定義這兩個類沒有必要。對類的設(shè)計者來說,這實(shí)在是進(jìn)退兩難。如果不將A聲明為B和C的虛基類,今后D的設(shè)計者就有可能需要修改B和C的定義,以便更有效地使用它們。通常,這很難做到,因為A,B和C的定義往往是只讀的。例如這樣的情況:A,B和C在一個庫中,而D由庫的用戶來寫。
另一方面,如果真的將A聲明為B和C的虛基類,往往會在空間和時間上強(qiáng)加給用戶額外的開銷。因為虛基類常常是通過對象指針來實(shí)現(xiàn)的,并非對象本身。自不必說,內(nèi)存中對象的分布是和編譯器相關(guān)的,但一條不變的事實(shí)是:如果A作為 "非虛" 基類,類型D的對象在內(nèi)存中的分布通常占用連續(xù)的內(nèi)存單元;如果A作為 "虛" 基類,有時,類型D的對象在內(nèi)存中的分布占用連續(xù)的內(nèi)存單元,但其中兩個單元包含的是指針,指向包含虛基類數(shù)據(jù)成員的內(nèi)存單元:
A是非虛基類時D對象通常的內(nèi)存分布:
/t A部分+ B部分+ A部分 + C部分 + D部分
A是虛基類時D對象在某些編譯器下的內(nèi)存分布:
/t/t/t ------------------------------------------------
/t/t/t |/t/t/t/t/t/t |
/t/t/t |/t/t/t/t/t/t +
/t B部分 + 指針 + C部分 + 指針 + D部分 + A部分
/t/t/t/t/t/t |/t/t +
/t/t/t/t/t/t |/t/t |
/t/t/t/t/t/t ------------------------
即使編譯器不采用這種特殊的實(shí)現(xiàn)策略,使用虛繼承通常也會帶來某種空間上的懲罰。
考慮到這些因素,看來,在進(jìn)行高效的類設(shè)計時如果涉及到MI,作為庫的設(shè)計者就要具有超凡的遠(yuǎn)見。然而現(xiàn)在的年代,常識都日益成為了稀有品,因而你會不明智地過多依賴于語言特性,這就不僅要求設(shè)計者能夠預(yù)計得到未來的需要,而且簡直就是要你做到徹底的先知先覺(參見條款M32)。
當(dāng)然,這也可以說成是在虛函數(shù)和非虛函數(shù)間選擇,但還是有重大的不同。條款36說明,虛函數(shù)具有定義明確的高級含義,非虛函數(shù)也同樣具有定義明確的高級含義,而且它們的含義有顯著的不同,所以在清楚自己想對子類的設(shè)計者傳達(dá)什么含義的基礎(chǔ)上,在二者之間作出選擇是可能的。但是,決定基類是否應(yīng)該是虛擬的,則缺乏定義明確的高級含義;相反,決定通常取決于整個繼承的層次結(jié)構(gòu),所以除非知道了整個層次結(jié)構(gòu),否則無法做出決定。如果正確地定義出個類之前需要清楚地知道將來怎么使用它,這種情況下將很難設(shè)計出高效的類。
就算避開了二義性問題,并且解決了是否應(yīng)該從基類虛擬繼承的疑問,還是會有許多復(fù)雜性問題等著你。為了長話短說,在此我僅提出應(yīng)該記住的其它兩點(diǎn):
? 向虛基類傳遞構(gòu)造函數(shù)參數(shù)。非虛繼承時,基類構(gòu)造函數(shù)的參數(shù)是由緊臨的派生類的成員初始化列表指定的。因為單繼承的層次結(jié)構(gòu)只需要非虛基類,繼承層次結(jié)構(gòu)中參數(shù)的向上傳遞采用的是一種很自然的方式:第n層的類將參數(shù)傳給第n-1層的類。但是,虛基類的構(gòu)造函數(shù)則不同,它的參數(shù)是由繼承結(jié)構(gòu)中最底層派生類的成員初始化列表指定的。這就造成,負(fù)責(zé)初始化虛基類的那個類可能在繼承圖中和它相距很遠(yuǎn);如果有新類增加到繼承結(jié)構(gòu)中,執(zhí)行初始化的類還可能改變。(避免這個問題的一個好辦法是:消除對虛基類傳遞構(gòu)造函數(shù)參數(shù)的需要。最簡單的做法是避免在這樣的類中放入數(shù)據(jù)成員。這本質(zhì)上是Java的解決之道:Java中的虛基類(即,"接口")禁止包含數(shù)據(jù))
? 虛函數(shù)的優(yōu)先度。就在你自認(rèn)為弄清了所有的二義之時,它們卻又在你面前搖身一變。再次看看關(guān)于類A,B,C和D的鉆石形狀的繼承圖。假設(shè)A定義了一個虛成員函數(shù)mf,C重定義了它;B和D則沒有重定義mf:
/t A virtual void mf();
/t /
/t /
/t /
/t B C virtual void mf();
/t /
/t /
/t /
/t D
根據(jù)以前的討論,你會認(rèn)為下面有二義:
D *pd = new D;
pd->mf();/t/t // A::mf或者C::mf?
該為D的對象調(diào)用哪個mf呢,是直接從C繼承的還是間接(通過B)從A繼承的那個呢?答案取決于B和C如何從A繼承。具體來說,如果A是B或C的非虛基類,調(diào)用具有二義性;但如果A是B和C的虛基類,就可以說C中mf的重定義優(yōu)先度高于最初A中的定義,因而通過pd對mf的調(diào)用將(無二義地)解析為C::mf。如果你坐下來仔細(xì)想想,這正是你想要的行為;但需要坐下仔細(xì)想想才能弄懂,也確實(shí)是一種痛苦。
也許至此你會承認(rèn)MI確實(shí)會導(dǎo)致復(fù)雜化。也許你認(rèn)識到每個人其實(shí)都不想使用它。也許你準(zhǔn)備建議國際C++標(biāo)準(zhǔn)委員會將多繼承從語言中去掉;或者至少你想向你的老板建議,全公司的程序員都禁止使用它。
也許你太性急了。
請記住,C++的設(shè)計者并沒有想讓多繼承難以使用;恰恰是,想讓一切都能以更合理的方式協(xié)調(diào)工作,這本身會帶來某些復(fù)雜性。上面的討論中你會注意到,這些復(fù)雜性很多是由于使用虛基類引起的。如果能避免使用虛基類 ---- 即,如果能避免產(chǎn)生那種致命的鉆石形狀繼承圖 ---- 事情就好處理多了。
例如,條款34中講到,協(xié)議類(Protocol class)的存在僅僅是為派生類制定接口;它沒有數(shù)據(jù)成員,沒有構(gòu)造函數(shù),有一個虛析構(gòu)函數(shù)(參見條款14),有一組用來指定接口的純虛函數(shù)。一個Person協(xié)議類看起來象下面這樣:
class Person {
public:
virtual ~Person();
virtual string name() const = 0;
virtual string birthDate() const = 0;
virtual string address() const = 0;
virtual string nationality() const = 0;
};
這個類的用戶在編程時必須使用Person的指針或引用,因為抽象類不能被實(shí)例化。
為了創(chuàng)建 "可以作為Person對象而使用" 的對象,Person的用戶使用工廠函數(shù)(factory function,參見條款34)來實(shí)例化具體的子類:
// 工廠函數(shù),從一個唯一的數(shù)據(jù)庫ID
// 創(chuàng)建一個Person對象
Person * makePerson(DatabaseID personIdentifier);
DatabaseID askUserForDatabaseID();
DatabaseID pid = askUserForDatabaseID();
Person *pp = makePerson(pid); // 創(chuàng)建支持Person
/t/t/t/t // 接口的對象
.../t/t/t // 通過Person的成員函數(shù)
/t/t/t/t // 操作*pp
delete pp;/t/t // 刪除不再需要的對象
這就帶來一個問題:makePerson返回的指針?biāo)赶虻膶ο笕绾蝿?chuàng)建呢?顯然,必須從Person派生出某種具體類,使得makePerson可以對其進(jìn)行實(shí)例化。
假設(shè)這個類被稱為MyPerson。作為一個具體類,MyPerson必須實(shí)現(xiàn)從Person繼承而來的純虛函數(shù)。這可以從零做起,但如果已經(jīng)存在一些組件可以完成大多數(shù)或全部所需的工作,那么從軟件工程的角度來說,能利用這些組件將再好不過。例如,假設(shè)已經(jīng)有一個和數(shù)據(jù)庫有關(guān)的舊類PersonInfo,它提供的功能正是MyPerson所需要的:
class PersonInfo {
public:
PersonInfo(DatabaseID pid);
virtual ~PersonInfo();
virtual const char * theName() const;
virtual const char * theBirthDate() const;
virtual const char * theAddress() const;
virtual const char * theNationality() const;
virtual const char * valueDelimOpen() const; // 看下文
virtual const char * valueDelimClose() const;
...
};
可以斷定這是一個很舊的類,因為它的成員函數(shù)返回的是const char*而不是string對象。但是,如果鞋合腳,為什么不穿呢?這個類的成員函數(shù)名暗示,這雙鞋穿上去會很舒服。
隨之你會發(fā)現(xiàn),當(dāng)初設(shè)計PersonInfo是用來方便地以各種不同格式打印數(shù)據(jù)庫字段,每個字段值的開頭和結(jié)尾用特殊字符串分開。默認(rèn)情況下,字段值的起始分隔符和結(jié)束分隔符為括號,所以字段值 "Ring-tailed Lemur" 將會這樣被格式化:
[Ring-tailed Lemur]
因為括號不是所有PersonInfo的用戶都想要的,虛函數(shù)valueDelimOpen和valueDelimClose允許派生類指定它們自己的起始分隔符和結(jié)束分隔符。PersonInfo類的theName,theBirthDate,theAddress以及theNationality的實(shí)現(xiàn)將調(diào)用這兩個虛函數(shù),在它們的返回值中添加適當(dāng)?shù)姆指舴?。拿PersonInfo::name作為例子,代碼看起來象這樣:
const char * PersonInfo::valueDelimOpen() const
{
return "[";/t/t // 默認(rèn)起始分隔符
}
const char * PersonInfo::valueDelimClose() const
{
return "]";/t/t // 默認(rèn)結(jié)束分隔符
}
const char * PersonInfo::theName() const
{
// 為返回值保留緩沖區(qū)。因為是靜態(tài)
// 類型,它被自動初始化為全零。
static char value[MAX_FORMATTED_FIELD_VALUE_LENGTH];
// 寫起始分隔符
strcpy(value, valueDelimOpen());
將對象的名字字段值添加到字符串中
// 寫結(jié)束分隔符
strcat(value, valueDelimClose());
return value;
}
有些人會挑剔PersonInfo::theName的設(shè)計(特別是使用了固定大小的靜態(tài)緩沖區(qū) ---- 參見條款23),但請將你的挑剔放在一邊,關(guān)注這一點(diǎn):首先,theName調(diào)用valueDelimOpen,生成它將要返回的字符串的起始分隔符;然后,生成名字值本身;最后,調(diào)用valueDelimClose。因為valueDelimOpen和valueDelimClose是虛函數(shù),theName返回的結(jié)果既依賴于PersonInfo,也依賴于從PersonInfo派生的類。
作為MyPerson的實(shí)現(xiàn)者,這是條好消息,因為在研讀Person文檔的細(xì)則時你發(fā)現(xiàn),name及其相關(guān)函數(shù)需要返回的是不帶修飾的值,即,不允許帶分隔符。也就是說,如果一個人來自Madagascar,調(diào)用這個人的nationality函數(shù)將返回"Madagascar",而不是 "[Madagascar]"。
MyPerson和PersonInfo之間的關(guān)系是,PersonInfo剛好有些函數(shù)使得MyPerson易于實(shí)現(xiàn)。僅次而已。沒看到有 "是一個" 或 "有一個" 的關(guān)系。它們的關(guān)系是 "用...來實(shí)現(xiàn)",而且我們知道,這可以用兩種方式來表示:通過分層(見條款40)和通過私有繼承(見條款42)。條款42指出,分層一般來說是更好的方法,但在有虛函數(shù)要被重新定義的情況下,需要使用私有繼承?,F(xiàn)在的情況是,MyPerson需要重新定義valueDelimOpen和valueDelimClose,所以不能用分層,而必須用私有繼承:MyPerson必須從PersonInfo私有繼承。
但MyPerson還必須實(shí)現(xiàn)Person接口,因而需要公有繼承。這導(dǎo)致了多繼承一個很合理的應(yīng)用:將接口的公有繼承和實(shí)現(xiàn)的私有繼承結(jié)合起來:
class Person {/t/t // 這個類指定了
public:/t/t/t // 需要被實(shí)現(xiàn)
virtual ~Person();/t/t // 的接口
virtual string name() const = 0;
virtual string birthDate() const = 0;
virtual string address() const = 0;
virtual string nationality() const = 0;
};
class DatabaseID { ... };/t // 被后面的代碼使用;
/t/t/t/t // 細(xì)節(jié)不重要
class PersonInfo {/t/t // 這個類有些有用
public:/t/t/t // 的函數(shù),可以用來
PersonInfo(DatabaseID pid);/t // 實(shí)現(xiàn)Person接口
virtual ~PersonInfo();
virtual const char * theName() const;
virtual const char * theBirthDate() const;
virtual const char * theAddress() const;
virtual const char * theNationality() const;
virtual const char * valueDelimOpen() const;
virtual const char * valueDelimClose() const;
...
};
class MyPerson: public Person, // 注意,使用了
/t private PersonInfo { // 多繼承
public:
MyPerson(DatabaseID pid): PersonInfo(pid) {}
// 繼承來的虛分隔符函數(shù)的重新定義
const char * valueDelimOpen() const { return ""; }
const char * valueDelimClose() const { return ""; }
// 所需的Person成員函數(shù)的實(shí)現(xiàn)
string name() const
{ return PersonInfo::theName(); }
string birthDate() const
{ return PersonInfo::theBirthDate(); }
string address() const
{ return PersonInfo::theAddress(); }
string nationality() const
{ return PersonInfo::theNationality(); }
};
用圖形表示,看起來象下面這樣:
/t Person PersonInfo
/t/t /
/t/t /
/t/t /
/t/t MyPerson
這種例子證明,MI會既有用又易于理解,盡管可怕的鉆石形狀繼承圖不會明顯消失。
然而,必須當(dāng)心誘惑。有時你會掉進(jìn)這樣的陷阱中:對某個需要改動的繼承層次結(jié)構(gòu)來說,本來用一個更基本的重新設(shè)計可以更好,但你卻為了追求速度而去使用MI。例如,假設(shè)為可以活動的卡通角色設(shè)計一個類層次結(jié)構(gòu)。至少從概念上來說,讓各種角色能跳舞唱歌將很有意義,但每一種角色執(zhí)行這些動作時方式都不一樣。另外,跳舞唱歌的缺省行為是什么也不做。
所有這些用C++來表示就象這樣:
class CartoonCharacter {
public:
virtual void dance() {}
virtual void sing() {}
};
虛函數(shù)自然地體現(xiàn)了這樣的約束:唱歌跳舞對所有CartoonCharacter對象都有意義。什么也不做的缺省行為通過類中那些函數(shù)的空定義來表示(參見條款36)。假設(shè)有一個特殊類型的卡通角色是蚱蜢,它以自己特殊的方式跳舞唱歌:
class Grasshopper: public CartoonCharacter {
public:
virtual void dance(); // 定義在別的什么地方
virtual void sing(); // 定義在別的什么地方
};
現(xiàn)在假設(shè),在實(shí)現(xiàn)了Grasshopper類后,你又想為蟋蟀增加一個類:
class Cricket: public CartoonCharacter {
public:
virtual void dance();
virtual void sing();
};
當(dāng)坐下來實(shí)現(xiàn)Cricket類時,你意識到,為Grasshopper類所寫的很多代碼可以重復(fù)使用。但這需要費(fèi)點(diǎn)神,因為要到各處去找出蚱蜢和蟋蟀唱歌跳舞的不同之處。你猛然間想出了一個代碼復(fù)用的好辦法:你準(zhǔn)備用Grasshopper類來實(shí)現(xiàn)Cricket類,你還準(zhǔn)備使用虛函數(shù)以使Cricket類可以定制Grasshopper的行為。
你立即認(rèn)識到這兩個要求 ---- "用...來實(shí)現(xiàn)" 的關(guān)系,以及重新定義虛函數(shù)的能力 ---- 意味著Cricket必須從Grasshopper私有繼承,但蟋蟀當(dāng)然還是一個卡通角色,所以你通過同時從Grasshopper和CartoonCharacter繼承來重新定義Cricket:
class Cricket: public CartoonCharacter,
/t private Grasshopper {
public:
virtual void dance();
virtual void sing();
};
然后準(zhǔn)備對Grasshopper類做必要的修改。特別是,需要聲明一些新的虛函數(shù)讓Cricket重新定義:
class Grasshopper: public CartoonCharacter {
public:
virtual void dance();
virtual void sing();
protected:
virtual void danceCustomization1();
virtual void danceCustomization2();
virtual void singCustomization();
};
蚱蜢跳舞現(xiàn)在被定義成象這樣:
void Grasshopper::dance()
{
執(zhí)行共同的跳舞動作;
danceCustomization1();
執(zhí)行更多共同的跳舞動作;
danceCustomization2();
執(zhí)行最后共同的跳舞動作;
}
蚱蜢唱歌的設(shè)計與此類似。
很明顯,Cricket類必須修改一下,因為它必須重新定義新的虛函數(shù):
class Cricket:public CartoonCharacter,
private Grasshopper {
public:
virtual void dance() { Grasshopper::dance(); }
virtual void sing() { Grasshopper::sing(); }
protected:
virtual void danceCustomization1();
virtual void danceCustomization2();
virtual void singCustomization();
};
這看來很不錯。當(dāng)需要Cricket對象去跳舞時,它執(zhí)行Grasshopper類中共同的dance代碼,然后執(zhí)行Cricket類中定制的dance代碼,接著繼續(xù)執(zhí)行Grasshopper::dance中的代碼,等等。
然而,這個設(shè)計中有個嚴(yán)重的缺陷,這就是,你不小心撞上了 "奧卡姆剃刀" ---- 任何一種奧卡姆剃刀都是有害的思想,William of Occam的尤其如此。奧卡姆者鼓吹:如果沒有必要,就不要增加實(shí)體?,F(xiàn)在的情況下,實(shí)體就是指的繼承關(guān)系。如果你相信多繼承比單繼承更復(fù)雜的話(我希望你相信),Cricket類的設(shè)計就沒必要復(fù)雜。(譯注:1) William of Occam(1285-1349),英國神學(xué)家,哲學(xué)家。2) 奧卡姆剃刀(Occam's razor)是一種思想,主要由William of Occam提出。之所以將它稱為 "奧卡姆剃刀",是因為William of Occam經(jīng)常性地、很銳利地運(yùn)用這一思想。)
問題的根本之處在于,Cricket類和Grasshopper類之間并非 "用...來實(shí)現(xiàn)" 的關(guān)系。而是,Cricket類和Grasshopper類之間享有共同的代碼。特別是,它們享有決定唱歌跳舞行為的代碼 ---- 蚱蜢和蟋蟀都有這種共同的行為。
說兩個類具有共同點(diǎn)的方式不是讓一個類從另一個類繼承,而是讓它們都從一個共同的基類繼承,蚱蜢和蟋蟀之間的公共代碼不屬于Grasshopper類,也不屬于Cricket,而是屬于它們共同的新的基類,如,Insect:
class CartoonCharacter { ... };
class Insect: public CartoonCharacter {
public:
virtual void dance(); // 蚱蜢和蟋蟀
virtual void sing(); // 的公共代碼
protected:
virtual void danceCustomization1() = 0;
virtual void danceCustomization2() = 0;
virtual void singCustomization() = 0;
};
class Grasshopper: public Insect {
protected:
virtual void danceCustomization1();
virtual void danceCustomization2();
virtual void singCustomization();
};
class Cricket: public Insect {
protected:
virtual void danceCustomization1();
virtual void danceCustomization2();
virtual void singCustomization();
};
/t CartoonCharacter
/t/t |
/t/t |
/t/t Insect
/t/t /
/t/t /
/t/t /
Grasshopper Cricket
可以看到,這個設(shè)計更清晰。只是涉及到單繼承,此外,只是用到了公有繼承。Grasshopper和Cricket定義的只是定制功能;它們從Insect一點(diǎn)沒變地繼承了dance和sing函數(shù)。William of Occam一定會很驕傲。
盡管這個設(shè)計比采用了MI的那個方案更清晰,但初看可能會覺得比使用MI的還要遜色。畢竟,和MI的方案相比,這個單繼承結(jié)構(gòu)中引入了一個全新的類,而使用MI就不需要。如果沒必要,為什么要引入一個額外的類呢?
這就將你帶到了多繼承誘人的本性面前。表面看來,MI好象使用起來更容易。它不需要增加新的類,雖然它要求在Grasshopper類中增加一些新的虛函數(shù),但這些函數(shù)在任何情況下都是要增加的。
設(shè)想有個程序員正在維護(hù)一個大型C++類庫,現(xiàn)在需要在庫中增加一個新的類,就象Cricket類要被增加到現(xiàn)有的的CartoonCharacter/Grasshopper層次結(jié)構(gòu)中一樣。程序員知道,有大量的用戶使用現(xiàn)有的層次結(jié)構(gòu),所以,庫的變化越大,對用戶的影響越大。程序員決心將這種影響降低到最小。對各種選擇再三考慮之后,程序員認(rèn)識到,如果增加一個從Grasshopper到Cricket的私有繼承連接,層次結(jié)構(gòu)中將不需要任何其它變化。程序員不禁因為這個想法露出了微笑,暗自慶幸今后可以大量地增加功能,而代價僅僅只是增加很小一點(diǎn)復(fù)雜性。
現(xiàn)在設(shè)想這個負(fù)責(zé)維護(hù)的程序員是你。那么,請抵御這一誘惑!
新聞熱點(diǎn)
疑難解答
圖片精選