第 2 / 6 部 待錄

類別的藝術

《實作模式》Podcast 準備稿:類別的藝術

書名: 實作模式 Implementation Patterns 作者: Kent Beck 系列: 設計品味系列(程式碼層次的設計直覺) 公開標題建議: 設計品味系列|Implementation Patterns.第二集:類別、命名,與「繼承的萬靈丹幻覺」(2/6) 涵蓋範圍: 第 5 章「類別 Class」(含 Value Object、繼承 vs 委派、命名等模式)

背景速覽

上一集講完了世界觀地基——溝通、簡單、彈性,以及「維護比開發貴」的經濟學。這一集進入第一個具體主題:類別。類別是物件導向程式設計裡影響範圍最廣的設計元素,這一章也是全書最長的一章,塞了十幾個模式。但別怕——它們其實圍繞著三個核心問題打轉:類別到底在表達什麼、怎麼命名、以及怎麼表達「變化」。

一句話重點

類別的本質是「把變化慢的邏輯和變化快的資料綁在一起」;而表達變化有三條路——條件判斷、繼承、委派——繼承不是萬靈丹,它是三條路裡限制最多、用一次就鎖死的那條。

值得討論的重點

重點一:類別是什麼?一個關於「變動速率」的宣告

Kent Beck 給類別下了一個我覺得非常漂亮的定義。每當你建立一個類別,你其實是在向讀者宣告一句話:

「這些邏輯屬於同一群,而且它們比所操作的資料變得更慢。這些資料值也屬於同一群,以相近的速率變化,並由相關的邏輯來操作。」

關鍵字是變動速率。資料變得比邏輯快——這是讓類別這個概念能成立的核心觀察。Point 這個類別的邏輯(怎麼算距離、怎麼平移)很穩定,但它包的 xy 資料一直在變。把「變得慢的邏輯」和「變得快的資料」綁在一起,這就是類別在做的事。

順帶他丟了一句很反直覺的話:類別是程式裡相對昂貴的設計元素,每個類別都應該做有意義的事,減少類別數量是一種改善——前提是剩下的類別不會因此變臃腫。這跟很多人「物件導向就是拼命拆類別」的直覺剛好相反。類別不是免費的,每個都要付學習、理解、命名、瀏覽的成本。

重點二:命名是設計的錨點,而隱喻是命名的捷徑

Kent Beck 說,找到一個恰如其分的名字,是程式設計裡最爽的時刻之一。你跟一個概念纏鬥半天,程式碼複雜得不像話,然後常常是在對話中,有人說「喔,這其實就是一個 Scheduler 嘛」——所有人鬆一口氣,正確的名字一出現,一連串的簡化跟著發生。

他主張:類別名稱是設計裡最核心的錨點,沒有比類別命名更重要的命名選擇了。 類別有了好名字,方法的名字會自然跟著來;反過來幾乎不會發生。

但命名有個天生的張力:你希望名字簡短(你會在對話裡天天講它),又希望它精確(精確常常需要好幾個字)。怎麼化解?答案是隱喻。他舉了 HotDraw 繪圖框架的經典例子:一個類別本來叫 DrawingObject,又長又空泛。後來 Ward Cunningham 引入「排版」的隱喻——把畫布想成一頁排好版的頁面,上面的圖形元素就是 figures。於是類別改名 Figure。在這個隱喻底下,FigureDrawingObject 同時更短、更豐富、也更精確。一個好隱喻,能讓單一個字帶出一整串聯想。

子類別的命名則相反——它不常在對話裡出現,所以可以犧牲簡短換取表達力,做法是在父類別名前面加修飾詞:StretchyHandleTransparencyHandle

重點三:表達變化的三條路——Conditional、Subclass、Delegation

這是本章的技術核心,也是最值得反覆咀嚼的部分。當你需要讓「不同的物件做不同的事」,你有三種武器:

  • Conditional(條件判斷)if/switch。優點是邏輯全部集中在一個類別裡,讀者不用到處跳。缺點是改不了——要加新行為就得改這個類別的程式碼,而且路徑越多,程式正確的機率越低。
  • Subclass(子類別):用繼承表達變化。優點是型別安全、變化點清楚。但有個致命限制——子類別表達的變化必須在「建立物件時」就決定,之後不能改。
  • Delegation(委派):把變化的部分委派給另一個可替換的物件。優點是執行期可以換、可以獨立測試。缺點是讀程式碼要在多個類別之間跳。

Kent Beck 給的圖形編輯器例子很傳神。編輯器按下滑鼠,有時要建矩形、有時要移動圖形。用 switch(getTool()) 寫,就有上面所有條件判斷的毛病;用子類別也不行——因為編輯器在生命週期中要切換工具,而子類別建好就鎖死了。於是只剩委派:getTool().mouseDown(),工具可以隨時抽換。

他的建議是:把這三種寫法在腦中視覺化一遍,朝看起來最好的那個方向走幾步,看程式碼有沒有變好。這三者之間要能隨意切換,視當下需求而定。

重點四:Value Object——把不可變的「數學微世界」嵌進你的程式

這是這章我最想推薦給聽眾的一個模式。Kent Beck 說,「物件帶有可變狀態」是思考運算的一種方式,但不是唯一的方式。數學提供了另一種——在絕對真理、無時間限制的世界裡思考。Java 的基本型別(大多)就活在數學世界:你對數字加 1,不是改變那個數字,而是建立一個新值。你沒辦法改變 0 本身。

Value Object 就是把這個精神帶進你自己的物件:物件一旦建立就不可變,所有操作都回傳新物件,而不是改自己。他舉會計系統的 Transaction——交易一旦建立,金額就釘死了,建構子還強制它一定記入兩個帳戶。讀這段程式碼你就放心了:不用擔心交易在記帳後被偷偷改掉。

他給的實作三步驟很實用:劃清「狀態世界」和「值世界」的界線、在建構子裡設定所有狀態(之後不再有欄位指派)、操作永遠回傳新物件。

最常見的反對意見是效能——要建一堆中間物件。但 Kent Beck 直接打臉:在整體成本裡這個論點通常站不住腳,因為大多數程式段落根本不是瓶頸。他還補了一個很重要的警告:「半套的 Value Object 是最糟的情況」——介面變複雜了,但你又不能安全假設狀態不會變,兩頭落空。要做就做徹底。

注意事項

⚠️ 「繼承不是萬靈丹」這個訊息在 2007 年是先進的反叛,但今天已經接近共識(「組合優於繼承」幾乎是新人都會背的口訣)。所以講這集時要小心別把它講成新發現。更值得帶聽眾深入的是 Kent Beck 點出的具體判準:繼承的致命傷是「只能用一次」「變化建好就鎖死」「平行階層的隱性耦合」。他花了快一年才解開一個保險系統的平行階層問題——這種真實的痛,比口號「組合優於繼承」有用得多。

⚠️ Value Object 在 2007 年要手刻,但現代語言給了你語法糖:Java 的 record、Kotlin 的 data class、Scala 的 case class,乃至於整個函數式語言生態,都把「不可變值」變成一等公民。所以聽眾今天的問題不再是「該不該手刻 Value Object」,而是「我有沒有善用語言給我的不可變工具」。Kent Beck 的洞見還在,但實作門檻已經被語言抹平了。

⚠️ 書裡有些模式今天看會覺得「太細了吧」——像 Pluggable Selector(用反射存方法名稱在欄位裡)他自己都警告會被過度使用、成本很高。這類模式我建議在節目裡點到為止、甚至直接說「這個現在很少用了」。寧可把時間花在三條路、命名、Value Object 這些歷久彌新的東西上,不要被 77 個模式的數量綁架。

專家補充

💡 「繼承只能用一次」這件事,背後其實是 Java 單一繼承的限制在作祟,但更深層的問題是繼承把父子綁成了白箱關係——子類別可能依賴父類別實作的微妙細節,父類別一改就炸。這就是 Joshua Bloch 在《Effective Java》裡那條著名的「Favor composition over inheritance」以及「Design and document for inheritance or else prohibit it」的由來。Kent Beck 這章可以看成那條準則的「為什麼」版本。

💡 Value Object 這個概念後來在 Eric Evans 的《Domain-Driven Design》裡被發揚光大,成了 DDD 的核心建構塊之一(Entity vs Value Object 的區分)。Kent Beck 這裡是從「程式碼可讀性/可推理性」的角度切入,DDD 則是從「領域建模」的角度切入,但講的是同一個東西:有些物件沒有身份,只有值,相等就是同一個。兩條路殊途同歸。

💡 他提到「有效使用子類別的關鍵,是把父類別的邏輯徹底分解成各做一件事的小方法,讓子類別只需要覆寫剛好一個方法」——這其實偷渡了後面「方法」那章的 Composed Method 模式。這也呼應上一集那句金句:設計的各層是互相支撐的。命名好不好、繼承順不順,最後都回到「方法有沒有切好」這件最微觀的事上。可以在這裡埋個伏筆,預告第五集會專門講方法。

討論問題

🎙️ 用「變動速率」這個尺子,檢查你手上某個類別:裡面的欄位是不是都以差不多的速度在變?有沒有哪個欄位明顯變得比別人快或慢?如果有,它可能根本不該待在這個類別裡。

🎙️ 回想你最近一次用繼承。如果現在要求你「執行期動態切換」這個行為,你的繼承會不會立刻崩掉?如果會,那當初是不是該用委派?

🎙️ 你的程式碼裡有沒有一個「半套的 Value Object」——大部分時候不變、但偶爾被人偷偷改一下?這種模糊地帶 Kent Beck 說是最糟的。你要嘛讓它徹底不可變,要嘛承認它就是可變狀態,你選哪邊?

🎙️ 挑一個你覺得命名很爛的類別。試著問:它背後有沒有一個更好的隱喻?像 DrawingObjectFigure 那樣,有沒有一個字能讓它瞬間清楚?

更大範圍關聯

  • 接《Effective Java》(Joshua Bloch):本章的「繼承 vs 委派」「謹慎使用繼承」直接對應 Bloch 的 Item「組合優於繼承」。Kent Beck 給哲學和痛點,Bloch 給更系統化的條款和 Java 細節。兩本互補。

  • 接《Domain-Driven Design》(Eric Evans):Value Object 是兩本書的交集。從這集的「不可變值讓程式碼好推理」,可以順勢接到 DDD「用 Value Object 建模領域概念」的更大視野。

  • 接《Refactoring》(Martin Fowler):本章反覆出現的「在 Conditional / Subclass / Delegation 之間切換」,在 Fowler 書裡就是一組具體的重構手法——Replace Conditional with Polymorphism、Replace Inheritance with Delegation。這集講「該往哪走」,Refactoring 講「怎麼安全地走過去」。

  • 接函數式編程:Value Object「不可變、操作回傳新值」根本就是函數式思維滲進物件導向。從這裡可以拉到更大的趨勢——過去十幾年主流語言(Java record、Kotlin、Rust 的所有權)都在往「預設不可變」靠攏。Kent Beck 在 2007 年就在推這件事。

錄製建議

目標約 20 分鐘,單人主講。建議節奏:

  • 0–2 分:承接開場。一句話回顧上集(溝通、經濟學),點出這集進入第一個具體主題「類別」,也是全書最厚的一章,但別被模式數量嚇到,抓三個核心問題就好。
  • 2–7 分:類別是什麼 + 命名。先講「變動速率」這個漂亮定義和「類別其實很貴」的反直覺觀點,再講命名是錨點、用 DrawingObject → Figure 的隱喻故事收尾。這段畫面感強,適合開場暖身。
  • 7–13 分:表達變化的三條路(本集主菜)。Conditional / Subclass / Delegation 各講優缺點,用圖形編輯器「切換工具」的例子串起來,重點敲在「繼承建好就鎖死、委派才能執行期換」。講慢一點。
  • 13–17 分:Value Object。用「Java 的數字加 1 是建新值」破題,講會計 Transaction 的例子,提醒「半套最糟」。
  • 17–20 分:辯證 + 收尾。誠實說「組合優於繼承今天已是共識,但具體痛點仍值得學」「Value Object 現在有語言糖了」。預告下一集講「狀態」——怎麼決定什麼該存、什麼該算。

口氣提醒:這章模式多,最大的風險是講成「逐條念清單」。務必圍繞「變化怎麼表達」這條主軸串,把零碎模式掛上去,而不是平鋪直敘。