Effective Java Item18 - 復合優先於繼承
May 05, 2018這篇是Effective Java - Favor composition over inheritance章節的讀書筆記 本篇的程式碼來自於原書內容
類似的主題 在設計模式 中也是再三強調 非常重要
Item18: 復合優先於繼承
繼承(Inheritance) 是實現代碼重用的常見方法 但他並非是唯一的工具 用的不好的話 會使你的類別非常脆弱
繼承的一個最大的缺點 就是打破了封裝性 也就是說 子類依賴於父類的實作細節的話 如果某個新版本中父類的實作細節變了 子類也會被破壞 所以當你使用繼承的時候 你就要一直注意你的父類的更新 並且以此決定你需不需要跟著更新
上例子 假設我們想要紀錄一個HashSet總共自從被創建以來 被添加過幾次 我們用一個InstrumentedHashSet去繼承它
因為HashSet有兩個添加函數 add 和 addAll 所以這兩個都要複寫
這個類別最大的問題 在於你的正確性依賴於父類的實作 如果舊的版本的HashSet裡面 addAll就是自己把某個collection的東西加到HashSet裡 那你的子類就會有正確的行為 但若新的版本裡面 他們覺得 addAll其實可以loop這個collection 對於每個元素都呼叫add就好 那你的子類就爆錯了
看一下為什麼 假設你InstrumentedHashSet加了三個元素
InstrumentedHashSet.addAll加了3 之後 呼叫super.addAll 然而父類改變實作 super.addAll呼叫三次super.add super.add呼叫InstrumentedHashSet.add 三次 所以總共是6
父類改變實作是非常常見的事 他也不需要在文檔寫得很清楚他的實作細節 因為你繼承了它 你就要有責任搞清楚他的實作會不會影響到你
解法一: 你可以說 簡單 我就不要複寫addAll就好了 客戶直接呼叫HashSet的addAll這樣就會得到正確的結果了
那也是因為你知道了HashSet的addAll實作依賴了HashSet.add 才會有正確結果 如果他明天又改回去 你的結果又不正確了
解法二: 那你會說 真麻煩 那我的InstrumentedHashSet.addAll就自己遍歷就好 不管HashSet是怎麼實作 我都會對
這個方法有兩個問題
1.你等於是重新實現一次addAll方法 就本末導致 沒利用到繼承的優勢 code reuse
2.你不能存取父類的私有域 有些方法無法實現
解法三: 既然override問題那麼多 我自己加一個新的方法addAllElements總可以吧
世事難料 如果父類的下一個版本有新增一個一樣名字的方法 那就回到剛剛的兩個問題
高譚淪陷 英雄登場
復合(Composition)
我們不去擴展原有的類 而是在新類中加上一個成員(私有域) 這個成員指到舊成員的一個實例
因為舊類變成了新類的一個成員 新類的每一個方法都可以呼叫舊類的任何方法 稱為轉發(forwarding) 新類的方法稱為轉發方法(forwarding method)
用剛剛的用一個新的CountingSet當例子
就是這麼直觀 Effective Java書上的例子過早最佳化 讓讀者難以理解這個主題想要表達的東西 看我的例子一目瞭然
再看一眼我們怎麼宣告這個Class
(這裡當然也可以丟HashSet進去 任何一種Set都可以)
注意 CountingSet其實就是把另一個Set給包裝起來了 你任意給我一個Set 我就回傳另一個多了個新功能的Set 有沒有覺得看起來很眼熟 本部落格的忠實讀者應該已經看出來 其實這個例子就是一個裝飾模式
總結
當你想讓B繼承A的時候 先問自己每個B都確實是A嗎 如果這個問題是不確定的 那就不應該用繼承 應該用復合
即使每個B都確實是A 再問最後一個問題 A是不是沒有缺陷的一個類 如果A有缺陷的話 你願不願意讓你的B有同樣的缺陷 因為繼承會把所有的缺陷都傳播到子類上 但復合可以允許設計新的API來隱藏這些缺陷