通用模組更深 (General-Purpose Modules Are Deeper)

★★★★Draft

FromA Philosophy of Software Design

📚 From the Books

通用模組更深 (General-Purpose Modules Are Deeper)Complexity

設計新模組時,該做成「通用(general-purpose)」還是「專用(special-purpose)」?Ousterhout 的答案是甜蜜點在中間:稍微通用(somewhat general-purpose)——功能反映你當下的需求,但介面要夠通用,能支援多種用途。最違反直覺的一點是:通用介面通常比專用介面更簡單、更深,即使模組只用於它原本的用途,通用做法仍然比較好。這是 深模組 那條「窄介面、厚實作」原則的實作路徑。

🧠 Why It's a Problem

IMPORTANT

「稍微通用」的精確意涵:功能反映當下需求,介面則夠通用能支援多種用途;介面對當下需求要好用,卻不被當下需求綁死。

⚖️ 設計對照(專用 vs. 通用)

Ousterhout 的教學案例:一個 GUI 文字編輯器,底層有個管理文字的類別。學生常把它寫成專用 API——把編輯器的 UI 操作直接對應成方法。

專用 API — 方法對應 UI 操作,淺且外洩
// 文字類別的方法直接照著鍵盤/滑鼠操作長:
class Text {
  backspace(cursor) { /* 刪游標左側一字元 */ }
  delete(cursor) { /* 刪游標右側一字元 */ }
  deleteSelection(selection) { /* 刪一段選取 */ }
}

看似「UI 好寫」,實際上:backspacedeletedeleteSelection 各自只在一個地方被呼叫,都是淺方法;UI 概念(selection、backspace 鍵)滲進了文字類別;每加一個 UI 操作,就得回到文字類別新增一個方法——UI 開發者最後也得跑去改底層。

通用 API — 用基本文字操作定義,窄而深
// 只用「基本文字操作」定義,不反映高階 UI 操作:
class Text {
  insert(position, newText) { /* 任意位置插入任意字串 */ }
  delete(start, end) { /* 刪除 start..end 的字元 */ }
  changePosition(position, numChars) { /* 從某位置前後移動,跨行自動處理 */ }
}

// UI 操作在「UI 程式碼裡」用通用方法拼出來:
text.delete(cursor, text.changePosition(cursor, 1));  // delete 鍵:刪右邊一字元
text.delete(text.changePosition(cursor, -1), cursor); // backspace 鍵:刪左邊一字元

Position(通用)而非 Cursor(特定於 UI)。單一 UI 操作的行數也許略多一點點,但方法更少更深、整體程式碼更少,而且「backspace 到底刪了哪些字元」直接寫在 UI 程式碼裡一目了然——專用版反而要繞到文字類別才看得出來。同一組 insert/delete 還能用在編輯器以外(例如「把檔案中所有 X 換成 Y」)。

NOTE

專用版的 backspace 是典型的假抽象(false abstraction):它看似把「刪了哪些字元」藏起來,但 UI 模組其實真的需要知道這件事,開發者最後還是得讀它的實作。把細節塞進介面背後,反而讓需要它的人更難拿到——這只製造模糊(obscurity)。

TIP

通用做法帶來更乾淨的分離,因此資訊隱藏更好:文字類別完全不需要知道 UI 細節,新增 UI 功能時也不必回頭改它。核心判斷是「誰需要知道什麼、什麼時候需要?」——當細節重要時就明確呈現,而非藏起來。

🔎 三個自我提問

辨識一個乾淨通用的類別,比從零設計出來容易。Ousterhout 給了三個平衡「通用」與「專用」的問題:

WARNING

通用不等於原始(primitive)。 如果使用者必須寫大量額外程式碼才能用你的類別完成當下任務,這是紅旗:介面沒有給出正確的功能粒度。通用要服膺於「常見用法易用」。

🔑 Takeaways

✍️ My Notes

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

📖 Further Reading