解讀《小類戴甩、大對(duì)象》

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)景下,你只可能是FatherSon中的一種洒敏,這是你的設(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壹蔓、SonPerson的對(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) 為了抽象出新的角色而引入菱形繼承的情況

例如虾标,我們從FatherSon抽象出公民(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,可以有FatherSon兩個(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ì),才是成功之道靶剑。

5 Refrence
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市桩引,隨后出現(xiàn)的幾起案子缎讼,更是在濱河造成了極大的恐慌,老刑警劉巖坑匠,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件血崭,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡厘灼,警方通過查閱死者的電腦和手機(jī)夹纫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來设凹,“玉大人舰讹,你說我怎么就攤上這事∩林欤” “怎么了月匣?”我有些...
    開封第一講書人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長奋姿。 經(jīng)常有香客問我锄开,道長,這世上最難降的妖魔是什么胀蛮? 我笑而不...
    開封第一講書人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任院刁,我火速辦了婚禮,結(jié)果婚禮上粪狼,老公的妹妹穿的比我還像新娘退腥。我一直安慰自己,他們只是感情好再榄,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開白布狡刘。 她就那樣靜靜地躺著,像睡著了一般困鸥。 火紅的嫁衣襯著肌膚如雪嗅蔬。 梳的紋絲不亂的頭發(fā)上剑按,一...
    開封第一講書人閱讀 49,772評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音澜术,去河邊找鬼艺蝴。 笑死,一個(gè)胖子當(dāng)著我的面吹牛鸟废,可吹牛的內(nèi)容都是我干的猜敢。 我是一名探鬼主播,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼盒延,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼缩擂!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起添寺,我...
    開封第一講書人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤胯盯,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后计露,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體博脑,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年薄坏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了趋厉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡胶坠,死狀恐怖君账,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情沈善,我是刑警寧澤乡数,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站闻牡,受9級(jí)特大地震影響净赴,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜罩润,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一玖翅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧割以,春花似錦金度、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至消玄,卻和暖如春跟伏,著一層夾襖步出監(jiān)牢的瞬間丢胚,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來泰國打工受扳, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留携龟,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓辞色,卻偏偏與公主長得像骨宠,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子相满,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 背景 時(shí)至今日立美,C++的核心戰(zhàn)場(chǎng)在于:對(duì)于性能,空間和實(shí)時(shí)性有高要求的系統(tǒng)方灾。 而在這類系統(tǒng)上建蹄,也有其特定的約束和挑...
    _袁英杰_閱讀 11,549評(píng)論 19 43
  • DCI[https://en.wikipedia.org/wiki/Data,_context_and_inter...
    MagicBowen閱讀 8,573評(píng)論 5 31
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法裕偿,內(nèi)部類的語法洞慎,繼承相關(guān)的語法,異常的語法嘿棘,線程的語...
    子非魚_t_閱讀 31,598評(píng)論 18 399
  • “Design is there to enable you to keep changing the softw...
    _張曉龍_閱讀 12,482評(píng)論 3 50
  • 我們?cè)谝宦烦砷L中劲腿,去一個(gè)又一個(gè)的地方,認(rèn)識(shí)一群又一群的人兒鸟妙。每個(gè)地方都會(huì)帶給我們不一樣的感覺焦人,我們邊吐槽邊懷戀,在...
    眉心沒有美人痣閱讀 165評(píng)論 0 1