軟體設計最根本的問題之一:兩段功能該放在一起,還是分開? 這問題在每個層級都成立——function、method、class、service。Ousterhout 的答案不是「越小越好」,而是看哪種結構整體複雜度更低:最佳的資訊隱藏、最少的依賴、最深的介面。
🧠 Why It's a Problem
「拆得越小越好」是個迷思——因為拆分本身會引入複雜度:
- 元件數量:元件越多越難追蹤,而且通常意味更多介面,每個介面都是成本。
- 管理開銷:原本一個物件搞定,現在得管理好幾個。
- 距離:元件被放遠了,開發者更難同時看到它們;如果它們之間其實有依賴,甚至可能被忽略而生 bug。
- 重複:原本一份的程式碼,可能得在每個拆出來的元件裡各存一份。
所以決定的判準只有一個:這些程式碼是否密切相關。 相關就靠攏,不相關就分開。
NOTE
判斷「兩段程式碼是否相關」的四個訊號:
- 共享資訊:兩段都依賴同一份知識(例如某種文件格式的語法)。
- 一起使用(雙向):用其一通常會用其二。注意要雙向——disk block cache 幾乎一定用 hash table,但 hash table 在別處也常單獨用,所以不該綁在一起。
- 概念上重疊:能被一個簡單的高階範疇涵蓋(如「字串操作」涵蓋子字串搜尋與大小寫轉換)。
- 不看另一段就難以理解:一段的存在只有搭配另一段才說得通。
⚖️ 何時合、何時分
合併能贏的三種典型情況——關鍵都在於它讓介面變窄、讓知識集中,而不只是把行數搬來搬去:
合併訊號一 — 共享資訊 / 簡化介面(HTTP 讀取 + 解析)
// 分開:讀取與解析各屬一類,中間得靠介面傳「請求字串」
class RequestReader {
read(socket) { /* 讀 socket 存成字串 */ return rawText; }
}
class RequestParser {
parse(rawText) { /* 解析出 method / headers / body */ }
}
// 問題:讀取端其實需要解析 header(Content-Length)
// 才判斷得出請求在哪結束 —— 兩者共享 HTTP 格式知識// 合併:讀取與解析同處,共享的格式知識不再跨介面流動
class HttpRequest {
static receive(socket) {
// 邊讀邊解析:讀 header 才知道 body 多長,一氣呵成
}
}合併後「請求字串」那組中間介面直接消失,程式碼更短、更簡單。這就是「共享資訊時合併」與「簡化介面時合併」同時發生的例子。
合併訊號二 — 讓常見情況自動發生(緩衝該內建)
// 反面教材:緩衝被切成獨立一層,逼每個使用者自己包
const stream = new BufferedInputStream(new FileInputStream(path));// 合併:緩衝設為預設,絕大多數人根本不必知道它存在
const stream = FileInputStream.open(path); // 內部已緩衝
// 少數需要關閉 / 替換緩衝的人,才用進階選項介面該為常見情況設計得最簡單。把「幾乎人人都要」的細節內建,正是合併的價值。
分開能贏的核心情況是通用與特殊混在一起:
分離訊號 — 通用機制混入特殊用途(undo 機制)
// 反例:文字類別同時扛「通用 undo 基礎設施」與各種專用 handler
class Text {
// 通用:維護動作清單、執行 undo/redo
// 特殊:文字、選取、游標各自的 undo 邏輯全塞在這
}
// 每新增一種可 undo 的實體,就得改文字類別 → 資訊洩漏// 改善:把通用機制抽成獨立類別,特殊動作各自成類
class History {
addAction(action) {} // action 只需實作 { redo(), undo() }
addFence() {} // 分組:哪些動作算一次 undo
undo() {}
redo() {}
}
// UndoableInsert / UndoableSelection / UndoableCursor 各懂自己一種動作
// History 不知道動作型別 → 可重用;每層都不必懂別層細節這裡的分離對象是同一個機制的通用與特殊部分。注意反向的澄清:文字類別本身是通用文字機制,卻可以合理地包含「只處理文字插入 / 刪除的 undo」——因為那段特殊 undo 與其他文字功能緊密相關,只是不該與 History 這種通用基礎設施混在一起。
CAUTION
紅旗 通用 / 特殊混合 (Special-General Mixture):一個通用機制裡混進了特化於某用途的程式碼。機制變複雜,還在機制與用途之間造成資訊洩漏——將來改那個用途,可能被迫動到底層機制。
⚖️ 方法層級:別只看長度
同樣的「合 vs. 分」也適用於 method。但**「超過 N 行就拆」這種教條沒道理**:拆方法會引入新介面、把相關程式碼分散,反而更難讀。長方法不一定壞——深方法(功能多、介面簡單)即使上百行也沒問題。只在能讓系統整體更簡單時才拆。
拆的機械步驟見 提取函式;這裡談的是要不要拆、拆完對不對:
- 抽出子任務:父方法介面不變,把能乾淨獨立的子任務移出去。條件是——讀子方法不必懂父方法,讀父方法不必懂子方法的實作。
- 拆成兩個對外方法:當一個方法的介面本身同時做了好幾件不相關的事,才把它拆開;每個新方法的介面都應比原本更簡單,且理想上多數呼叫端只需呼叫其中一個。
WARNING
紅旗 連體方法 (Conjoined Methods):拆完後你得在父子(或兩個)方法之間來回翻,才搞得懂怎麼運作。程式碼物理上分開了,卻只有看著彼此才能理解——這代表拆錯了。同理,若拆成兩個方法後呼叫端得「呼叫全部、還在它們之間搬狀態」,也是壞拆分,很容易退化成一堆淺方法。
🔑 Takeaways
- 決定合或分,唯一判準是整體複雜度:選能帶來最佳資訊隱藏、最少依賴、最深介面的那個結構。
- 拆分不免費——元件數、管理開銷、距離、重複都是它的成本,「越小越好」是迷思。
- 合併訊號:共享資訊、雙向一起使用、概念重疊、不看另一段就難以理解。合併常能讓中間介面消失、讓常見情況自動發生。
- 分離訊號:同一機制的通用與特殊部分混在一起(紅旗 Special-General Mixture)——把通用抽成獨立類別,特殊留在它真正相關的地方。
- 方法長度不是判準:深方法再長也行。拆完若出現連體方法(要來回翻才懂),就是拆錯了。
No notes yet — jot your takeaways or Q&A here.