---
title: "組合和繼承"
date: "2026-03"
category: "設計模式與架構"
tags: ["設計模式與架構"]
description: "組合優於繼承的關鍵因素是去耦合，或者説，就像積木一樣的模組，而狀態則通過外部因素給入——大部分情況下采用靜態方法就足夠了，但少數情況下可能需要的是一個組合類，下面許多方法共享同樣的狀態——但是..."
source: "https://enriquemark.com/zh-hant/posts/%E7%B5%84%E5%90%88%E5%92%8C%E7%B9%BC%E6%89%BF"
---

組合優於繼承的關鍵因素是去耦合，或者説，就像積木一樣的模組，而狀態則通過外部因素給入——大部分情況下采用靜態方法就足夠了，但少數情況下可能需要的是一個組合類，下面許多方法共享同樣的狀態——但是，我為什麼不採用完全的DI，讓狀態從外部注入呢？也就是說，更精確的來講：只有所有組合方法都需要共享同樣的單一狀態時，我才需要用類——哪怕這個狀態本身也還是從類外面注入進來的，區別就是一個是DI方法，一個是DI類。對於一般的情況來説，DI方法就夠了。

而組合本身也一直強調，什麼時候用它？[組合](/zh-hant/posts/组合) 裡面提到「有一個」和「是一個」的區別，類本身是字面意義上的「一類」，換言之，他們是一個物種——共享結構上固定的（就像上面所説的）、提前定義好的父類狀態（屬性）或方法。子類確實可以寫，但那也是父類允許的情況，子類本身的範圍和邊界被父類限制。這裡和介面不同的點是，介面雖然也是對結構的約束，但它更專注於結構本身的抽象，而父類是更底一層的抽象，它除了結構之外，還有具體的行為——有具體內容的方法。而這些具體正是保證里氏替換原則（LSP）的基礎——任何子類都可以替代父類，因為他們本身就完全包含（繼承）了父類的一切內容。多餘的那部分只是沒被使用而已。

但是，標準的LSP不是硬約束，假設底層以來了父類的「可覆寫」方法，而子類完全覆寫了，那就會炸。這也是為啥濫用繼承反而容易耦合——不符合LSP原則的繼承就不該出現——或者説，父類應該約定好，其可覆寫方法是「擴展性」的——而且這些擴展部分，本身就可以作為一個約定而存在了，比如叫什麼名字、輸入輸出是什麼，只是邏輯內部可以讓你隨便寫。從這裡看，就會發下轄，哪怕是擴展，設計良好的父類也能保證LSP能運作，因為結構（輸入-輸出）是型別固定的，正常來説哪怕是擴展，用的時候都不會出問題。從一開始就應該保證子類再繼承的時候不會破壞核心功能，這些就是保證LSP的核心骨架邏輯。但是，即便如此，這也是結構約束，萬一子類就是胡搞，導致方法直接出錯，返回null（返回型別系統預設不管這個），或者搞了預期的副作用，那還是會出問題。畢竟你不可能窮盡一切，只要開了擴展點，那什麼牛鬼蛇神都可能出來。最大化保證LSP是可以做到的，但這也是繼承濫用做不到的地方。

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

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

而且這麼來説，組合本身也是可以和繼承結合使用的——比如一個大型的工具模組——就算是樂高也分各種型別，你是磚塊我是小人，小人之見可以隨便拼，但它（一般而言）也不會和磚塊結合。那這時，功能上的一系列的「有一個」，就要遵循「是一個」的模式來構建自身了。哪怕他們整體仍然是「有一個」的模組，但自成一脈。
