開發模組時遇到一段無法避免的複雜性,只有兩條路:讓使用者扛,或讓模組內部自己扛。Ousterhout 的主張是——只要這複雜性和模組提供的功能有關,就該由模組內部吸收。介面簡單比實作簡單更重要;開發者多吃點苦,換使用者少吃苦,這筆帳幾乎永遠划算。
🧠 Why It's a Problem
- 使用者遠多於開發者:大多數模組被寫一次、被用無數次。讓少數開發者多做一點,勝過讓每一個使用者都多學一點、多處理一點。
- 「解決容易的、把難的丟給別人」是誘惑:不確定某條件怎麼處理,就拋例外讓呼叫端接;不確定要用哪種策略,就開幾個設定參數讓管理員自己決定。短期你輕鬆了,但複雜性沒有消失,只是被往上推。
- 往上推會被放大:拋出的例外,每一個呼叫端都得處理;暴露的設定參數,每一個部署它的管理員都得搞懂怎麼設。你省下的一次麻煩,變成了 N 份麻煩。
- 這是讓模組變深的另一個角度——不是靠功能疊厚,而是靠主動把痛苦攬到介面底下。(深模組本身見 深模組。)
IMPORTANT
判準很簡單:若複雜性與模組所提供的功能相關,通常應由模組內部處理。身為開發者,目標是讓使用者的生活盡可能輕鬆——即使這意味著你自己要多做點事。
⚖️ 設計對照(往上推 vs. 往下拉)
設定參數是最常見的「往上推」形式:表面上你給了使用者彈性,實際上你只是把「該用什麼值」這個你答不出來的問題,轉手給了每一個部署者。
往上推 — 用設定參數把決策丟給使用者
// 開發者不確定緩衝區該多大,於是開一個旋鈕丟出去
class FileReader {
constructor(path, { bufferSize } = {}) {
// 沒有預設;使用者不填就出事
this.bufferSize = bufferSize;
this.path = path;
}
}
// 每一個呼叫端都被迫回答一個它其實不懂的問題
const r = new FileReader('/data/log', { bufferSize: 8192 });「8192 好還是 65536 好?」這個問題你沒有比使用者更會答——但你比使用者更有機會去量測、去挑一個好預設。把它丟出去,等於讓每個部署現場各自猜一次。
往下拉 — 內部挑一個合理預設,介面收乾淨
// 模組自己吸收「該用多大緩衝」這個複雜性
class FileReader {
constructor(path) {
this.path = path;
this.bufferSize = this.#pickBufferSize(path); // 依檔案大小自動決定
}
#pickBufferSize(path) {
// 開發者做一次功課,換所有使用者不必做
/* ...量測/啟發式,藏在門後... */
}
}
// 呼叫端只需表達意圖
const r = new FileReader('/data/log');使用者不再需要懂緩衝這件事。要調整策略、換啟發式,改的是模組內部,呼叫端一行都不用動。多出來的那點功課由開發者一次付清。
TIP
這不代表消滅所有設定參數。有些值真的只有部署現場才知道(例如外部服務位址)。問自己:這個決定,是我比使用者更有能力做,還是真的只有他們知道? 前者往下拉,後者才往上留。
WARNING
也別把這原則推過頭。往下拉只在複雜性與本模組功能相關、且你確實能替使用者做得更好時才成立。若你把不屬於這裡的複雜性硬吞進來,或吞下的東西使用者其實常常想自己控制,反而會讓模組變得又笨重又難用——痛苦沒有減少,只是搬錯了地方。
🔑 Takeaways
- 面對無法避免的複雜性,預設讓模組內部吸收,而不是往使用者身上推。
- 記住那句話:讓開發者吃苦,好讓使用者不必吃苦;因為使用者遠比開發者多。
- 警惕「往上推」的兩個誘惑:亂拋例外、亂開設定參數——它們把你一次的麻煩複製成每個呼叫端 / 每個部署的麻煩。
- 設定參數的檢驗:只有當那個值真的只有使用者知道時才暴露;凡是你能靠量測挑出合理預設的,就自己挑。
- 別走過頭:只吞下與本模組功能相關、且你確實能做得更好的複雜性。
No notes yet — jot your takeaways or Q&A here.