第 5 / 11 部 待錄

合併或分離

《A Philosophy of Software Design 軟體設計的哲學》Podcast 準備稿:合併或分離

書名: 軟體設計的哲學 A Philosophy of Software Design 作者: John Ousterhout 系列: 旗艦評書(恩普拉氏) 公開標題建議: 評書|A Philosophy of Software Design.該合還是該分?破除「函式越短越好」的教條 (5/11) 涵蓋範圍: 第9章 合併或分離

背景速覽

這是整本書裡跟《Clean Code》對撞最直接的一章。Uncle Bob 那句「函式超過 N 行就拆」幾乎是業界肌肉記憶,而 Ousterhout 在這一章直接寫下:「這種教條基本上沒道理。」這一章回答一個每天都要做幾十次的微決策——眼前這兩段程式碼,該放一起還是該分開? Ousterhout 不給死規則,給五條判準:什麼時候該合(共享資訊、簡化介面、去重)、什麼時候該分(通用與特殊要分家),以及方法層級該怎麼拆、怎麼合。

一句話重點

「該合還是該分」沒有「行數」這種懶人答案——唯一的判準是「整個系統會不會因此更簡單」:共享同一份知識的、能簡化介面的、能消除重複的,就該合;而把「通用機制」和「特定用途」混在一起的,就該分;至於方法多長,只要它是深的(介面比實作簡單),一百行也無所謂。

值得討論的重點

1. 三種「該合」的訊號

Ousterhout 給三個合併的理由,都很可操作:

  • 共享資訊時合併:HTTP 伺服器的讀取與解析共享「請求格式」這份知識——不先解析 header 就不知道請求多長,所以合成一個類別反而更短更簡單。
  • 簡化介面時合併:兩個模組合併後,新介面可能比原本任一個都簡單,因為它們之間原本要傳遞的那組介面直接消失了。Java I/O 又一次當反例:若 FileInputStreamBufferedInputStream 合併、緩衝設預設,大多數人根本不會察覺緩衝存在。
  • 去除重複時合併:同一段 code 反覆出現是「你還沒找到正確抽象」的紅旗。處理法不只是機械式抽方法(片段太短、或要傳一堆 by-reference 參數時,抽了反而更糟),而是重組結構讓重複自然消失——例如把多個錯誤點共用的清理 code 移到方法尾端統一處理。

2. 通用與特殊要分家:本書最精緻的一條分離原則

「該分」最重要的一條:一個提供通用機制的模組,就只該提供那個機制,不要混入特化某用途的 code。 Ousterhout 稱混進去的情況為紅旗「通用/特殊混合」。最漂亮的案例是編輯器的 undo 機制:

  • 反例:把所有 undo 邏輯塞進文字類別——通用的「管理動作清單」跟特殊的「undo 一次選取/游標變更」全攪在一起,新增一種可 undo 的東西就得改文字類別。
  • 正解:抽出一個通用的 History 類別(只管動作清單、執行 undo/redo、分組),各種具體 undo 動作(UndoableInsertUndoableSelection…)各自獨立成類別,分組策略則由 UI 高層用 addFence 控制。三層各不需懂彼此細節,History 因此能重用到任何應用。

3. 一個關鍵澄清:分的是「同一機制的通用 vs 特殊」

這條很容易被誤用,Ousterhout 特別澄清:要分開的是同一個機制內的通用與特殊(通用 undo 基礎設施 vs 某個特殊 undo 動作,這兩者要分)。但不同機制的通用與特殊混在一起反而合理——文字類別本身是通用文字機制,裡頭放「只處理文字插入/刪除的 undo code」是 OK 的,因為那份特殊 undo 跟文字功能緊密相關,只是它不該跟 History 那個通用基礎設施攪在一起。這個區分很細,但正是避免「拆過頭」和「拆錯地方」的關鍵。

4. 方法層級的拆與合:破除「N 行就拆」

Ousterhout 直接開火:「方法超過 N 行就拆」這種教條沒道理。拆方法會引入新介面、把相關 code 拆散反而更難讀,只在能讓系統整體更簡單時才拆。他給兩種良性拆法:

  • 抽出子任務:父方法介面不變,前提是子任務能「乾淨獨立」——讀子方法不必懂父方法、讀父方法不必懂子方法實作。
  • 拆成兩個對外方法:當原方法介面本身太複雜(同時做好幾件不相關的事),拆成各承擔一部分、且每個新介面都更簡單的方法,理想是多數呼叫端只需呼叫其中一個。

反面紅旗叫「連體方法(conjoined methods)」:拆完之後你得在父子之間來回翻才看得懂——那就是拆錯了,兩段物理上分開、卻只能看著彼此理解。

注意事項

⚠️ 這一章最危險的誤讀是把「別亂拆」聽成「都別拆、寫長函式沒關係」。Ousterhout 的標準其實比「N 行」更高、更難——他要的是「每個方法做一件事且做完整、介面比實作簡單」,長短只是結果。一個三百行的深方法可以,一個十行卻要呼叫端搬一堆狀態的淺方法不行。判準是「深不深」,不是「長不長」。

⚠️ 「去除重複」也別走極端。片段只有一兩行、或抽出後簽章臃腫(要傳一堆區域變數)時,抽方法的好處可能是負的。DRY 是手段不是教條——目標是「找到對的抽象」,不是「消滅每一個重複字元」。

⚠️ goto 那段要小心講。Ousterhout 確實說「從巢狀程式碼跳脫」這類情境 goto 仍可用——但這是 C 那種沒有更好跳脫機制的語境。現代語言多半有更乾淨的做法(提前 return、defer、try-with-resources),別讓聽眾以為「大師說 goto 可以用」就到處 goto。

專家補充

💡 這集是整季對照線的第二主戰場(第一是 EP2 深模組)。Uncle Bob《Clean Code》第三章:「函式應該短小,再短小」「Extract till you drop(抽到不能再抽)」;Ousterhout 第九章:「N 行就拆沒道理」「連體方法是紅旗」。兩人幾乎是逐句對著幹。但精準的講法仍是——Uncle Bob 在優化「一個函式讀起來像不像散文」,Ousterhout 在優化「拆完之後系統整體複雜度有沒有降」。 兩者在很多時候不衝突(一個既短、又深、又獨立的函式兩派都愛),衝突只發生在「為了短而拆出淺方法」時。這正是藍圖 §5 要聽眾自己判斷的灰色地帶。

💡 History 那個 undo 案例,其實就是 GoF 的 Command 模式 + Memento 的精神,但 Ousterhout 沒掉書袋,他從「通用/特殊分離」這個第一原理把它推導出來。這示範了一件事:好的設計模式不該是背下來套用的,而該是從『複雜性往哪走』自然長出來的。 可以接到「設計模式是結論不是公式」的討論。

💡 「連體方法」這個紅旗特別實用,因為它給了「過度拆分」一個可觀測的症狀:你得開兩個檔案、來回對照才看得懂一段邏輯。 下次 code review 看到 reviewer 一直在兩個小函式間跳,那就是連體方法在報警。

討論問題

🎙️ 開場鉤子:「『函式超過二十行就該拆』——這句話你一定聽過,可能還信了很多年。今天這位史丹佛教授要當著 Uncle Bob 的面說:這條規則,基本上沒道理。那到底什麼時候該拆、什麼時候該合?」

🎙️ 自問自答:「我把一個大函式拆成五個小函式,到底是讓它更好讀了,還是讓人得開五個地方來回翻?」——用「連體方法」這個紅旗回答:如果你得看著彼此才懂,你不是拆乾淨了,你是把一件事撕成了五片。

🎙️ 帶走的一題:「找一段你最近重構時『為了短而拆出去』的小函式。問自己:它有獨立的意義嗎?還是讀它的人非得回去看呼叫端才懂?如果是後者——也許把它合回去,反而更清楚。」

更大範圍關聯

  • 與《Clean Code》正面對撞:本集是整季對照組的第二主戰場,「N 行就拆」vs「拆得讓系統更簡單才拆」直接回應 [軟體工程領域切分藍圖] §5「Clean 到底乾不乾淨」。強烈建議搭配 Reading Uncle Bob「函式」那一集對著聽,讓聽眾自己評判。
  • 設計模式的再理解History/undo 案例對應 GoF 的 Command/Memento,但走的是「第一原理推導」而非「套模式」——接到「設計模式是結論不是公式」的討論。
  • DRY 的源頭:「去除重複=找到對的抽象」呼應《The Pragmatic Programmer》的 DRY 原則;而「別為 DRY 而 DRY」也呼應近年對「錯誤的抽象比重複更貴」(Sandi Metz)的反思——軟工三經典之外的當代補充。

錄製建議

  • 建議時長:約 22–24 分鐘。配比:三種「該合」約 8 分、通用/特殊分離(undo 案例)約 8 分、方法拆合+破除 N 行教條約 8 分。
  • 討論策略:開場直接點燃「N 行就拆」這個聽眾最熟的教條,製造張力;中段用 History/undo 案例把「通用/特殊分離」演成一個漂亮的正解(這是本章最有成就感的設計);結尾回到方法層級,用「連體方法」這個可觀測的紅旗給聽眾一把當場能用的尺。
  • 這是對照線高潮之一,講的時候務必平衡——目標不是讓聽眾「站 Ousterhout、踩 Uncle Bob」,而是讓他們學會「在『為了短而拆』時踩煞車」。