iT邦幫忙

2025 iThome 鐵人賽

DAY 15
0

https://ithelp.ithome.com.tw/upload/images/20250903/20124462P1N8QjGguI.png

消除你程式碼的臭味 Day 15- 組合優於繼承:用小能力組出行為

物件導向的繼承理論,源自於生物學家林奈的分類法。
界、門、綱、目、科、屬、種。Dog is-an Animal

這在上課時候聽好像不錯,但用在軟體開發中,這套思想可能會讓你崩潰。

為什麼?
因為你想要的通常不是一整隻「動物」,你只是想要它「會叫」這個行為。
這就是著名的「香蕉-猩猩問題」:你想得到一根香蕉,結果卻得到一隻拿著香蕉的大猩猩,以及整片叢林。

繼承讓你被綁死在一個僵化的家族樹上。
組合則給你一盒樂高積木,讓你自由地拼裝出你需要的任何東西。

先做小能力,再用組合把它們接起來。

組合優於繼承

什麼是組合?

  • 用多個小而單一的能力(函式或小物件)拼接出需要的行為。
  • 每個能力彼此獨立,可替換、可測試。

什麼時候選組合而非繼承?

  • 需要選配能力,而不是固定階層。
  • 行為會變動或疊加,不適合硬編在父類。
  • 想避免菱形繼承、方法覆寫地獄與隱性耦合。

經典案例:為了改一個方法就繼承

// 🔴 臭味開始:你建立了一個無法輕易擺脫的血緣關係
class Animal { 
  breathe() {  ...  }
  eat() {  ... }
  speak(){ return '...'; } 
}

class Dog extends Animal { 
  speak(){ return 'woof'; } 
}
  • 它很難用: 想建立一個「玩具狗」(ToyDog)。它也會叫,但它不會呼吸、不會吃東西。要怎麼辦?
    ToyDog 也繼承 Animal,然後覆寫掉 breatheeat 讓它們丟出例外?這方法不行,在分類體系遇到第一個現實世界的例外時,就已經發臭了。

  • 它造成了緊耦合: Dog 現在與 Animal 的所有內部實作都是緊耦合。
    如果 Animal 的某個基礎方法(breathe)改變了,所有繼承它的子類都可能在你意想不到的地方壞掉。這叫「脆弱基底類別問題」。

  • 你得到了一整隻猩猩: 當你只需要 speak 這個「香蕉」時,繼承把 breatheeat 這些你可能根本不需要的東西(猩猩和叢林)也一併塞給了你。

https://ithelp.ithome.com.tw/upload/images/20250917/20124462bsxHGOIajq.png

組合你真正要的行為

像玩樂高一樣組裝你的物件與行為。

https://ithelp.ithome.com.tw/upload/images/20250917/20124462YtHOrROndr.png

// 🟢 好味道:這不是繼承,這是組裝。

// 這些是你的樂高積木:一個個獨立的能力。
const canSpeak = (saying) => ({ speak: () => saying });
const canMove = (speed) => ({ move: () => `moves at ${speed}` });
const canBreathe = () => ({ breathe: () => 'breathing...' });

// 這是一個「組裝線」或「工廠」。
function createDog() {
  // 按需取用你需要的積木,把它們組合起來。
  return {
    ...canSpeak('woof'),
    ...canMove('fast'),
    ...canBreathe()
  };
}

function createToyDog() {
  // 玩具狗只需要「叫」的能力。很好,只給它這個。
  return {
    ...canSpeak('electronic woof')
  };
}

const realDog = createDog();
const toyDog = createToyDog();
  • 依賴更單純: 組合讓你徹底擺脫了分類的束縛。把對應的能力組裝起來就行了。

  • 零耦合: canSpeakcanMove 這兩個能力之間,以及它們和最終的物件之間,沒有任何耦合。你可以獨立修改、測試、替換任何一個能力,而不會影響到其他部分。

重構方法

  1. 找出變化點,打包成能力:首先,找到那些在不同子類中一直被修改的方法,把它們抽出來,變成一個個獨立的「能力函式」。

  2. 用工廠取代 new:接著,建立一個專門的「工廠」或「產生器」。它的任務就是像組裝樂高一樣,把需要的能力組合起來,回傳最終的物件。這樣就不再需要 new Subclass() 這種寫法。

  3. 新舊並行,安全過渡:先讓新的組合路徑和舊的繼承路徑同時存在。觀察一段時間,確認兩者輸出的結果完全一致後,再放心移除舊的繼承程式碼。

檢查清單

在手癢想打出 extends 關鍵字之前,停下來問自己:

  1. 我需要的是一個嚴格的「is-a」父子關係(會繼承父層所有),還是一個「has-a」或「can-do」的能力工具箱?(99% 的情況下,是後者)。

  2. 我能否把這個「行為」抽成一個獨立的、可被任何物件使用的零件?

  3. 如果我用了繼承,未來出現一個不完全符合父類別定義的例外情況時,該怎麼辦?

今日重點

  • 優先採用組合,而非繼承 (Favor Composition over Inheritance)。

  • 行為模組化 (Modularize Behaviors): 將複雜的行為分解為獨立、可重用的「能力單元」,在需要時動態組合。

  • 提升程式碼品質: 降低耦合,提升替換與測試性。

用「組合」取代不必要的「繼承」,你不是在寫程式,你是在設計一個更除臭的生態系。


上一篇
Day 14- 複雜判斷:抽到具名函式裡
下一篇
Day 16- 單一職責:找到唯一修改理由,告別脆弱程式碼
系列文
消除你程式碼的臭味18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言