天天看點

面向對象随想 -- 多用組合來代替繼承

讓我們先來簡單看看什麼是繼承群組合。隻是例子,不是概念。

假設有三個特性(子產品), ModuleA, ModuleB, ModuleC。有兩個類 ClassA 和 ClassB 需要使用它們。ClassA 會用到 ModuleA 和 ModuleB,ClassB 會用到全部三個特性。而且我們還假設現在全部A,B和C子產品會隻被ClassA和ClassB用到。

繼承:

class ClassA {

functions of ModuleA;

functions of ModuleB;

};

class ClassB extends ClassA {

functions of ModuleC;

};  

組合:

class ModuleA {};

class ModuleB {};

class ModuleC {};

class ClassA {

ModuleA ma;

ModuleB mb;

};

class ClassB {

ModuleA ma;

ModuleB mb;

ModuleC mc;

};

//或者:

class ClassB {

ClassA ca;

ModuleC mc;

};

兩種方法都重用了子產品。

對于組合,我們一般把子產品叫做元件。

在遊戲開發中,這樣的組合又叫做基于元件的實體系統。

組合比繼承有哪些優勢?

  1. 組合允許将問題分割成互不依賴的子元件。每個元件可以由不同的開發者開發。我們可以讓三個程式員來同時開發 ModuleA, ModuleB 以及 ModuleC。
  2. 元件可以在運作時被替換,删除,或者添加(動态綁定)。與之相反,繼承不能或者非常難(取決于程式設計語言)在運作時替換,删除,或者添加特性(靜态綁定)。
  3. 組合比繼承有更少的耦合。繼承強制了 ClassB 和 ClassA 之間的耦合,但組合不會。
  4. 元件可以被用在其它的組合當中用以構成不同的行為。在繼承中則沒有辦法隻重用一個單個特性。繼承強制了父類的所有特性被帶入到了子類。
  5. 更少的代碼備援。繼承比較容易導緻備援。B 繼承自 A,通常 B 隻需要 A 的部分特性,不需要的部分就成了備援。如果 B 是由 A 的部分元件組合來的,那麼 B 可以舍棄不需要的元件來避免備援。
  6. 更好的封裝。組合是基于公開接口的。每個元件不知道其它元件的内部細節。這正是封裝所指的。而繼承則把父類的内部細節(保護的接口)透露給了子類。更糟的是,子類通常會對父類做某些假設,而且父類也會假設子類會尊重它要求的接口。
  7. 容易更改。在組合中,任何元件都可以被更改而保證對其它元件影響很小,隻要保持公開的接口。繼承則強制了很緊密耦合的層次鍊。對于層次鍊任何節點的改變都将影響整個鍊。
  8. 小子產品和原子子產品。組合中,大的子產品是由小的子產品組成的,是以子產品的層次可以得到控制。有可能我們隻有兩個層次,原子子產品,和真實的功能子產品,因 而層次非常平坦。在繼承中,大的子產品來自從小的子產品繼承。想要一個子產品?繼承吧,得到一個新的層級。這就可能造成有很多的層級,導緻系統的子產品層次非常複 雜且難以了解。

對于 "is a" 關系我們應該用繼承?是,也不是。

雖然我們被教導繼承要 "is a" 組合要 "has a",在很多情況下 "is a" 也可以被了解為 "has a"。比如說,程式員是一種人,是以程式員從人繼承。但如果我們把程式員當成是一種具有程式設計工作的人,那麼程式員就可以由人和程式設計工作來組合。

組合的不足之處:

唯一的不足之處大概就是我們最終會得到非常多的子產品(類)。這個很難說是不足,而且從其它角度看,這個其實是益處。對于C++,大量的小類完全沒有影響, 不會讓代碼變大,也不會讓運作變緩慢。對于 Java,C#,Objective C,這可能會增大代碼。但除非你是工作在資源有限的裝置上,這個不是大問題。

何時用組合:

  1. 當 ClassB 使用 ClassA 而不是實作 ClassA 時。
  2. 當元件會被改變時(添加,替換,删除)。
  3. 即使 ClassB 和 ClassA 具有 "is a" 的關系,如果這種關系在未來會發生變化,我們應該用組合而不是繼承。一個程式員是人,但未來他可以選擇别的工作,比如股票經紀。如果程式員是從人繼承而 來,那麼當工作變化時我們就比較麻煩。是以用組合。程式員隻是一個普通的人,一個有着程式設計工作的普通人。
  4. 當繼承不是必須的時候。沒錯,我們要多用組合來代替繼承。

何時用繼承:

  1. 當 ClassB 是實作 ClassA 而不是使用 ClassA。比如說,ClassA 可能是具有多态函數的抽象對話框,有着顯示隐藏等函數。ClassB 是一個檔案對話框需要實作這些多态函數。
  2. 當 ClassB 和 ClassA 有明顯的 "is a" 關系而且這種關系不會在運作時改變時。也就是說,綁定永遠是靜态的。雖然貓可以被當成是一個具有動物特征和貓特征的物體,但這兩個特征永遠不會改變。把貓 特征換成狗特征會産生狗而不是貓。是以這種綁定是靜态的,關系也是固定的 "is a",是以讓貓從動物繼承是很自然的。
  3. 當 ClassA 的接口也是 ClassB 的接口時。這個其實很難講。如果用組合實作其實也可以,ClassB 可以重寫代理接口來重定向到 ClassA 上。
  4. 當 ClassB 會被用到使用 ClassA 的地方時。這個也暗喻了 ClassB "is a" ClassA。 然而,對于支援類型多态(泛型程式設計)的語言,比如 C++,可以通過用模闆類型來避免繼承。

總結:

我展示了我們應該多用組合而不是繼承。而且如果你 google "composition inheritance" (無引号),上百萬的網頁會支援這個觀點。但這絕不是說在組合對繼承的鬥争中組合是赢家。不,而且根本沒有鬥争。對于适當的問題我們要選擇适當的方法,保 證自然和邏輯性。沒有什麼是絕對的好或者壞,對或者錯。

繼續閱讀