解讀《小類、大對象》

sweet tip: 本文的一些背景知識來源于袁英杰《小類,大對象:C++》,建議先閱讀《小類,大對象:C++》。

2015年,初次接觸小類、大對象的時(shí)候,還不知道其背后的設(shè)計(jì)意圖。但是直覺上給我一個(gè)很強(qiáng)的沖擊:原來利用這樣一種多重繼承的手段,就可以使類的職責(zé)更加單一,符合了高內(nèi)聚、低耦合的設(shè)計(jì)。之前寫過一篇文章,叫做《淺析ROLE》,跟袁英杰的《小類,大對象:C++》談到的很多內(nèi)容很相似。但是對于其背后的設(shè)計(jì)哲學(xué),以及存在的一些陷阱,卻全然不知。后來,通過反復(fù)實(shí)踐,也跳進(jìn)過一些坑。曾經(jīng)一度,甚至開始對它產(chǎn)生懷疑:雖然設(shè)計(jì)是好的,但是如果這個(gè)架構(gòu)引入很多故障,那么是不是值得去用它呢?

其實(shí),會用和用好之間還有很遠(yuǎn)的路要走。用好,需要了解其背后的設(shè)計(jì)過程。任何一個(gè)設(shè)計(jì),都是存在其約束和上下文的,如果不想了解其上下文,而把它作為一個(gè)放之四海的準(zhǔn)則,往往會產(chǎn)生很多讓人困惑的問題。正如文章《小類,大對象:C++》中談到,有些規(guī)則甚至要靠人為的約定保證的,這就要求人懂得這個(gè)架構(gòu)背后的設(shè)計(jì)原理,以及清晰知道自己用這個(gè)架構(gòu)的設(shè)計(jì)意圖。

《小類,大對象:C++》核心的實(shí)現(xiàn)是多重繼承,但是文章中沒有用具體的代碼實(shí)現(xiàn)來展示多重繼承的優(yōu)勢和一些問題的規(guī)避,只是文字上的描述,比如菱形繼承中數(shù)據(jù)重復(fù)的問題。本文將把這些以示例代碼的形式展開,旨在讓自己有更深入的認(rèn)識,也期望能夠幫助到有類似困惑的人。

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

既然你對角色進(jìn)行了劃分,在某種場景下,你只可能是FatherSon中的一種,這是你的設(shè)計(jì)意圖(而我們常常會忘記這個(gè)初心)。這種情況下,甚至連編譯器都看不過去了,會通過報(bào)錯(cuò)來提示你,它搞不清楚你現(xiàn)在到底是父親還是兒子。

也許更較真一點(diǎn),你說,我跟我的媽媽和兒子同時(shí)在一起吃飯,那我在這頓飯上我既是父親又是兒子。哈哈,那我也來較真一下,你可能在吃其中某一口飯的時(shí)候是像個(gè)父親一樣的吃,在吃另一口的時(shí)候,像個(gè)兒子再吃。在某一個(gè)時(shí)刻(就是你決定調(diào)用eat方法的時(shí)刻),你一定是處于某個(gè)角色,而不是兩個(gè)兼有。

所以對eat的調(diào)用應(yīng)該是這樣的,它一定是某個(gè)角色在調(diào)用:

    Person person;
    Father& father = person; 
    father.eat();
2 菱形繼承
  • 傳統(tǒng)意義上的繼承關(guān)系是這樣的(它是單繼承,向下生長):
  • 《小類,大對象:C++》中講的繼承關(guān)系是這樣的(多重繼承),稱之為倒置樹(它是向上生長的):

那么,是不是利用小類、大對象做設(shè)計(jì),就完全摒棄了傳統(tǒng)的繼承方式呢?答案是否定的。傳統(tǒng)的繼承方式,對于消除重復(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ī)制的限制,典型的多重繼承帶來的二義性,編譯器會報(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的對象去訪問Man,都將是非法的。這也更強(qiáng)烈地表明了我們的設(shè)計(jì)意圖:在這個(gè)繼承體系里,Man僅僅用來消除重復(fù),不作為角色使用。

因此,這樣調(diào)用會失?。?/p>

    Person person;
    Man& man = person; //compile error
    man.eat();

這樣也會失敗:

    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();

而對于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)的需要。對于每個(gè)角色,它確實(shí)需要有自己的一份數(shù)據(jù)拷貝,即便這些數(shù)據(jù)和另外一個(gè)角色是重復(fù)的。這些“重復(fù)數(shù)據(jù)”在每個(gè)角色那里都有自己的不同狀態(tài)。另外,由于外部訪問是基于某個(gè)具體角色的,所以不會造成二義性問題。(摘自:《小類,大對象:C++》)

例如下面的代碼場景:

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ù)。(摘自:《小類,大對象: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
{
};

對于同一個(gè)Person,可以有FatherSon兩個(gè)角色,但是絕對不應(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ì),會給后續(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

你可能會說,干嘛費(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)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 背景 時(shí)至今日,C++的核心戰(zhàn)場在于:對于性能,空間和實(shí)時(shí)性有高要求的系統(tǒng)。 而在這類系統(tǒng)上,也有其特定的約束和挑...
    _袁英杰_閱讀 12,794評論 19 43
  • DCI[https://en.wikipedia.org/wiki/Data,_context_and_inter...
    MagicBowen閱讀 9,280評論 5 31
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,692評論 18 399
  • “Design is there to enable you to keep changing the softw...
    _張曉龍_閱讀 13,131評論 3 50
  • 我們在一路成長中,去一個(gè)又一個(gè)的地方,認(rèn)識一群又一群的人兒。每個(gè)地方都會帶給我們不一樣的感覺,我們邊吐槽邊懷戀,在...
    眉心沒有美人痣閱讀 198評論 0 1

友情鏈接更多精彩內(nèi)容