第 5 / 6 部 待錄

方法與集合

《實作模式》Podcast 準備稿:方法與集合

書名: 實作模式 Implementation Patterns 作者: Kent Beck 系列: 設計品味系列(程式碼層次的設計直覺) 公開標題建議: 設計品味系列|Implementation Patterns.第五集:Composed Method 與集合的隱喻 (5/6) 涵蓋範圍: 第 8 章「方法 Method」、第 9 章「集合 Collections」

背景速覽

前面幾集從世界觀一路講到類別、狀態、行為。這一集合併兩個最微觀、最貼地的主題:方法集合——也就是你每分每秒手指底下真正在打的東西。方法那章是全書最核心的一章(Composed Method 幾乎是其他所有模式的基礎),集合那章則展示了「同一個概念,選不同的型別就是在對讀者說不同的話」。把兩章放一起,正好構成「程式碼最小單位的品味」這一集。

一句話重點

把方法切成「同一抽象層次的小步驟」(Composed Method),用意圖而非實作來命名;選集合時,介面是在向讀者宣告「有序嗎、唯一嗎、能用 key 找嗎」,實作則只關效能——而 99% 的時候答案就是 ArrayList。

值得討論的重點

重點一:Composed Method——全書的「方法基本功」

Kent Beck 說這是他最重要的方法模式。Composed Method 的定義很簡單:一個方法由「對其他方法的呼叫」組成,而且這些被呼叫的方法都大致在同一個抽象層次。

什麼叫「混合抽象層次」?他的反例很傳神:

void compute() {
  input();
  flags |= 0x0080;   // 突然一個位元運算
  output();
}

input()output() 是高層意圖,中間突然冒出一個 flags |= 0x0080 的低層位元操作——讀者會被這個落差絆倒。程式碼在抽象層次平順流動時最好讀,突然切換層次會打斷流暢感。

關於「方法該多長」,他的態度很反教條。有人主張「不超過一頁」「5 到 15 行」,但他說限制本身引出了「為什麼」的問題。真正的洞見是:你閱讀程式碼有兩種模式——看整體結構時,一次看到一大片是有價值的(空白行的分布給你結構線索);但要理解細節時,大方法就成了障礙,因為你的腦一次只裝得下一個「腦容量」的細節。

最實用的建議是他的「先 inline 再重切」工作法:如果你的方法拆得很難讀,把所有小方法 inline 回去、變成一個大方法,然後基於你剛學到的東西重新切分。他反覆強調:基於事實組合方法,而不是推測——先讓程式碼跑起來,等所有細節攤在你面前,再來合理地分割。預先過度結構化,等你實作中學到新東西,就得重做。

重點二:Intention-Revealing Name——用「目的」命名,有時要懂得「克制」

方法該以潛在呼叫者可能有的目的來命名,而不是實作策略。Customer.linearCustomerSearch(id) 是爛的——linear 是實作細節;Customer.find(id) 才對。

他講了一個很有意思的點:作為程式碼作者,你的目標不是「盡快把你知道的一切都說出來」,有時候需要克制。 除非實作策略跟使用者相關,否則別放進名字裡。好奇的人可以自己去看方法本體。

更深的洞見:從「呼叫端程式碼長什麼樣」來想方法名字,因為讀者最可能在那裡第一次遇到它。呼叫端的程式碼應該在「講一個故事」,命名方法時要讓它們幫忙說這個故事。這跟前幾集的命名哲學一脈相承——命名不是貼標籤,是讓程式碼能被當成散文來讀。

他還埋了一個跟測試有關的觀點(別忘了他是 TDD 之父):自動化測試可以傳達不適合放進註解的資訊。你可以寫個測試,確保「以錯誤順序呼叫方法時會丟適當的例外」——這比寫一句註解「請先呼叫 A 再呼叫 B」強太多,因為測試會跟著程式碼一起被檢查、一起重構,而註解會默默過期、沒有任何即時回饋。

重點三:Method Object——把「不可能的方法」變回可讀

這是 Kent Beck 點名「最喜愛的模式之一」。場景是:一個超長的方法,塞滿參數和暫存變數,你想抽方法卻發現——每抽一塊就要傳一長串參數、子方法又難命名。死路。

Method Object 的解法很巧:把這個方法本身變成一個類別complexCalculation() 變成 ComplexCalculator 類別;原方法的每個參數、區域變數、欄位,都變成新類別的欄位;原方法搬進新類別的 calculate() 方法。

魔法在於:一旦這些變數都變成欄位,你抽方法時就不用傳任何參數了——因為所有資料都在欄位裡。Kent Beck 說「確認行為一致後,樂趣就開始了」:你開始輕鬆地抽方法,常會發現有些變數可以從欄位降級回區域變數,難以隔離的共同子表達式變成有意義的 helper method。一個糾結的爛攤子,逐步被梳理成清晰的、可逐層理解的結構。

重點四:集合的介面在「說話」,實作只關效能(而且 99% 是 ArrayList)

集合這章的核心洞見:集合表達了程式設計最基本的變化之一——數量的變化。有句老諺語「唯一有趣的數字是 0、1、many」——欄位不存在是 0,欄位存在是 1,欄位裝個集合就是 many。

而選集合時,介面和實作是在回答兩種完全不同的問題:

  • 介面是在向讀者宣告語意:List 說「有序、能用位置找」;Set 說「不重複、沒順序」;SortedSet 說「有序又不重複」;Map 說「能用 key 找」。你選哪個介面,就是在告訴讀者「這堆東西有沒有順序、能不能重複、怎麼存取」。
  • 實作幾乎只關效能:ArrayList 存取快增刪慢、LinkedList 相反、HashSet 最快但無序。

但這裡有個 Kent Beck 很務實的數據:絕大多數集合都是 ArrayList,HashSet 是遙遠的第二名(他數過 Eclipse+JDK,ArrayList 約 3400 次、HashSet 約 800 次引用)。快速做法就是在這兩個之間選一個,有量測證據說某操作是瓶頸了,再換更專門的。先簡單,必要時再特化,夠快就停手。

他還誠實地自我打臉:他曾「教條式地」把 JUnit 裡所有變數宣告都泛化到最通用的介面,結果同一個物件這裡是 Iterable、那裡是 Collection、又一處是 List——反而更難讀。最後統一宣告成 List 才清楚。這個故事很重要:一致性有時比理論上的精確更重要

關於繼承集合,他給了明確的戒律:別繼承 ArrayList 來做你的 Library,要用委派。繼承會把一堆不該暴露的操作(clear()toArray())洩漏給客戶端,而且浪費掉你唯一的繼承額度。只有在你真的在實作一個「能加進 java.util 的通用集合」時才繼承——其他所有情況,都把集合當成一個欄位藏起來。

注意事項

⚠️ Method Object 是個強大的「最後手段」,但它有點重——你為了拆一個方法,生出一個只有一個公開方法的類別。在有 lambda、closure、local function 的現代語言裡,很多「Method Object」場景可以更輕量地解決(把暫存狀態用閉包捕捉、或拆成幾個帶參數的私有函式)。所以建議聽眾:先試 Composed Method,真的卡到「參數爆炸 + 難命名」的死路,再搬出 Method Object。別一看到長方法就上重武器。

⚠️ 集合這章有大量的效能數據和「什麼時候換 LinkedList / TreeSet」的指引,但這些都建立在 2007 年的 JVM 和硬體上。今天 CPU 快太多、JIT 聰明太多,很多「效能差異」在真實負載下根本量不出來。更重要的是 Kent Beck 自己的主張沒過時:先用最簡單的(ArrayList / HashMap),用量測而非直覺驅動優化。 聽眾要帶走的是這個紀律,不是背那幾張效能對照表。

⚠️ 「用意圖命名、別洩漏實作」是好原則,但別走火入魔到「為命名而命名」。Kent Beck 自己在 helper method 那段就警告:helper 可能太短——一個只是包了一行建構子呼叫的 helper,溝通價值跟直接寫沒差。命名和抽方法是有成本的(多一個要讀、要跳的東西),只有當它真的幫忙說故事時才值得。簡單和溝通要平衡,這是第一集就立下的價值觀。

專家補充

💡 Composed Method 的「同一抽象層次」原則,和 Uncle Bob《Clean Code》裡的「降層規則(The Stepdown Rule)」是同一件事——程式碼應該像一篇文章,每個函式後面緊跟著比它低一層抽象的函式,讓你可以由上而下一層層讀下去。Kent Beck 給了直覺(層次突變會絆倒讀者),Uncle Bob 把它formalize成一條可操作的規則。兩本一起看,這個概念會立體起來。

💡 「先 inline 再重切」這個工作法,其實是重構的精髓,也呼應 Kent Beck 自己的名言「Make it work, make it right, make it fast」——先讓它能跑(別管結構),等事實都攤開了再 make it right(重新組合方法)。它對抗的是工程師最常見的壞習慣:在還沒搞懂問題前,就急著把程式碼「設計」得漂漂亮亮,結果學到新東西後全部白工。這跟第四集破除教條、第一集「基於事實而非推測」是同一種務實精神貫穿全書。

💡 「集合的介面在說話、實作只關效能」這個區分,在現代語言裡被推得更遠。很多語言(Kotlin、Scala、Rust)在型別系統層級就區分「不可變集合」和「可變集合」——List vs MutableList——讓「這個集合會不會被改」也變成介面宣告的一部分。這正是 Kent Beck 在書裡抱怨「Iterable 沒辦法宣告式地表達『不該被改』」的那個缺口,被後來的語言補上了。他指出的問題,語言設計者聽到了。

討論問題

🎙️ 挑你程式裡一個你自己都不太敢動的超長方法。試試 Kent Beck 的工作法:先把它的小方法全 inline 回去變成一坨,再基於你現在的理解重新切——切出來的結構,跟原本的一樣嗎?

🎙️ 看你最近寫的一個方法名。它傳達的是「目的」(呼叫者想幹嘛)還是「實作」(它內部怎麼做)?如果把它放到呼叫端,那行程式碼讀起來像在「講故事」嗎?

🎙️ 你選集合型別時,是先想「語意」(要不要有序、要不要唯一)還是先想「效能」(哪個快)?Kent Beck 說介面選語意、實作選效能,而且實作 99% 就是 ArrayList——你有沒有為了想像中的效能,過早選了 LinkedList 或 TreeSet?

🎙️ 你的程式裡有沒有一個類別是「繼承了某個集合」?它有沒有因此暴露了一堆不該被外部呼叫的操作?如果改成「把集合當欄位藏起來、只暴露有意義的方法」,介面會不會乾淨很多?

更大範圍關聯

  • 接《Clean Code》:本集和 Clean Code 的「函式」那章幾乎是雙胞胎——函式要小、一個函式一件事、降層規則、用意圖命名。Uncle Bob 寫得更像戒律手冊,Kent Beck 給你背後的取捨和工作法(先 inline 再重切)。想把「怎麼寫好一個函式」搞透,兩本對讀最好。

  • 接《Refactoring》(Martin Fowler):Method Object 就是 Fowler 的「Replace Method with Method Object」;Composed Method 對應「Extract Function / Inline Function」的反覆運用。這集講「為什麼、什麼時候」,Refactoring 給「一步步怎麼做」的機械式步驟。

  • 接《Effective Java》(Joshua Bloch):集合這章的「優先用介面型別宣告」「用委派而非繼承來包裝集合」在 Bloch 那裡都有更系統化的條款。Kent Beck 還誠實補了 Bloch 沒強調的反面——「過度泛化反而難讀,一致性有時更重要」,這個務實的補充很珍貴。

  • 接 TDD 與測試文化:本集「用測試取代註解來溝通」「測試是設計練習」是 Kent Beck TDD 哲學的延伸。測試不只抓 bug,它是會自我驗證、不會過期的「活文件」。這條線可以拉到更大的討論:在 AI 輔助寫程式的時代,「可執行的規格(測試)」比「會過期的文字(註解/文件)」更重要——這個老觀點反而越來越新。

錄製建議

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

  • 0–2 分:承接。回顧前四集的路徑(世界觀→類別→狀態→行為),點出這集合併兩個最貼地的主題——方法和集合,是「程式碼最小單位的品味」。
  • 2–8 分:方法那章的核心。Composed Method(同一抽象層次)+ 那個 flags |= 0x0080 的絆腳例子,接「方法該多長」的反教條,落在「先 inline 再重切」「基於事實不基於推測」這個金句工作法。這段是本集主菜之一。
  • 8–11 分:命名 + Method Object。意圖命名、「有時要克制別全說出來」,再快速講 Method Object 的魔法(變數變欄位、抽方法不用傳參數)。
  • 11–16 分:集合那章。「0/1/many」破題,講透「介面說語意、實作只關效能、99% 是 ArrayList」,用 Kent Beck 自己「過度泛化反而難讀」的打臉故事收這段,順帶帶「委派而非繼承集合」。
  • 16–20 分:辯證 + 收尾。誠實面對「Method Object 在 lambda 時代可更輕」「效能數據過時但紀律不過時」。預告最後一集——進入完全不同的世界:框架開發,所有價值觀都要翻轉。

口氣提醒:兩章塞一集,資訊量大,最大風險是趕。寧可砍掉幾個小模式(helper method 的細節、collection 工具類別的概覽),把 Composed Method、命名、集合的介面/實作之分這三個歷久彌新的東西講透。