背景
時至今日,C++
的核心戰(zhàn)場在于:對于性能绩郎,空間和實時性有高要求的系統(tǒng)。
而在這類系統(tǒng)上翁逞,也有其特定的約束和挑戰(zhàn):
在這類系統(tǒng)上肋杖,內(nèi)存管理始終是個需要關(guān)注的問題。而通用內(nèi)存管理算法挖函,要么容易導致內(nèi)存碎片状植,要么會導致內(nèi)存浪費。而為了避免這樣的問題怨喘,最好是自己定義內(nèi)存管理器津畸。
內(nèi)存分配是可能失敗的。為了避免這樣的問題必怜,高可靠系統(tǒng)的做法一般是按照系統(tǒng)定義的規(guī)格預先分配內(nèi)存肉拓。比如,系統(tǒng)承諾的規(guī)格是
10000
個Session
梳庆,那么Session Instance
相關(guān)的內(nèi)存帝簇,會按照規(guī)格,在系統(tǒng)啟動時就預先分配10000
個Session Object
需要的內(nèi)存靠益。其背后的原則是:如果失敗丧肴,則盡早失敗(fail fast
);如果內(nèi)存不足胧后,就應(yīng)該在未發(fā)布時芋浮,通過系統(tǒng)加載就可以及時發(fā)現(xiàn);而不是把風險留到在業(yè)務(wù)現(xiàn)場運行時發(fā)生失敗壳快。對于需要大量銷售的設(shè)備纸巷,成本很重要。而內(nèi)存作為成本構(gòu)成眶痰,也是非常寶貴的資源瘤旨。另外,由于競爭對手的存在竖伯,如何在讓客戶付出相同的成本下存哲,能夠支撐更多的業(yè)務(wù)因宇,始終是一個重要的指標。
對于高性能計算系統(tǒng)祟偷,所有的領(lǐng)域?qū)ο?/strong>察滑,盡管部分數(shù)據(jù)也可能會持久化,但在運行時修肠,都會放在內(nèi)存之中贺辰,與另外一些純粹的運行時狀態(tài)一起,構(gòu)成了運行時領(lǐng)域?qū)ο蟮恼w嵌施。而對于有高可靠性要求的系統(tǒng)饲化,運行時依然會對需要持久化的狀態(tài)進行持久化——那怕只是采用主備方式其實也是一種持久化——所以系統(tǒng)事實上是無狀態(tài)的。
該如何在這類系統(tǒng)上應(yīng)用小類吗伤,大對象的設(shè)計模式吃靠,將是本文隨后討論的內(nèi)容。
委托
回到《小類牲芋,大對象》的文章里的例子撩笆,我們首先角色實現(xiàn)分割到了不同的類中,從而得到了四個類:
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ù)不同對象的需要對這四個對象進行組合夕冲。而首先進入我們視野的,是我們最熟悉的委托(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)系進行組合
ConcreteParent parent;
ConcreteChild child;
ConcreteSubordinate subordinate;
};
毫無疑問裂逐,這是令人生厭的中間人(MiddleMan
)實現(xiàn)(參見《重構(gòu)》)歹鱼。
另外,TypeAPerson
將自己作為一個整體展現(xiàn)給了所有客戶卜高;而不同客戶真正需要的卻是不同的角色弥姻,因而,無論從依賴范圍角度掺涛,還是依賴穩(wěn)定度角度庭敦,都無疑增大了系統(tǒng)的耦合度(參見《變化驅(qū)動:正交設(shè)計》)。
因而薪缆,按照我們最初的意圖秧廉,如下的實現(xiàn)方式才是我們真正想要的:
struct TypeAPerson
{
Parent& getParent()
{
return parent;
}
Child& getChild()
{
return child;
}
Subordinate& getSubordinate()
{
return subordinate;
}
private:
// 通過委托關(guān)系進行組合
ConcreteParent parent;
ConcreteChild child;
ConcreteSubordinate subordinate;
};
TypeAPerson
僅僅應(yīng)該聚合(組合)其所需的角色實現(xiàn),其唯一的職責是當做一個角色工廠拣帽,面對不同的客戶疼电,將對象轉(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
也實現(xiàn)了相關(guān)角色的話修陡,它也可以和f
沧侥,g
配合。如下:
struct TypeBPerson
{
Parent& getParent()
{
return parent;
}
Subordinate& getSubordinate()
{
return subordinate;
}
private:
// 通過委托關(guān)系進行組合
ConcreteParent1 parent; // 與TypeAPerson的Parent實現(xiàn)不同
ConcreteSubordinate subordinate;
};
// object
TypeBPerson person2;
// cast to Parent role
f(person2.getParent());
// cast to Subordinate role
g(person2.getSubordinate());
通過這個例子濒析,我們可以清晰的看出:將上帝類根據(jù)自己的上下文需要正什,分拆成多個角色類的好處:
- 客戶代碼僅僅依賴的自己所需要依賴的角色啥纸,而不關(guān)心提供角色服務(wù)的對象号杏,這解開了客戶與具體對象之間的耦合; 這不僅縮小了依賴范圍斯棒,也讓客戶向著穩(wěn)定的方向依賴盾致;
每個對象自身的不同角色實現(xiàn)是更加高內(nèi)聚,更加單一職責荣暮。角色與角色之間的耦合也從上帝類那種缺乏封裝庭惜,從而更容易導致高耦合的方式中解脫出來。讓各個角色代碼都更加正交穗酥;
不同對象間的角色护赊,如果實現(xiàn)是相同的,可以直接復用砾跃,這讓代碼復用更加容易骏啰;
對象對多重職責的實現(xiàn)更加簡單,只需要通過一個承擔角色工廠職責的類來實例化對象抽高。
Can We Do Better?
從委托的實現(xiàn)方式看判耕,已經(jīng)基本上達到了我們的意圖。
但是翘骂,我們也注意到:我們不得不讓TypeAPerson
提供一組get
接口壁熄,來暴露自己實現(xiàn)的角色。這些get
接口碳竟,像Singleton
的getInstance()
一樣草丧。因而,上述的實現(xiàn)模式也是一種創(chuàng)建者模式莹桅。這也正是我們將其稱之為角色工廠的原因昌执。
那么,是否還有更為簡便的方法來實現(xiàn)角色工廠的職責统翩?
我們不難發(fā)現(xiàn):TypeAPerson
與它所承擔的角色之間存在IS-A
關(guān)系仙蚜;而且,由于TypeAPerson
沒有任何業(yè)務(wù)邏輯代碼厂汗,從而也沒有改寫任何一個父類(角色)的行為委粉,因此這種IS-A
關(guān)系必然是滿足里氏替換原則。那我們?yōu)楹尾辉囋嚩嘀乩^承:
struct TypeAPerson
: ConcreteParent
, ConcreteChild
, ConcreteSubordinate
{
};
這樣的方式娶桦,在組合TypeAPerson
時贾节,明顯比委托的實現(xiàn)方式更為簡潔汁汗。
而在調(diào)用側(cè)使用角色時,由于IS-A
這種關(guān)系的存在栗涂,其角色轉(zhuǎn)換也可以自動完成知牌,從而也更為簡潔:
// object
TypeAPerson person1;
// naturally cast to Parent role.
f(person1);
// naturally cast to Subordinate role.
g(person1);
當然,多重繼承的實現(xiàn)方式斤程,相對于委托方式角寸,還是存在一點缺點:你無法阻止ParentAPerson
可以向ConcreteParent
這個更具體的角色轉(zhuǎn)化;但在委托方式下忿墅,由于ParentAPerson
的具體組合的對象都作為了私有實現(xiàn)細節(jié)扁藕,然后通過getter
這種更有彈性的函數(shù)方式,將具體的角色實現(xiàn)疚脐,比如ConcreteParent
亿柑,轉(zhuǎn)化為更抽象的角色Parent
;從而具備更好的封裝性棍弄。
對于這類問題望薄,其解法有二:一則可以通過語言提供的機制進行強制約束,二則通過人為的約定呼畸。多重繼承的方式痕支,在C++
里沒有強制禁止向一個代表某種具體實現(xiàn)的父類進行轉(zhuǎn)換的手段,但站在便利性的角度役耕,我們更傾向于選擇通過人為的約定采转。畢竟我們清晰的知道我們的設(shè)計意圖是什么。
角色依賴
在之前的例子中瞬痘,為了突出要點故慈,給出的各個角色的實現(xiàn)都是孤立的。但在實際項目中框全,角色之間存在依賴關(guān)系察绷,也是一種常見的現(xiàn)象。
比如津辩,在我們的例子中拆撼,TypeBPerson
的下屬角色的某個實現(xiàn),需要調(diào)用上司角色所提供的服務(wù)喘沿。如下圖所示:
這個難不倒我們闸度,我們可以讓下屬角色持有一個指向上司角色的指針,然后在構(gòu)造下屬角色時進行依賴注入蚜印。如下是一種委托方式的實現(xiàn):
struct ConcreteUnderling : Underling
{
ConcreteUnderling(Boss& boss)
: boss(boss)
{}
void acceptTask()
{
boss.assignTask();
// ...
}
// ...
private:
Boss& boss; // 引用會消耗一個指針寬度的內(nèi)存
};
struct TypeBPerson
{
TypeBPerson()
: underling(boss)
{}
Boss& getBoss()
{
return boss;
}
Underling& getUnderling()
{
return underling;
}
private:
ConcreteBoss boss; // 確保 Boss 在 Underling 之前
ConcreteUnderling underling;
// ...
};
這樣的實現(xiàn)莺禁,存在如下問題:
首先,需要確保不同角色的構(gòu)造順序窄赋,一旦角色Underling
依賴了角色Boss
哟冬,那么楼熄,在TypeBPerson
里,就最好確保boss
定義在Underling
之前浩峡,以免由于構(gòu)造順序所造成的調(diào)用問題可岂。
其次,一旦一個角色引用了另外一個角色翰灾,那就需要通過引用進行依賴注入缕粹。這會增加由于引用所消耗的內(nèi)存,對象間的關(guān)聯(lián)越多预侯,那么指針消耗的空間就越大致开。
尤其是當我們追求高內(nèi)聚峰锁,低耦合的設(shè)計時萎馅,伴隨而生的是很多很小單一職責的類,類與類之間會通過引用進行職責委托虹蒋。這對于那些內(nèi)存不是一個重要問題的系統(tǒng)而言糜芳,或許并不重要。在內(nèi)存珍貴的嵌入式設(shè)備上魄衅,這會是一個問題峭竣。
而這個問題,也會反過來約束C++
程序員即便知道高內(nèi)聚晃虫,低耦合是正確的皆撩,在內(nèi)存約束面前,也只能采取更糟糕的實現(xiàn)方式哲银。
How About Inheritance?
對于角色關(guān)聯(lián)所導致的問題扛吞,換成多重繼承也不會讓情況變得更好:
struct TypeBPerson
: ConcreteBoss // 確保 Boss 在 Underling 之前
, ConcreteUnderling
,
{
TypeBPerson()
: ConcreteUnderling(*this)
{}
};
這兩種實現(xiàn),差別在于從成員變量便為繼承荆责,而不變的是:都要注意聲明順序滥比,都會造成由引用帶來的內(nèi)存消耗。
工廠方法
幸運的是做院,相對于委托盲泛,通過繼承,我們可以擁有更多的武器键耕。
對外部的依賴寺滚,在繼承體系下,我們可以通過著名的工廠方法來引入屈雄,而不是通過經(jīng)典的構(gòu)造時依賴注入方式村视。比如:
struct ConcreteUnderling : Underling
{
void acceptTask()
{
getBoss().assignTask();
// ...
}
private:
// 通過工廠方法引入依賴
virtual Boss& getBoss() = 0;
};
struct TypeBPerson : ConcreteBoss
, ConcreteUnderling
{
Boss& getBoss() override
{
return *this;
}
};
通過這樣的方法,首先解決了角色構(gòu)造順序的問題棚亩。因為蓖议,一個角色對于另外一個角色的引用虏杰,只有到整個對象構(gòu)造結(jié)束后,運行時才會進行獲取勒虾。當然你需要避免任何在構(gòu)造函數(shù)里對于其它角色的引用纺阔,而事實上,根據(jù)多個項目的實踐修然,這種構(gòu)造時引用關(guān)系都可以合理的避免笛钝。
內(nèi)存優(yōu)勢
但這個例子還不能彰顯工廠方法的內(nèi)存優(yōu)勢。讓我們換個例子:
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
需要消耗三個引用的空間開銷玻靡。
現(xiàn)在將其換成基于工廠方法的實現(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
中贝,無論其對外部有多少個角色引用囤捻,都只需要耗費一個指針內(nèi)存的開銷,那就是虛表指針(vptr
)邻寿。如果Role1
本來就存在其它virtual
函數(shù)蝎土,那么這些外部引用,無論存在多少绣否,都沒有增加任何額外的空間開銷誊涯。
簡化工廠管理成本
我們知道,按照高內(nèi)聚蒜撮,低耦合的實現(xiàn)方式暴构,會導致一堆小類。如果不用小類段磨,大對象的方式取逾,而是讓一個個小類可獨立實例化。那么這些小類之間如果存在引用關(guān)系薇溃,一則需要更多的內(nèi)存消耗菌赖,二則,你還不得不需要寫很多工廠來對這些小對象進行構(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);
}
}
當對象種類很多時琉用,這樣的承擔工廠職責的代碼就會很多。而這類的代碼是極其無趣而令人厭煩的策幼。
當然對于其它應(yīng)用語言邑时,往往會提供一個框架,來管理這類工廠職責特姐。比如Java
的Spring
晶丘。程序員需要做的是:將對象,及對象間的關(guān)聯(lián)關(guān)系,通過xml
配置文件進行描述浅浮,Spring
框架會根據(jù)這個配置文件來履行工廠職責沫浆。
可是,這樣的方法在嵌入式C++
下不是一個可行的途徑滚秩,程序員們還是不得不親自去實現(xiàn)专执。
而在小類,大對象的實現(xiàn)模式下郁油,固定模式的工廠方法就完成了這些本股,程序員不會比Java
下用XML
進行配置,需要付出的努力更多桐腌。
struct Object
: Role1
, Role2
, Role3
, Role4
{
// 這些工廠方法即屬于工廠職責的實現(xiàn)
Role2& getRole2() override { return *this; }
Role3& getRole3() override { return *this; }
Role4& getRole4() override { return *this; }
};
Once For All
更妙的是拄显,在一個對象上,無論一個角色被多少其它角色引用案站,最后都只需要實現(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:
// 只實現(xiàn)一次,就滿足了Role1嚼吞,Role2盒件,Role3的需要。
Role4& getRole4() override { return *this; }
};
即便對于一個來自于外部的角色舱禽,也是如此:
struct Object
: Role1
, Role2
, Role3
{
// Role4是External Role,因而需要在對象構(gòu)造時注入
Object(Role4& role4) : role4(role4) {}
private:
// 只實現(xiàn)一次恩沽,就滿足了Role1誊稚,Role2,Role3的需要罗心。
Role4& getRole4() override { return role4; }
Role4& role4;
};
更清晰的依賴語義描述
使用工廠方法來表達依賴的例子如下:
struct Role1
{
void f()
{
// ..
// 對依賴的引用
getRole2().doSth();
// ...
getRole3().blah();
// ...
}
private:
// 對依賴的聲明
virtual Role2& getRole2() = 0;
virtual Role3& getRole3() = 0;
};
不難看出里伯,這段代碼的語義如下:
并且這樣的實現(xiàn)方式也是完全模式化的,因而我們定義如下兩個宏:
#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)疾瓮,一個角色,通過USE_ROLE
語義飒箭,僅僅聲明自己對另外一個角色的依賴狼电,卻完全無需關(guān)心這個角色的實現(xiàn)來自何處,也完全無需關(guān)注誰會注入給它弦蹂。這實現(xiàn)了與經(jīng)典依賴注入方式完全相同的語義肩碟,達到了完全相同的解耦效果。
而對于工廠的實現(xiàn)凸椿,同樣也有明確的模式和清晰的語義:
struct Object
: Role1
, Role2
, Role3
, Role4
{
// 這些工廠方法即屬于工廠職責的實現(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ù)我們的項目經(jīng)驗密末,這些性能優(yōu)勢微乎其微握爷。但如果在你的項目中,經(jīng)過事后測量严里,確實發(fā)現(xiàn)熱點處可以通過直接引用提升性能新啼,那就可以在那個點,將工廠方法刹碾,改為直接引用的方式燥撞。而這個改動,并不困難迷帜。
繼承樹倒置
當使用單根繼承時物舒,如果子類沒有任何代碼,這樣的繼承是沒有太多意義的戏锹。比如:
struct Base
{
void foo();
void bar();
private:
int a;
int b;
};
// 這樣的繼承冠胯,由于子類沒有任何代碼,
// 如果不是出于某些特定的目的,是沒有任何意義的锦针。
struct Derived : Base
{};
但是荠察,當使用多重繼承時,子類沒有任何實現(xiàn)代碼奈搜,卻表達了一個非常有價值的語義:組合悉盆。
TypeAPerson
有效的將多個類的數(shù)據(jù)和行為都組合到一個對象上。最重要的是馋吗,這個沒有任何實現(xiàn)代碼的子類焕盟,恰恰是我們設(shè)計時所追求的單一職責 —— TypeAPerson
的唯一職責是:將所有角色組合到一個對象身上。
這樣的設(shè)計是一種以組合的方式宏粤,最終聚合到單個對象類脚翘。它和經(jīng)典的單根繼承方式所導致的繼承樹正好相反。因而商架,我們也稱它為繼承樹倒置模式堰怨。下圖是來自于一個項目的例子:
繼承優(yōu)于委托
通常在設(shè)計中,我們得到的建議往往是:委托優(yōu)于繼承蛇摸。其原因在于:委托是黑盒復用备图,而繼承是一種白盒復用。
但正如我們之前討論的,在多角色對象的實現(xiàn)中揽涮,最終的對象類抠藕,沒有任何業(yè)務(wù)實現(xiàn)代碼,因此不會對父類產(chǎn)生任何實現(xiàn)上的依賴蒋困。角色類的所有實現(xiàn)盾似,對對象類類而言,在邏輯上和一個黑盒無異雪标。
而反過來零院,繼承式組合,相對于委托式組合村刨,至少有如下優(yōu)勢:
- 簡化了組合方式告抄;
- 大大降低了內(nèi)存開銷;
- 消除了角色構(gòu)造順序問題嵌牺;
- 大大簡化了依賴管理問題打洼;
- 對象到角色的自動轉(zhuǎn)換;
因而逆粹,在多角色對象的場景下募疮,繼承式組合要優(yōu)于委托式組合。
為何這樣的多重繼承不邪惡
過去僻弹,多重繼承在面向?qū)ο笊鐓^(qū)內(nèi)一直頗有爭議阿浓。大多數(shù)書籍都會建議:盡量避免使用多重繼承,要謹慎的使用多重繼承奢方。于是多重繼承就逐漸變?yōu)槌绦騿T唯恐避之不及的東西搔扁。
多重繼承的邪惡之處主要體現(xiàn)在幾個方面:
菱形繼承所帶來的數(shù)據(jù)重復,以及名字二義性蟋字。因此,
C++
引入了virtual
繼承來解決這類問題;即便不是菱形繼承扭勉,多個父類之間的名字也可能存在沖突鹊奖,從而導致的二義性;
如果子類需要擴展或改寫多個父類的方法時,造成子類的職責不明涂炎,語義混亂;
相對于委托忠聚,繼承是一種白盒復用,即子類可以訪問父類的
protected
成員, 這會導致更強的耦合唱捣。而多重繼承两蟀,由于耦合了多個父類,相對于單根繼承震缭,這會產(chǎn)生更強的耦合關(guān)系赂毯。
但我們看看TypeAPerson
,它沒有任何代碼。因而它沒有操作任何父類的數(shù)據(jù)和方法党涕,所以烦感,第3
點和第4
點的缺點并不存在。
關(guān)于第2
點所描述的二義性問題膛堤,這需要從兩個方面來看:子類的內(nèi)部和外部手趣。
從子類內(nèi)部的角度,由于無需訪問父類肥荔,所以绿渣,多個父類之間即便存在名字沖突,在子類內(nèi)部也不會造成二義性問題燕耿。
而從子類外部來看中符,如果直接通過子類的實例來調(diào)用成員函數(shù),這種二義性確實可能存在缸棵。但對于一個多角色對象舟茶,所有外部訪問都應(yīng)該是基于角色的。而對于每個角色堵第,名字的對應(yīng)關(guān)系是明確的吧凉,沒有任何二義性。所以踏志,多角色對象特定的訪問模式阀捅,決定了在外部也不會造成二義性。
至于第1
點针余,菱形繼承帶來的兩個問題:數(shù)據(jù)重復和二義性饲鄙。
我們首先應(yīng)該避免不符合我們需要的菱形繼承。
對于由設(shè)計而自然產(chǎn)生的菱形繼承圆雁,我們無需使用virtual
繼承來避免數(shù)據(jù)重復忍级。這分為兩種情況:
基類數(shù)據(jù)的重復正是每個角色實現(xiàn)的需要。對于每個角色伪朽,它確實需要有自己的一份數(shù)據(jù)拷貝轴咱,即便這些數(shù)據(jù)和另外一個角色是重復的。這些“重復數(shù)據(jù)”在每個角色那里都有自己的不同狀態(tài)烈涮。另外朴肺,由于外部訪問是基于某個具體角色的,所以不會造成二義性問題坚洽。
如果基類數(shù)據(jù)是共享的戈稿,那也不應(yīng)該使用
virtual
繼承,而是通過委托關(guān)系來共享數(shù)據(jù)讶舰。這樣鞍盗,就可以更加合理的避免數(shù)據(jù)重復需了。
至于行為重復,由于角色與角色之間的需求是不應(yīng)該重疊的橡疼。所以援所,對于同一個對象,很難出現(xiàn)兩個角色之間有相同的行為子集欣除。如果出現(xiàn)住拭,則說明這兩個角色的職責都不單一。將重疊的行為子集定義為一個新的角色历帚,是一個更合理的設(shè)計選擇滔岳。
綜上所屬,對于多角色對象而言挽牢,這種組合方式不會從實質(zhì)上帶來多重繼承所引起的任何問題谱煤。
簡化內(nèi)存管理成本
在開篇時,我們已經(jīng)提到:很多通信設(shè)備禽拔,為了避免內(nèi)存管理所導致的問題:比如碎片化刘离,浪費,以及運行時內(nèi)存分配失敗睹栖,會對領(lǐng)域?qū)ο?/strong>自定義自己的內(nèi)存管理器硫惕,并在系統(tǒng)加載時,就會預先分配所需的所有內(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)聚低耦合的方式而導致了很多小對象,就不得不為每個小對象都定義自己的內(nèi)存管理器曼氛,并且要按照其最大數(shù)量來預先分配豁辉,這會隨著小對象種類的增多,而大大加重內(nèi)存管理的負擔舀患。
另外徽级,在高性能計算領(lǐng)域,為了降低cache miss rate聊浅,一般的做法是將關(guān)聯(lián)訪問數(shù)據(jù)都盡可能的放在一起灰追。如果分隔為很多小對象,它們都從不同的內(nèi)存區(qū)域進行存放的話狗超,對于性能會造成不同程度的負面影響。
而通過大對象的方式朴下,所有的數(shù)據(jù)最后都聚集在一個對象身上努咐,它們的內(nèi)存是連續(xù)的。這對于性能及性能優(yōu)化都有幫助殴胧。
其它問題
數(shù)據(jù)該怎樣存放
數(shù)據(jù)按照高內(nèi)聚渗稍,低耦合的原則佩迟,歸屬于各個不同的角色。然后竿屹,角色間根據(jù)需要报强,引用并訪問對方的接口仑荐。
私有角色
有些角色屈嗤,純粹是因為一個對象自身的需要炬丸,并不需要公開給外部沛鸵,則可以通過private
繼承(參見《The Virtues Of Bastard》)進行組合磷醋。其它角色對它的引用涌乳,依然通過USE_ROLE(Role)
的方式獲取悉默。
總結(jié)
本文介紹了使用C++
曲梗,在高性能計算領(lǐng)域哮缺,內(nèi)存受限系統(tǒng)下弄跌,對于小類,大對象實現(xiàn)方式的主要方面尝苇。
對于這類系統(tǒng)铛只,小類,大對象糠溜,會帶來各方面的幫助:
- 清晰:有助于建立與領(lǐng)域清晰映射的領(lǐng)域模型淳玩;
- 彈性:在滿足性能,空間的約束前提下诵冒,遵從高內(nèi)聚低耦合的設(shè)計原則凯肋,讓軟件易于理解,易于變化汽馋;
- 簡單:在滿足領(lǐng)域特定約束的前提下侮东,降低了諸多偶發(fā)成本。
因此豹芯,小類悄雅,大對象設(shè)計模式,成為我們最近幾個電信產(chǎn)品的設(shè)計的基石铁蹈。幾年前宽闲,我當時所在的團隊,設(shè)計了一款t-shirt
握牧,表達了小類容诬,大對象對于我們那個項目的重要性,以及我們對它的喜愛: