sweet tip: 本文的一些背景知識(shí)來源于袁英杰的《小類符喝,大對(duì)象:C++》,建議先閱讀《小類甜孤,大對(duì)象:C++》协饲。
2015年畏腕,初次接觸小類、大對(duì)象的時(shí)候茉稠,還不知道其背后的設(shè)計(jì)意圖描馅。但是直覺上給我一個(gè)很強(qiáng)的沖擊:原來利用這樣一種多重繼承的手段,就可以使類的職責(zé)更加單一而线,符合了高內(nèi)聚铭污、低耦合的設(shè)計(jì)。之前寫過一篇文章膀篮,叫做《淺析ROLE》嘹狞,跟袁英杰的《小類,大對(duì)象:C++》談到的很多內(nèi)容很相似誓竿。但是對(duì)于其背后的設(shè)計(jì)哲學(xué)磅网,以及存在的一些陷阱,卻全然不知筷屡。后來涧偷,通過反復(fù)實(shí)踐,也跳進(jìn)過一些坑毙死。曾經(jīng)一度燎潮,甚至開始對(duì)它產(chǎn)生懷疑:雖然設(shè)計(jì)是好的,但是如果這個(gè)架構(gòu)引入很多故障扼倘,那么是不是值得去用它呢确封?
其實(shí),會(huì)用和用好之間還有很遠(yuǎn)的路要走唉锌。用好隅肥,需要了解其背后的設(shè)計(jì)過程。任何一個(gè)設(shè)計(jì)袄简,都是存在其約束和上下文的,如果不想了解其上下文泛啸,而把它作為一個(gè)放之四海的準(zhǔn)則绿语,往往會(huì)產(chǎn)生很多讓人困惑的問題。正如文章《小類候址,大對(duì)象:C++》中談到吕粹,有些規(guī)則甚至要靠人為的約定保證的,這就要求人懂得這個(gè)架構(gòu)背后的設(shè)計(jì)原理岗仑,以及清晰知道自己用這個(gè)架構(gòu)的設(shè)計(jì)意圖匹耕。
《小類,大對(duì)象:C++》核心的實(shí)現(xiàn)是多重繼承荠雕,但是文章中沒有用具體的代碼實(shí)現(xiàn)來展示多重繼承的優(yōu)勢(shì)和一些問題的規(guī)避稳其,只是文字上的描述驶赏,比如菱形繼承中數(shù)據(jù)重復(fù)的問題。本文將把這些以示例代碼的形式展開既鞠,旨在讓自己有更深入的認(rèn)識(shí)煤傍,也期望能夠幫助到有類似困惑的人。
1 多個(gè)父類存在同名的方法
struct Father
{
void eat()
{
cout<<"Father::eat"<<endl;
}
};
struct Son
{
void eat()
{
cout<<"Son::eat"<<endl;
}
};
struct Person : Father, Son
{
};
下面的調(diào)用是錯(cuò)誤的嘱蛋,因?yàn)橛衅缌x:
Person person;
person.eat(); //compile error
既然你對(duì)角色進(jìn)行了劃分蚯姆,在某種場(chǎng)景下,你只可能是Father
和Son
中的一種洒敏,這是你的設(shè)計(jì)意圖(而我們常常會(huì)忘記這個(gè)初心)龄恋。這種情況下,甚至連編譯器都看不過去了凶伙,會(huì)通過報(bào)錯(cuò)來提示你郭毕,它搞不清楚你現(xiàn)在到底是父親還是兒子。
也許更較真一點(diǎn)镊靴,你說铣卡,我跟我的媽媽和兒子同時(shí)在一起吃飯,那我在這頓飯上我既是父親又是兒子偏竟。哈哈煮落,那我也來較真一下,你可能在吃其中某一口飯的時(shí)候是像個(gè)父親一樣的吃踊谋,在吃另一口的時(shí)候蝉仇,像個(gè)兒子再吃。在某一個(gè)時(shí)刻(就是你決定調(diào)用eat
方法的時(shí)刻)殖蚕,你一定是處于某個(gè)角色轿衔,而不是兩個(gè)兼有。
所以對(duì)eat
的調(diào)用應(yīng)該是這樣的睦疫,它一定是某個(gè)角色在調(diào)用:
Person person;
Father& father = person;
father.eat();
2 菱形繼承
- 傳統(tǒng)意義上的繼承關(guān)系是這樣的(它是單繼承害驹,向下生長):
- 《小類,大對(duì)象:C++》中講的繼承關(guān)系是這樣的(多重繼承)蛤育,稱之為倒置樹(它是向上生長的):
那么宛官,是不是利用小類、大對(duì)象做設(shè)計(jì)瓦糕,就完全摒棄了傳統(tǒng)的繼承方式呢底洗?答案是否定的。傳統(tǒng)的繼承方式咕娄,對(duì)于消除重復(fù)等亥揖,仍然是一件利器,二者不沖突圣勒。正是由于二者的共存费变,導(dǎo)致了菱形繼承無可避免摧扇。
2.1 產(chǎn)生菱形繼承的幾種情況
(1) 為了消除重復(fù)而引入菱形繼承的情況
通過Man::eat()
消除Father::eat()
和Son::eat()
中的重復(fù),像下面的代碼:
struct Man
{
void eat()
{
cout<<"Man::eat"<<endl;
}
};
struct Father : Man
{
void eat()
{
Man::eat();
cout<<"Father::eat"<<endl;
}
};
struct Son : Man
{
void eat()
{
Man::eat();
cout<<"Son::eat"<<endl;
}
};
struct Person : Father, Son
{
};
如果你是這么調(diào)用eat
方法胡控,是行不通的:
Person person;
Man& man = person; //compile error
man.eat();
這是語言機(jī)制的限制扳剿,典型的多重繼承帶來的二義性,編譯器會(huì)報(bào)錯(cuò)昼激。
但是庇绽,仍然需要回到設(shè)計(jì)去討論這個(gè)問題,因?yàn)閮H僅是為了消除重復(fù)橙困,我們應(yīng)該用private繼承瞧掺,防止外部直接把Man
當(dāng)做角色使用。
代碼像這樣:
struct Father: private Man
{
凡傅、辟狈、、
};
struct Son : private Man
{
夏跷、哼转、、
};
這樣槽华,企圖通過Father
壹蔓、Son
或Person
的對(duì)象去訪問Man
,都將是非法的猫态。這也更強(qiáng)烈地表明了我們的設(shè)計(jì)意圖:在這個(gè)繼承體系里佣蓉,Man
僅僅用來消除重復(fù),不作為角色使用亲雪。
因此勇凭,這樣調(diào)用會(huì)失敗:
Person person;
Man& man = person; //compile error
man.eat();
這樣也會(huì)失斠逶:
Person person;
Father& father = person;
Man& man = father; //compile error
man.eat();
(2) 為了抽象出新的角色而引入菱形繼承的情況
例如虾标,我們從Father
和Son
抽象出公民(Citizen
)這個(gè)角色,Citizen
有選舉權(quán)(vote
)灌砖。
struct Citizen
{
void vote()
{
}
};
struct Father : Citizen
{
};
struct Son : Citizen
{
};
struct Person : Father, Son
{
};
這樣使用是錯(cuò)誤的:
Person person;
Citizen& citizen = person; //compile error
citizen.vote();
從語言機(jī)制上看夺巩,這個(gè)編譯錯(cuò)誤是由于存在歧義。
其實(shí)周崭,從設(shè)計(jì)意圖上看,Citizen
作為新的角色誕生喳张,應(yīng)該作為它的直接子類的角色存在续镇,這就是類的層次設(shè)計(jì)的問題。編譯器的錯(cuò)誤销部,就像在告訴你摸航,不是所有的Person
都是Citizen
制跟。
所以,我們應(yīng)該這樣使用Citizen
:
Person person;
Father& father = person;
Citizen& citizen = father;
citizen.vote();
或者用ROLE
來表示的話酱虎,是這樣:
Person person;
person.ROLE(Father).ROLE(Citizen).vote();
而對(duì)于ROLE(Citizen)
的實(shí)現(xiàn)雨膨,放在Father
這一層,不要讓Person
看到這個(gè)ROLE
的存在:
struct Father: Citizen
{
读串、聊记、、
IMPL_ROLE(Citizen);
};
如果真的必須要通過Person
操作Citizen
恢暖,你需要重新考慮一下排监,角色的抽取是不是合理。如果你真的覺得每一個(gè)Person
都應(yīng)該是Citizen
, 那么Citizen
應(yīng)該是屬于Person
的一個(gè)角色杰捂。像下面這樣:
struct Person : Father, Son, Citizen
{
};
(3) 為了抽象出新的接口而引入菱形繼承的情況
例如舆床,像下面這樣:
struct Man
{
virtual void eat() = 0;
};
struct Father : Man
{
void eat()
{
cout<<"Father::eat"<<endl;
}
};
struct Son : Man
{
void eat()
{
cout<<"Son::eat"<<endl;
}
};
struct Person : Father, Son
{
};
這種情況,跟新的角色的提取很類似嫁佳,但是意圖不同挨队。我們可以用相同的手段來解決這種菱形繼承的問題,那就是類的分層設(shè)計(jì)和使用蒿往。
有些方式可以保證用戶使用正確的類層次:
namespace
{
void g(Man& man)
{
man.eat();
}
}
void f(Father& father)
{
g(father);
}
使用的時(shí)候可能是這樣的:
Person person;
f(person)盛垦;
這樣,我們可以通過namespace
或者private
的方式熄浓,隱藏g(Man& man)
情臭,防止被外部用戶直接調(diào)用,只給外部提供入?yún)?code>Father的接口f(Father& father)
赌蔑。
2.2 菱形繼承中的數(shù)據(jù)重復(fù)
- 基類數(shù)據(jù)的重復(fù)正是每個(gè)角色實(shí)現(xiàn)的需要俯在。對(duì)于每個(gè)角色,它確實(shí)需要有自己的一份數(shù)據(jù)拷貝娃惯,即便這些數(shù)據(jù)和另外一個(gè)角色是重復(fù)的跷乐。這些“重復(fù)數(shù)據(jù)”在每個(gè)角色那里都有自己的不同狀態(tài)。另外趾浅,由于外部訪問是基于某個(gè)具體角色的愕提,所以不會(huì)造成二義性問題。(摘自:《小類皿哨,大對(duì)象:C++》)
例如下面的代碼場(chǎng)景:
struct Man
{
Man(bool isOldEnough) : isOldEnough(isOldEnough)
{}
private:
bool isOldEnough;
};
struct Father : Man
{
Father() : Man(true)
{}
};
struct Son : Man
{
Son() : Man(false)
{}
};
struct Person : Father, Son
{
};
- 如果基類數(shù)據(jù)是共享的浅侨,那也不應(yīng)該使用
virtual
繼承,而是通過委托關(guān)系來共享數(shù)據(jù)证膨。這樣如输,就可以更加合理的避免數(shù)據(jù)重復(fù)。(摘自:《小類,大對(duì)象:C++》)
例如下面的例子不见,就是不必要的數(shù)據(jù)重復(fù)澳化。
struct Age
{
Age(int age) : age(age)
{}
int getAge() const
{
return Age;
}
private:
int age;
};
struct Father : Age
{
};
struct Son : Age
{
};
struct Person : Father, Son
{
};
對(duì)于同一個(gè)Person
,可以有Father
和Son
兩個(gè)角色稳吮,但是絕對(duì)不應(yīng)該有兩個(gè)age
缎谷。所以這類數(shù)據(jù)重復(fù)是要避免的。
通過"委托"(私有繼承)來處理這類數(shù)據(jù)重復(fù)是可以的:
struct Age
{
Age(int age) : age(age)
{}
int getAge() const
{
return Age;
}
private:
int age;
};
struct Father
{
int getAge() const
{
return ROLE(Age).getAge();
}
private:
USE_ROLE(Age);
};
struct Son
{
int getAge() const
{
return ROLE(Age).getAge();
}
private:
USE_ROLE(Age);
};
struct Person : Father, Son, private : Age
{
private:
IMPL_ROLE(Age);
};
2.3 為什么不使用虛繼承灶似?
你仍然可以通過虛繼承來規(guī)避上面的所有問題(指編譯問題):
struct Father: virtual Man
{
列林、、喻奥、
};
struct Son : virtual Man
{
席纽、、撞蚕、
};
但是润梯,這正如不能工作的軟件一樣,包羅萬象的軟件同樣糟糕甥厦。它沒有任何設(shè)計(jì)意圖可言纺铭,僅僅是騙過編譯器。這種不明意圖的設(shè)計(jì)刀疙,會(huì)給后續(xù)的維護(hù)和擴(kuò)展帶來無盡的隱患舶赔。
3 防止過度使用ROLE
struct Citizen
{
void vote()
{
}
};
struct Father : Citizen
{
};
struct Person : Father, Son, Worker
{
};
例如下面的ROLE(Citizen)
是完全沒有必要的。
struct Father : Citizen
{
void doVote()
{
ROLE(Citizen).vote();
}
};
因?yàn)橐坏┰?code>void doVote()中使用了ROLE(Citizen)
谦秧,需要做額外的兩個(gè)工作竟纳,即在Father
中聲明USE_ROLE(Citizen)
和在Person
中定義IMPL_ROLE(Citizen)
struct Father : Citizen
{
void doVote()
{
ROLE(Citizen).vote();
}
private:
USE_ROLE(Citizen);
};
struct Person : Father, Son, Worker
{
private:
IMPL_ROLE(Citizen);
};
而這些工作完全沒有必要,子類調(diào)用父類的方法疚鲤,直接用::
就行锥累。
struct Father : Citizen
{
void doVote()
{
Citizen::vote();
}
所以,一切從簡集歇,不要過度使用ROLE桶略。ROLE
用于沒有直接繼承關(guān)系但是有共同根的類之間方法的調(diào)用。
4 End
你可能會(huì)說诲宇,干嘛費(fèi)這么大勁去理清楚這些問題际歼,我們完全可以避免出現(xiàn)菱形繼承。如果你覺得你完全可以避免這種菱形繼承的問題姑蓝,那你就錯(cuò)了鹅心,當(dāng)系統(tǒng)足夠復(fù)雜、繼承關(guān)系足夠復(fù)雜時(shí)纺荧,它們可能分布在遙遠(yuǎn)的地方巴帮,你很難全局把握溯泣;且不說這些類和模塊由不同人維護(hù),即便是同一個(gè)維護(hù)榕茧,天長日久,也足以讓你難以理清已經(jīng)存在的繼承關(guān)系客给。而承認(rèn)這些問題的存在并做到心中有數(shù)用押,然后按照我們的約束和原則去做設(shè)計(jì),才是成功之道靶剑。