小類帅刊,大對象:C++

背景

時至今日,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ī)格是10000Session梳庆,那么Session Instance相關(guān)的內(nèi)存帝簇,會按照規(guī)格,在系統(tǒng)啟動時就預先分配10000Session 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());

從這段示例代碼,我們可以清晰的看出减拭,fg是完全不依賴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接口碳竟,像SingletongetInstance()一樣草丧。因而,上述的實現(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)用語言邑时,往往會提供一個框架,來管理這類工廠職責特姐。比如JavaSpring晶丘。程序員需要做的是:將對象,及對象間的關(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;  
  
};

不難看出里伯,這段代碼的語義如下:

use語義
use語義

并且這樣的實現(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)在幾個方面:

  1. 菱形繼承所帶來的數(shù)據(jù)重復,以及名字二義性蟋字。因此,C++引入了virtual繼承來解決這類問題;

  2. 即便不是菱形繼承扭勉,多個父類之間的名字也可能存在沖突鹊奖,從而導致的二義性;

  3. 如果子類需要擴展改寫多個父類的方法時,造成子類的職責不明涂炎,語義混亂;

  4. 相對于委托忠聚,繼承是一種白盒復用,即子類可以訪問父類的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ù)重復忍级。這分為兩種情況:

  1. 基類數(shù)據(jù)的重復正是每個角色實現(xiàn)的需要。對于每個角色伪朽,它確實需要有自己的一份數(shù)據(jù)拷貝轴咱,即便這些數(shù)據(jù)和另外一個角色是重復的。這些“重復數(shù)據(jù)”在每個角色那里都有自己的不同狀態(tài)烈涮。另外朴肺,由于外部訪問是基于某個具體角色的,所以不會造成二義性問題坚洽。

  2. 如果基類數(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握牧,表達了小類容诬,大對象對于我們那個項目的重要性,以及我們對它的喜愛:

團隊T-Shirt
團隊T-Shirt
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末沿腰,一起剝皮案震驚了整個濱河市览徒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌颂龙,老刑警劉巖习蓬,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纽什,死亡現(xiàn)場離奇詭異,居然都是意外死亡躲叼,警方通過查閱死者的電腦和手機芦缰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來枫慷,“玉大人让蕾,你說我怎么就攤上這事×鹘福” “怎么了涕俗?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長神帅。 經(jīng)常有香客問我再姑,道長,這世上最難降的妖魔是什么找御? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任元镀,我火速辦了婚禮,結(jié)果婚禮上霎桅,老公的妹妹穿的比我還像新娘栖疑。我一直安慰自己,他們只是感情好滔驶,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布遇革。 她就那樣靜靜地躺著,像睡著了一般揭糕。 火紅的嫁衣襯著肌膚如雪萝快。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天著角,我揣著相機與錄音揪漩,去河邊找鬼。 笑死吏口,一個胖子當著我的面吹牛奄容,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播产徊,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼昂勒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了舟铜?” 一聲冷哼從身側(cè)響起叁怪,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎深滚,沒想到半個月后奕谭,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡痴荐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年血柳,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片生兆。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡难捌,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出鸦难,到底是詐尸還是另有隱情根吁,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布合蔽,位于F島的核電站击敌,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏拴事。R本人自食惡果不足惜沃斤,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望刃宵。 院中可真熱鬧衡瓶,春花似錦、人聲如沸牲证。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽坦袍。三九已至十厢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間键闺,已是汗流浹背寿烟。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留辛燥,地道東北人筛武。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像挎塌,于是被迫代替她去往敵國和親徘六。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

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