合在一起,還是分開? (Better Together or Better Apart?)

★★★★Draft

FromA Philosophy of Software Design

📚 From the Books

合在一起,還是分開? (Better Together or Better Apart?)Complexity

軟體設計最根本的問題之一:兩段功能該放在一起,還是分開? 這問題在每個層級都成立——function、method、class、service。Ousterhout 的答案不是「越小越好」,而是看哪種結構整體複雜度更低:最佳的資訊隱藏、最少的依賴、最深的介面。

🧠 Why It's a Problem

「拆得越小越好」是個迷思——因為拆分本身會引入複雜度

所以決定的判準只有一個:這些程式碼是否密切相關。 相關就靠攏,不相關就分開。

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

✍️ My Notes

No notes yet — jot your takeaways or Q&A here.

📖 Further Reading

🔗 Dive Deeper