Ousterhout 的原則:軟體設計很難,第一個冒出來的點子幾乎不可能是最佳設計。所以對每個重要的設計決策,都該考慮多個彼此差異夠大的選項,再從中挑選——或結合它們的優點。這就是「設計兩次」。它不只讓這次的設計更好,還會持續磨練你的設計技藝。
🧠 為什麼要多設計一次
- 第一個直覺不是最佳解:面對重要設計決策,別停在第一個點子。至少草擬幾個替代方案——不需要把每個方案的所有細節釘死,只要勾出幾個最重要的方法即可。
- 替代方案要「差異夠大」:盡量挑差異很大的方案,學到的東西最多。即使你確信只有一條合理路線,也至少考慮第二個設計——再爛都好,因為思考爛設計的弱點、再對比好設計的優點,本身就極具教育意義。
- 拿方案的缺點當線索:如果所有方案都不夠好,就再生一個。用原方案暴露的問題當提示——它會把你導向更對的方向。
- 可套用到多個層級:介面、模組實作、乃至高層次的系統拆解都適用。每個層級都是同樣的判斷:能比較幾個方案,就更容易看出最佳路徑。
NOTE
介面與實作的評估重點不同。設計介面時,最重要的考量是「對上層程式碼是否易用」;設計實作時,最看重的則是簡潔與效能。
⚖️ 同一個介面的兩種設計
以 GUI 編輯器的文字類別為例,設計介面時至少該擺出差異夠大的幾種風格來比:
方案 A — 行導向:以整行為單位插入、修改、刪除
// 介面以「行」為單位
class TextBuffer {
insertLine(index, text) {}
deleteLine(index) {}
replaceLine(index, text) {}
}
// 上層若要處理「跨行」或「部分行」的操作,得自己拆行、合行
buffer.replaceLine(3, buffer.getLine(3).slice(0, 5) + "X");行導向會把跨行 / 部分行的編排責任推回給上層。
方案 B — 字元導向:以個別字元插入、刪除
// 介面以「單一字元」為單位
class TextBuffer {
insertChar(pos, ch) {}
deleteChar(pos) {}
}
// 多字元操作只能靠迴圈;每個字元一次呼叫,可能顯著較慢
for (const ch of "hello") buffer.insertChar(pos++, ch);字元導向讓上層得用迴圈處理,且每個字元都呼叫一次文字模組,實作可能明顯較慢。
行與字元兩個方案都逼上層做額外的文字操作——這正是紅旗。它提示介面應該與上層真正的操作對齊,而這個推理會把你導向第三種、也是可能更好的設計:範圍導向,直接對任意範圍的字元(可跨行)操作。最佳設計常常不是三者之一,而是結合多個方案優點的新設計。
TIP
比較方案時可問自己幾個問題:哪個介面更簡單?哪個更通用?哪個能讓實作更高效?這些提問會逼出方案之間真正的取捨。
CAUTION
「設計兩次」對非常聰明的人特別難接受。他們成長過程中,第一個直覺往往就足以拿好成績,於是養成「一次就做對」的習慣。但職涯推進到更困難的問題時,第一個點子終究會不再夠好——大型軟體系統就是這種問題,沒有人能一次就做對。堅持實作第一個點子,反而讓他們的真實潛力被低估。這不是不夠聰明,而是問題真的很難。
🔑 Takeaways
- 對重要決策考慮多個差異夠大的方案,別停在第一個點子——第一個幾乎不會是最佳。
- 即使只想得到一條路,也至少擺一個對照方案:比較好壞本身就在教你什麼是好設計。
- 最佳解常是結合各方案優點的新設計;方案的缺點是導向它的線索。
- 額外時間很少(小類別大概一兩小時),相較實作的好幾天綽綽有餘;且這習慣會持續磨練你的設計技藝。
No notes yet — jot your takeaways or Q&A here.