EnriqueMark
設計模式與架構

組合和繼承

2026-03 → 2026-09 更新 繁體中文

組合優於繼承的關鍵因素是去耦合,或者説,就像積木一樣的模組,且應當盡可能的消除外部的狀態和副作用。理想的組合,彼此應當是原子的。只對自身的內部負責,而不關心外部。但類就不一樣了,繼承本身就暗示了相反的東西,它的關鍵點在於「狀態和行為的共享」——這會造成無形間的耦合,這是代價,但在某些時候確實是需要的。這裡我們可以從何時用組合看出來。什麼時候用它?組合裡面提到「有一個」和「是一個」的區別,類本身是字面意義上的「一類」,換言之,他們是一個物種——共享結構上固定的(就像上面所説的)、提前定義好的父類狀態(屬性)和方法(實現的具體行為)。

而我們所説的耦合,指的就是這種狀態共享上帶來的隱藏關聯。父類和子類共享一個可變的狀態,但它對下面的繼承者擴展了什麼是無知的,換言之它無法預料子類到底依賴了自己的哪個部分,也許你更改了一個無關緊要的東西,但遠在天邊的另一個功能壞了;或者反過來,任何一個子類改了祖父類的狀態,使用這個狀態的他自己的父類就可能出問題,等等諸如此類——這就是隱式依賴,順著整個鏈條傳導的後果。

正常來説哪怕是擴展,用的時候都不該出問題,如果約定良好,確保繼承時的紀律和測試覆蓋,的確可以讓繼承鏈出問題時不至於出生產事故。但是,我為什麼非要搞得這麼耦合呢?萬一子類就是胡搞,導致方法直接出錯,或者搞了非預期的副作用,只要你是關聯的,那就還是會出問題。畢竟在團隊開發中,每個人都只能看到自己的一個部分,甚至人員流動之下,前後都是彼此不共享資訊的。撰寫父類的人不可能窮盡一切,只要開了擴展點,那什麼牛鬼蛇神都可能出來。

如果我需要專注於狀態和行為的具體實現,且希望後續的依賴方「繼承」(字面意義上)這些內容,那這種情況下用基類和繼承的模式是標準做法,沒什麼冗餘的。畢竟我不能讓人類當一個佛蘭肯斯坦,用一大堆不相干的「積木」去拼湊,就算這樣最終能跑起來,它也會像科學怪人那樣是讓人困惑的——雖然就像上述説的一樣,能動,但我們的印象就是人類有其固有的狀態,這些狀態必須是繼承的而不是拼湊的,人類必須得是「是一個」,所以根本不是標準做法了,而是成了「反模式」。

但有些情況下我們就需要組合:積木、去耦合化的模組。它面向的是「有一個」。我只是想要一個復用的功能,就跟樂高一樣,可以在任何地方拼,也不依賴什麼必然的固定狀態,那組合就再合適不過了。你拼成房子還是直升機我不感興趣,只要你能拼,那你就能用。

而對組合來説,至關重要的一個東西就是「介面」。何為介面?如果説類是專注於狀態和行為的具體實現,那麼介面就是專注於結構本身的約束——它不關心實現,而只關心這個實現應該是什麼形狀的。

從另外一個方面說,組合併不與類排斥。因為組合的靈活性,它的拼裝場所可以發生在任何地方——例如類的裡面。即便是類,我也沒必要把各種工具方法都作為這個類的固有行為塞進去,那隻會造成過於臃腫的方法膨脹。需要的時候直接使用組合工具,為什麼不呢?只要組合能夠確保自己的輸入和輸出是不變的,那麼它對任何使用方也都是明確的。