背景
時(shí)至今日,C++的核心戰(zhàn)場在于:對于性能,空間和實(shí)時(shí)性有高要求的系統(tǒng)。
而在這類系統(tǒng)上,也有其特定的約束和挑戰(zhàn):
在這類系統(tǒng)上,內(nèi)存管理始終是個(gè)需要關(guān)注的問題。而通用內(nèi)存管理算法,要么容易導(dǎo)致內(nèi)存碎片,要么會導(dǎo)致內(nèi)存浪費(fèi)。而為了避免這樣的問題,最好是自己定義內(nèi)存管理器。
內(nèi)存分配是可能失敗的。為了避免這樣的問題,高可靠系統(tǒng)的做法一般是按照系統(tǒng)定義的規(guī)格預(yù)先分配內(nèi)存。比如,系統(tǒng)承諾的規(guī)格是
10000個(gè)Session,那么Session Instance相關(guān)的內(nèi)存,會按照規(guī)格,在系統(tǒng)啟動時(shí)就預(yù)先分配10000個(gè)Session Object需要的內(nèi)存。其背后的原則是:如果失敗,則盡早失敗(fail fast);如果內(nèi)存不足,就應(yīng)該在未發(fā)布時(shí),通過系統(tǒng)加載就可以及時(shí)發(fā)現(xiàn);而不是把風(fēng)險(xiǎn)留到在業(yè)務(wù)現(xiàn)場運(yùn)行時(shí)發(fā)生失敗。對于需要大量銷售的設(shè)備,成本很重要。而內(nèi)存作為成本構(gòu)成,也是非常寶貴的資源。另外,由于競爭對手的存在,如何在讓客戶付出相同的成本下,能夠支撐更多的業(yè)務(wù),始終是一個(gè)重要的指標(biāo)。
對于高性能計(jì)算系統(tǒng),所有的領(lǐng)域?qū)ο?/strong>,盡管部分?jǐn)?shù)據(jù)也可能會持久化,但在運(yùn)行時(shí),都會放在內(nèi)存之中,與另外一些純粹的運(yùn)行時(shí)狀態(tài)一起,構(gòu)成了運(yùn)行時(shí)領(lǐng)域?qū)ο蟮恼w。而對于有高可靠性要求的系統(tǒng),運(yùn)行時(shí)依然會對需要持久化的狀態(tài)進(jìn)行持久化——那怕只是采用主備方式其實(shí)也是一種持久化——所以系統(tǒng)事實(shí)上是無狀態(tài)的。
該如何在這類系統(tǒng)上應(yīng)用小類,大對象的設(shè)計(jì)模式,將是本文隨后討論的內(nèi)容。
委托
回到《小類,大對象》的文章里的例子,我們首先角色實(shí)現(xiàn)分割到了不同的類中,從而得到了四個(gè)類:
struct ConcreteChild : Child
{
// 子女角色相關(guān)接口
void getAdviceFromParent() {...}
// ...
private:
// 子女角色所需的數(shù)據(jù)成員
// ...
};
struct ConcreteParent : Parent
{
// 父母角色相關(guān)接口
void tellStory() {...}
void playGameWithChild() {...}
// ...
private:
// 父母角色所需的數(shù)據(jù)成員
// ...
};
struct ConcreteBoss : Boss
{
// 老板角色相關(guān)接口
public void assignTask() {...}
public void motivate() {...}
// ...
private:
// 老板角色所需的數(shù)據(jù)成員
// ...
};
struct ConcreteUnderling : Underling
{
// 下屬角色相關(guān)接口
public void acceptTask() {...}
public void reportStatus() {...}
// ...
private:
// 下屬角色所需的數(shù)據(jù)成員
// ...
};
然后,我們根據(jù)不同對象的需要對這四個(gè)對象進(jìn)行組合。而首先進(jìn)入我們視野的,是我們最熟悉的委托(Delegation):
struct TypeAPerson
{
// 父母角色相關(guān)接口
void tellStory()
{
parent.tellStory();
}
void playGameWithChild()
{
parent.playGameWithChild();
}
// 孩子角色相關(guān)接口
void getAdviceFromParent()
{
child.getAdviceFromParent();
}
// 下屬角色相關(guān)接口
void acceptTask()
{
subordinate.acceptTask();
}
void reportStatus()
{
subordinate.reportStatus();
}
private: // 通過委托關(guān)系進(jìn)行組合
ConcreteParent parent;
ConcreteChild child;
ConcreteSubordinate subordinate;
};
毫無疑問,這是令人生厭的中間人(MiddleMan)實(shí)現(xiàn)(參見《重構(gòu)》)。
另外,TypeAPerson將自己作為一個(gè)整體展現(xiàn)給了所有客戶;而不同客戶真正需要的卻是不同的角色,因而,無論從依賴范圍角度,還是依賴穩(wěn)定度角度,都無疑增大了系統(tǒng)的耦合度(參見《變化驅(qū)動:正交設(shè)計(jì)》)。
因而,按照我們最初的意圖,如下的實(shí)現(xiàn)方式才是我們真正想要的:
struct TypeAPerson
{
Parent& getParent()
{
return parent;
}
Child& getChild()
{
return child;
}
Subordinate& getSubordinate()
{
return subordinate;
}
private:
// 通過委托關(guān)系進(jìn)行組合
ConcreteParent parent;
ConcreteChild child;
ConcreteSubordinate subordinate;
};
TypeAPerson僅僅應(yīng)該聚合(組合)其所需的角色實(shí)現(xiàn),其唯一的職責(zé)是當(dāng)做一個(gè)角色工廠,面對不同的客戶,將對象轉(zhuǎn)化為不同的角色:
// client of Parent
void f(Parent& parent)
{
// ...
parent.tellStory();
// ...
parent.playGameWithChild();
// ...
}
// ...
// client of Subordinate
void g(Subordinate& subordinate)
{
// ...
subordinate.acceptTask();
// ...
subordinate.reportStatus();
// ...
}
// object
TypeAPerson person1;
// cast to Parent role
f(person1.getParent());
// cast to Subordinate role
g(person1.getSubordinate());
從這段示例代碼,我們可以清晰的看出,f和g是完全不依賴TypeAPerson的,而只依賴自己真正需要依賴的角色。因而,如果TypeBPerson也實(shí)現(xiàn)了相關(guān)角色的話,它也可以和f,g配合。如下:
struct TypeBPerson
{
Parent& getParent()
{
return parent;
}
Subordinate& getSubordinate()
{
return subordinate;
}
private:
// 通過委托關(guān)系進(jìn)行組合
ConcreteParent1 parent; // 與TypeAPerson的Parent實(shí)現(xiàn)不同
ConcreteSubordinate subordinate;
};
// object
TypeBPerson person2;
// cast to Parent role
f(person2.getParent());
// cast to Subordinate role
g(person2.getSubordinate());
通過這個(gè)例子,我們可以清晰的看出:將上帝類根據(jù)自己的上下文需要,分拆成多個(gè)角色類的好處:
- 客戶代碼僅僅依賴的自己所需要依賴的角色,而不關(guān)心提供角色服務(wù)的對象,這解開了客戶與具體對象之間的耦合; 這不僅縮小了依賴范圍,也讓客戶向著穩(wěn)定的方向依賴;

每個(gè)對象自身的不同角色實(shí)現(xiàn)是更加高內(nèi)聚,更加單一職責(zé)。角色與角色之間的耦合也從上帝類那種缺乏封裝,從而更容易導(dǎo)致高耦合的方式中解脫出來。讓各個(gè)角色代碼都更加正交;
不同對象間的角色,如果實(shí)現(xiàn)是相同的,可以直接復(fù)用,這讓代碼復(fù)用更加容易;
對象對多重職責(zé)的實(shí)現(xiàn)更加簡單,只需要通過一個(gè)承擔(dān)角色工廠職責(zé)的類來實(shí)例化對象。
Can We Do Better?
從委托的實(shí)現(xiàn)方式看,已經(jīng)基本上達(dá)到了我們的意圖。
但是,我們也注意到:我們不得不讓TypeAPerson提供一組get接口,來暴露自己實(shí)現(xiàn)的角色。這些get接口,像Singleton的getInstance()一樣。因而,上述的實(shí)現(xiàn)模式也是一種創(chuàng)建者模式。這也正是我們將其稱之為角色工廠的原因。
那么,是否還有更為簡便的方法來實(shí)現(xiàn)角色工廠的職責(zé)?
我們不難發(fā)現(xiàn):TypeAPerson與它所承擔(dān)的角色之間存在IS-A關(guān)系;而且,由于TypeAPerson沒有任何業(yè)務(wù)邏輯代碼,從而也沒有改寫任何一個(gè)父類(角色)的行為,因此這種IS-A關(guān)系必然是滿足里氏替換原則。那我們?yōu)楹尾辉囋嚩嘀乩^承:
struct TypeAPerson
: ConcreteParent
, ConcreteChild
, ConcreteSubordinate
{
};
這樣的方式,在組合TypeAPerson時(shí),明顯比委托的實(shí)現(xiàn)方式更為簡潔。
而在調(diào)用側(cè)使用角色時(shí),由于IS-A這種關(guān)系的存在,其角色轉(zhuǎn)換也可以自動完成,從而也更為簡潔:
// object
TypeAPerson person1;
// naturally cast to Parent role.
f(person1);
// naturally cast to Subordinate role.
g(person1);
當(dāng)然,多重繼承的實(shí)現(xiàn)方式,相對于委托方式,還是存在一點(diǎn)缺點(diǎn):你無法阻止ParentAPerson可以向ConcreteParent這個(gè)更具體的角色轉(zhuǎn)化;但在委托方式下,由于ParentAPerson的具體組合的對象都作為了私有實(shí)現(xiàn)細(xì)節(jié),然后通過getter這種更有彈性的函數(shù)方式,將具體的角色實(shí)現(xiàn),比如ConcreteParent,轉(zhuǎn)化為更抽象的角色Parent;從而具備更好的封裝性。
對于這類問題,其解法有二:一則可以通過語言提供的機(jī)制進(jìn)行強(qiáng)制約束,二則通過人為的約定。多重繼承的方式,在C++里沒有強(qiáng)制禁止向一個(gè)代表某種具體實(shí)現(xiàn)的父類進(jìn)行轉(zhuǎn)換的手段,但站在便利性的角度,我們更傾向于選擇通過人為的約定。畢竟我們清晰的知道我們的設(shè)計(jì)意圖是什么。
角色依賴
在之前的例子中,為了突出要點(diǎn),給出的各個(gè)角色的實(shí)現(xiàn)都是孤立的。但在實(shí)際項(xiàng)目中,角色之間存在依賴關(guān)系,也是一種常見的現(xiàn)象。
比如,在我們的例子中,TypeBPerson的下屬角色的某個(gè)實(shí)現(xiàn),需要調(diào)用上司角色所提供的服務(wù)。如下圖所示:

這個(gè)難不倒我們,我們可以讓下屬角色持有一個(gè)指向上司角色的指針,然后在構(gòu)造下屬角色時(shí)進(jìn)行依賴注入。如下是一種委托方式的實(shí)現(xiàn):
struct ConcreteUnderling : Underling
{
ConcreteUnderling(Boss& boss)
: boss(boss)
{}
void acceptTask()
{
boss.assignTask();
// ...
}
// ...
private:
Boss& boss; // 引用會消耗一個(gè)指針寬度的內(nèi)存
};
struct TypeBPerson
{
TypeBPerson()
: underling(boss)
{}
Boss& getBoss()
{
return boss;
}
Underling& getUnderling()
{
return underling;
}
private:
ConcreteBoss boss; // 確保 Boss 在 Underling 之前
ConcreteUnderling underling;
// ...
};
這樣的實(shí)現(xiàn),存在如下問題:
首先,需要確保不同角色的構(gòu)造順序,一旦角色Underling依賴了角色Boss,那么,在TypeBPerson里,就最好確保boss定義在Underling之前,以免由于構(gòu)造順序所造成的調(diào)用問題。
其次,一旦一個(gè)角色引用了另外一個(gè)角色,那就需要通過引用進(jìn)行依賴注入。這會增加由于引用所消耗的內(nèi)存,對象間的關(guān)聯(lián)越多,那么指針消耗的空間就越大。
尤其是當(dāng)我們追求高內(nèi)聚,低耦合的設(shè)計(jì)時(shí),伴隨而生的是很多很小單一職責(zé)的類,類與類之間會通過引用進(jìn)行職責(zé)委托。這對于那些內(nèi)存不是一個(gè)重要問題的系統(tǒng)而言,或許并不重要。在內(nèi)存珍貴的嵌入式設(shè)備上,這會是一個(gè)問題。
而這個(gè)問題,也會反過來約束C++程序員即便知道高內(nèi)聚,低耦合是正確的,在內(nèi)存約束面前,也只能采取更糟糕的實(shí)現(xiàn)方式。
How About Inheritance?
對于角色關(guān)聯(lián)所導(dǎo)致的問題,換成多重繼承也不會讓情況變得更好:
struct TypeBPerson
: ConcreteBoss // 確保 Boss 在 Underling 之前
, ConcreteUnderling
,
{
TypeBPerson()
: ConcreteUnderling(*this)
{}
};
這兩種實(shí)現(xiàn),差別在于從成員變量便為繼承,而不變的是:都要注意聲明順序,都會造成由引用帶來的內(nèi)存消耗。
工廠方法
幸運(yùn)的是,相對于委托,通過繼承,我們可以擁有更多的武器。
對外部的依賴,在繼承體系下,我們可以通過著名的工廠方法來引入,而不是通過經(jīng)典的構(gòu)造時(shí)依賴注入方式。比如:
struct ConcreteUnderling : Underling
{
void acceptTask()
{
getBoss().assignTask();
// ...
}
private:
// 通過工廠方法引入依賴
virtual Boss& getBoss() = 0;
};
struct TypeBPerson : ConcreteBoss
, ConcreteUnderling
{
Boss& getBoss() override
{
return *this;
}
};
通過這樣的方法,首先解決了角色構(gòu)造順序的問題。因?yàn)?,一個(gè)角色對于另外一個(gè)角色的引用,只有到整個(gè)對象構(gòu)造結(jié)束后,運(yùn)行時(shí)才會進(jìn)行獲取。當(dāng)然你需要避免任何在構(gòu)造函數(shù)里對于其它角色的引用,而事實(shí)上,根據(jù)多個(gè)項(xiàng)目的實(shí)踐,這種構(gòu)造時(shí)引用關(guān)系都可以合理的避免。
內(nèi)存優(yōu)勢
但這個(gè)例子還不能彰顯工廠方法的內(nèi)存優(yōu)勢。讓我們換個(gè)例子:
struct Role1
{
Role1
( Role2& role2
, Role3& role3
, Role4& role4)
: role2(role2)
, role3(role3)
, role4(role4)
{}
// methods
private:
Role2& role2;
Role3& role3;
Role4& role4;
};
struct Object : Role1
, Role2
, Role3
, Role4
{
Object()
: Role1(*this, *this, *this)
{}
};
這種通過直接引用的方式,讓Role1需要消耗三個(gè)引用的空間開銷。
現(xiàn)在將其換成基于工廠方法的實(shí)現(xiàn):
struct Role1
{
// methods
private:
virtual Role2& getRole2() = 0;
virtual Role3& getRole3() = 0;
virtual Role4& getRole4() = 0;
};
struct Object : Role1
, Role2
, Role3
, Role4
{
Role2& getRole2() override { return *this; }
Role3& getRole3() override { return *this; }
Role4& getRole4() override { return *this; }
};
現(xiàn)在我們可以清晰的看出,對于Role1,無論其對外部有多少個(gè)角色引用,都只需要耗費(fèi)一個(gè)指針內(nèi)存的開銷,那就是虛表指針(vptr)。如果Role1本來就存在其它virtual函數(shù),那么這些外部引用,無論存在多少,都沒有增加任何額外的空間開銷。
簡化工廠管理成本
我們知道,按照高內(nèi)聚,低耦合的實(shí)現(xiàn)方式,會導(dǎo)致一堆小類。如果不用小類,大對象的方式,而是讓一個(gè)個(gè)小類可獨(dú)立實(shí)例化。那么這些小類之間如果存在引用關(guān)系,一則需要更多的內(nèi)存消耗,二則,你還不得不需要寫很多工廠來對這些小對象進(jìn)行構(gòu)造和關(guān)聯(lián)。比如:
struct A
{
A(B* b, C* c) : b(b), c(c) {}
// ...
private:
B* b;
C* c;
};
struct AFactory
{
static A* create()
{
B* b = ... // get b;
C* c = ... // get c;
return new A(b, c);
}
}
當(dāng)對象種類很多時(shí),這樣的承擔(dān)工廠職責(zé)的代碼就會很多。而這類的代碼是極其無趣而令人厭煩的。
當(dāng)然對于其它應(yīng)用語言,往往會提供一個(gè)框架,來管理這類工廠職責(zé)。比如Java的Spring。程序員需要做的是:將對象,及對象間的關(guān)聯(lián)關(guān)系,通過xml配置文件進(jìn)行描述,Spring框架會根據(jù)這個(gè)配置文件來履行工廠職責(zé)。
可是,這樣的方法在嵌入式C++下不是一個(gè)可行的途徑,程序員們還是不得不親自去實(shí)現(xiàn)。
而在小類,大對象的實(shí)現(xiàn)模式下,固定模式的工廠方法就完成了這些,程序員不會比Java下用XML進(jìn)行配置,需要付出的努力更多。
struct Object
: Role1
, Role2
, Role3
, Role4
{
// 這些工廠方法即屬于工廠職責(zé)的實(shí)現(xiàn)
Role2& getRole2() override { return *this; }
Role3& getRole3() override { return *this; }
Role4& getRole4() override { return *this; }
};
Once For All
更妙的是,在一個(gè)對象上,無論一個(gè)角色被多少其它角色引用,最后都只需要實(shí)現(xiàn)一次。比如:
struct Role1
{
// ...
private:
virtual Role4& getRole4() = 0;
};
struct Role2
{
// ...
private:
virtual Role4& getRole4() = 0;
};
struct Role3
{
// ...
private:
virtual Role4& getRole4() = 0;
};
struct Object
: Role1
, Role2
, Role3
, Role4
{
private:
// 只實(shí)現(xiàn)一次,就滿足了Role1,Role2,Role3的需要。
Role4& getRole4() override { return *this; }
};
即便對于一個(gè)來自于外部的角色,也是如此:
struct Object
: Role1
, Role2
, Role3
{
// Role4是External Role,因而需要在對象構(gòu)造時(shí)注入
Object(Role4& role4) : role4(role4) {}
private:
// 只實(shí)現(xiàn)一次,就滿足了Role1,Role2,Role3的需要。
Role4& getRole4() override { return role4; }
Role4& role4;
};
更清晰的依賴語義描述
使用工廠方法來表達(dá)依賴的例子如下:
struct Role1
{
void f()
{
// ..
// 對依賴的引用
getRole2().doSth();
// ...
getRole3().blah();
// ...
}
private:
// 對依賴的聲明
virtual Role2& getRole2() = 0;
virtual Role3& getRole3() = 0;
};
不難看出,這段代碼的語義如下:

并且這樣的實(shí)現(xiàn)方式也是完全模式化的,因而我們定義如下兩個(gè)宏:
#define USE_ROLE(RoleType) \\\\
virtual RoleType& get##RoleType() = 0
#define ROLE(RoleType) get##RoleType()
通過它們,之前的例子就可以修改為:
struct Role1
{
void f()
{
// 對依賴的引用
ROLE(Role2).doSth();
// ...
ROLE(Role3).blah();
}
private:
// 對依賴的聲明
USE_ROLE(Role2);
USE_ROLE(Role3);
};
不難發(fā)現(xiàn),一個(gè)角色,通過USE_ROLE語義,僅僅聲明自己對另外一個(gè)角色的依賴,卻完全無需關(guān)心這個(gè)角色的實(shí)現(xiàn)來自何處,也完全無需關(guān)注誰會注入給它。這實(shí)現(xiàn)了與經(jīng)典依賴注入方式完全相同的語義,達(dá)到了完全相同的解耦效果。
而對于工廠的實(shí)現(xiàn),同樣也有明確的模式和清晰的語義:
struct Object
: Role1
, Role2
, Role3
, Role4
{
// 這些工廠方法即屬于工廠職責(zé)的實(shí)現(xiàn)
Role2& getRole2() override { return *this; }
Role3& getRole3() override { return *this; }
Role4& getRole4() override { return *this; }
};
因而,我們可以定義如下的宏:
#define IMPL_ROLE(RoleType) \\\\
RoleType& get##RoleType() override { return *this; }
利用它,我們就可以將工廠代碼改寫為:
struct Object
: Role1
, Role2
, Role3
, Role4
{
private:
IMPL_ROLE(Role2);
IMPL_ROLE(Role3);
IMPL_ROLE(Role4);
};
直接引用,還是工廠方法
直接引用,相對于工廠方法,會帶來更多的內(nèi)存成本,以及工廠管理成本。
但直接引用,會存在微弱的性能優(yōu)勢。根據(jù)我們的項(xiàng)目經(jīng)驗(yàn),這些性能優(yōu)勢微乎其微。但如果在你的項(xiàng)目中,經(jīng)過事后測量,確實(shí)發(fā)現(xiàn)熱點(diǎn)處可以通過直接引用提升性能,那就可以在那個(gè)點(diǎn),將工廠方法,改為直接引用的方式。而這個(gè)改動,并不困難。
繼承樹倒置
當(dāng)使用單根繼承時(shí),如果子類沒有任何代碼,這樣的繼承是沒有太多意義的。比如:
struct Base
{
void foo();
void bar();
private:
int a;
int b;
};
// 這樣的繼承,由于子類沒有任何代碼,
// 如果不是出于某些特定的目的,是沒有任何意義的。
struct Derived : Base
{};
但是,當(dāng)使用多重繼承時(shí),子類沒有任何實(shí)現(xiàn)代碼,卻表達(dá)了一個(gè)非常有價(jià)值的語義:組合。
TypeAPerson有效的將多個(gè)類的數(shù)據(jù)和行為都組合到一個(gè)對象上。最重要的是,這個(gè)沒有任何實(shí)現(xiàn)代碼的子類,恰恰是我們設(shè)計(jì)時(shí)所追求的單一職責(zé) —— TypeAPerson的唯一職責(zé)是:將所有角色組合到一個(gè)對象身上。
這樣的設(shè)計(jì)是一種以組合的方式,最終聚合到單個(gè)對象類。它和經(jīng)典的單根繼承方式所導(dǎo)致的繼承樹正好相反。因而,我們也稱它為繼承樹倒置模式。下圖是來自于一個(gè)項(xiàng)目的例子:

繼承優(yōu)于委托
通常在設(shè)計(jì)中,我們得到的建議往往是:委托優(yōu)于繼承。其原因在于:委托是黑盒復(fù)用,而繼承是一種白盒復(fù)用。
但正如我們之前討論的,在多角色對象的實(shí)現(xiàn)中,最終的對象類,沒有任何業(yè)務(wù)實(shí)現(xiàn)代碼,因此不會對父類產(chǎn)生任何實(shí)現(xiàn)上的依賴。角色類的所有實(shí)現(xiàn),對對象類類而言,在邏輯上和一個(gè)黑盒無異。
而反過來,繼承式組合,相對于委托式組合,至少有如下優(yōu)勢:
- 簡化了組合方式;
- 大大降低了內(nèi)存開銷;
- 消除了角色構(gòu)造順序問題;
- 大大簡化了依賴管理問題;
- 對象到角色的自動轉(zhuǎn)換;
因而,在多角色對象的場景下,繼承式組合要優(yōu)于委托式組合。
為何這樣的多重繼承不邪惡
過去,多重繼承在面向?qū)ο笊鐓^(qū)內(nèi)一直頗有爭議。大多數(shù)書籍都會建議:盡量避免使用多重繼承,要謹(jǐn)慎的使用多重繼承。于是多重繼承就逐漸變?yōu)槌绦騿T唯恐避之不及的東西。
多重繼承的邪惡之處主要體現(xiàn)在幾個(gè)方面:
菱形繼承所帶來的數(shù)據(jù)重復(fù),以及名字二義性。因此,
C++引入了virtual繼承來解決這類問題;即便不是菱形繼承,多個(gè)父類之間的名字也可能存在沖突,從而導(dǎo)致的二義性;
如果子類需要擴(kuò)展或改寫多個(gè)父類的方法時(shí),造成子類的職責(zé)不明,語義混亂;
相對于委托,繼承是一種白盒復(fù)用,即子類可以訪問父類的
protected成員, 這會導(dǎo)致更強(qiáng)的耦合。而多重繼承,由于耦合了多個(gè)父類,相對于單根繼承,這會產(chǎn)生更強(qiáng)的耦合關(guān)系。
但我們看看TypeAPerson,它沒有任何代碼。因而它沒有操作任何父類的數(shù)據(jù)和方法,所以,第3點(diǎn)和第4點(diǎn)的缺點(diǎn)并不存在。
關(guān)于第2點(diǎn)所描述的二義性問題,這需要從兩個(gè)方面來看:子類的內(nèi)部和外部。
從子類內(nèi)部的角度,由于無需訪問父類,所以,多個(gè)父類之間即便存在名字沖突,在子類內(nèi)部也不會造成二義性問題。
而從子類外部來看,如果直接通過子類的實(shí)例來調(diào)用成員函數(shù),這種二義性確實(shí)可能存在。但對于一個(gè)多角色對象,所有外部訪問都應(yīng)該是基于角色的。而對于每個(gè)角色,名字的對應(yīng)關(guān)系是明確的,沒有任何二義性。所以,多角色對象特定的訪問模式,決定了在外部也不會造成二義性。
至于第1點(diǎn),菱形繼承帶來的兩個(gè)問題:數(shù)據(jù)重復(fù)和二義性。
我們首先應(yīng)該避免不符合我們需要的菱形繼承。
對于由設(shè)計(jì)而自然產(chǎn)生的菱形繼承,我們無需使用virtual繼承來避免數(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è)具體角色的,所以不會造成二義性問題。
如果基類數(shù)據(jù)是共享的,那也不應(yīng)該使用
virtual繼承,而是通過委托關(guān)系來共享數(shù)據(jù)。這樣,就可以更加合理的避免數(shù)據(jù)重復(fù)。
至于行為重復(fù),由于角色與角色之間的需求是不應(yīng)該重疊的。所以,對于同一個(gè)對象,很難出現(xiàn)兩個(gè)角色之間有相同的行為子集。如果出現(xiàn),則說明這兩個(gè)角色的職責(zé)都不單一。將重疊的行為子集定義為一個(gè)新的角色,是一個(gè)更合理的設(shè)計(jì)選擇。
綜上所屬,對于多角色對象而言,這種組合方式不會從實(shí)質(zhì)上帶來多重繼承所引起的任何問題。
簡化內(nèi)存管理成本
在開篇時(shí),我們已經(jīng)提到:很多通信設(shè)備,為了避免內(nèi)存管理所導(dǎo)致的問題:比如碎片化,浪費(fèi),以及運(yùn)行時(shí)內(nèi)存分配失敗,會對領(lǐng)域?qū)ο?/strong>自定義自己的內(nèi)存管理器,并在系統(tǒng)加載時(shí),就會預(yù)先分配所需的所有內(nèi)存。如下:
struct Object
: Role1
, Role2
, Role3
, Role4
{
// 重載 new, delete
void* operator new(size_t);
void free(void* p);
private:
IMPL_ROLE(Role2);
IMPL_ROLE(Role3);
IMPL_ROLE(Role4);
};
namespace
{
// 定義數(shù)量為500的Object對象池。
ObjectAllocator<Object, 500> allocator;
}
// 大對象從自己的對象池分配
void* Object::operator new(size_t)
{
return allocator.alloc();
}
void Object::free(void* p)
{
return allocator.free(p);
}
但如果系統(tǒng)由于高內(nèi)聚低耦合的方式而導(dǎo)致了很多小對象,就不得不為每個(gè)小對象都定義自己的內(nèi)存管理器,并且要按照其最大數(shù)量來預(yù)先分配,這會隨著小對象種類的增多,而大大加重內(nèi)存管理的負(fù)擔(dān)。
另外,在高性能計(jì)算領(lǐng)域,為了降低cache miss rate,一般的做法是將關(guān)聯(lián)訪問數(shù)據(jù)都盡可能的放在一起。如果分隔為很多小對象,它們都從不同的內(nèi)存區(qū)域進(jìn)行存放的話,對于性能會造成不同程度的負(fù)面影響。
而通過大對象的方式,所有的數(shù)據(jù)最后都聚集在一個(gè)對象身上,它們的內(nèi)存是連續(xù)的。這對于性能及性能優(yōu)化都有幫助。
其它問題
數(shù)據(jù)該怎樣存放
數(shù)據(jù)按照高內(nèi)聚,低耦合的原則,歸屬于各個(gè)不同的角色。然后,角色間根據(jù)需要,引用并訪問對方的接口。
私有角色
有些角色,純粹是因?yàn)橐粋€(gè)對象自身的需要,并不需要公開給外部,則可以通過private繼承(參見《The Virtues Of Bastard》)進(jìn)行組合。其它角色對它的引用,依然通過USE_ROLE(Role)的方式獲取。
總結(jié)
本文介紹了使用C++,在高性能計(jì)算領(lǐng)域,內(nèi)存受限系統(tǒng)下,對于小類,大對象實(shí)現(xiàn)方式的主要方面。
對于這類系統(tǒng),小類,大對象,會帶來各方面的幫助:
- 清晰:有助于建立與領(lǐng)域清晰映射的領(lǐng)域模型;
- 彈性:在滿足性能,空間的約束前提下,遵從高內(nèi)聚低耦合的設(shè)計(jì)原則,讓軟件易于理解,易于變化;
- 簡單:在滿足領(lǐng)域特定約束的前提下,降低了諸多偶發(fā)成本。
因此,小類,大對象設(shè)計(jì)模式,成為我們最近幾個(gè)電信產(chǎn)品的設(shè)計(jì)的基石。幾年前,我當(dāng)時(shí)所在的團(tuán)隊(duì),設(shè)計(jì)了一款t-shirt,表達(dá)了小類,大對象對于我們那個(gè)項(xiàng)目的重要性,以及我們對它的喜愛:
