設計新模組時,該做成「通用(general-purpose)」還是「專用(special-purpose)」?Ousterhout 的答案是甜蜜點在中間:稍微通用(somewhat general-purpose)——功能反映你當下的需求,但介面要夠通用,能支援多種用途。最違反直覺的一點是:通用介面通常比專用介面更簡單、更深,即使模組只用於它原本的用途,通用做法仍然比較好。這是 深模組 那條「窄介面、厚實作」原則的實作路徑。
🧠 Why It's a Problem
- 未來需求難以預測:通用派主張多做一點以備日後之用;但軟體的未來用途很難猜準,過度通用往往塞進永遠用不到的功能,又把當下需求搞得難用。
- 專用做法製造淺方法:把介面直接對應「當下想支援的每個操作」,會讓模組塞滿一堆各自只服務一個情境的淺方法,使用者得認識一大票 API。
- 專用做法造成資訊洩漏:當一個模組的方法為另一個模組的概念量身打造,兩邊就綁死了——上層概念滲透進底層,任何新操作都得同時改兩個類別,破壞「每個類別能獨立發展」的目標。
- 「稍微」很關鍵:走火入魔做成大而全的東西,反而讓當下需求都難以完成,這也是失敗。
IMPORTANT
「稍微通用」的精確意涵:功能反映當下需求,介面則夠通用能支援多種用途;介面對當下需求要好用,卻不被當下需求綁死。
⚖️ 設計對照(專用 vs. 通用)
Ousterhout 的教學案例:一個 GUI 文字編輯器,底層有個管理文字的類別。學生常把它寫成專用 API——把編輯器的 UI 操作直接對應成方法。
專用 API — 方法對應 UI 操作,淺且外洩
// 文字類別的方法直接照著鍵盤/滑鼠操作長:
class Text {
backspace(cursor) { /* 刪游標左側一字元 */ }
delete(cursor) { /* 刪游標右側一字元 */ }
deleteSelection(selection) { /* 刪一段選取 */ }
}看似「UI 好寫」,實際上:backspace、delete、deleteSelection 各自只在一個地方被呼叫,都是淺方法;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 給了三個平衡「通用」與「專用」的問題:
- 能涵蓋當下所有需求的最簡介面是什麼? 在不削減能力下減少方法數,通常代表你正走向更通用的方法(一個
delete(start, end)取代backspace/delete/deleteSelection三個)。但這只在每個方法本身仍簡單時才算數——若得靠一堆額外參數才換來方法數變少,你並沒有真的在簡化。 - 這個方法會在多少情境下被使用? 只為單一特定用途而生的方法(如
backspace)是紅旗;試試能不能用一個通用方法取代多個專用方法。 - 這個 API 能輕鬆滿足當下需求嗎? 這題幫你判斷有沒有做過頭。把文字類別做成只能單字元
insert/delete,雖然又簡單又通用,但編輯器得寫滿迴圈、批次操作效能還差——所以內建字元範圍操作才對。
WARNING
通用不等於原始(primitive)。 如果使用者必須寫大量額外程式碼才能用你的類別完成當下任務,這是紅旗:介面沒有給出正確的功能粒度。通用要服膺於「常見用法易用」。
🔑 Takeaways
- 甜蜜點是稍微通用:功能反映當下需求,介面夠通用能支援多種用途。
- 通用介面通常比專用介面更簡單、更深——即使模組只用於原本用途,通用做法仍較好。
- 專用做法製造淺方法與資訊洩漏,把上下層綁死;通用做法帶來乾淨分離與更好的資訊隱藏。
- 小心假抽象:藏住一個呼叫端其實需要知道的細節,只是製造模糊。
- 用三問校準:最簡介面為何、方法會用在幾種情境、當下需求是否好滿足;但別把通用做成原始。
No notes yet — jot your takeaways or Q&A here.