消除例外處理複雜性的最佳做法:定義 API 時讓錯誤根本不存在。
聽起來像褻瀆,但實務上極其有效。
Tcl unset 的修正版本#
回到 Tcl unset 的例子:
- 原本:
unset拋例外當變數不存在 - 修正後:
unset不再拋例外,遇到不存在的變數直接什麼都不做就回傳
語意上的微調是關鍵:
| 舊定義 | 新定義 |
|---|---|
| 「刪除一個變數」 | 「確保某變數不存在」 |
| 變數不存在 → 工作做不了 → 必須報錯 | 變數不存在 → 工作已經完成 → 直接回傳 |
透過調整定義的「義務」描述,把錯誤條件直接消失——不再有任何「錯誤情境」需要回報。
範例:Windows 的檔案刪除#
Windows 的設計:
- 檔案被某行程開著時,禁止刪除
- 使用者必須找到佔用檔案的行程並 kill 它
- 有時甚至重開機才能刪檔
Unix 的設計優雅得多:
- 檔案被開著時呼叫 delete → 不立即刪除,而是標記為待刪
- 檔名從目錄移除(其他行程無法再開到舊檔;同名新檔可建)
- 已開啟該檔的行程繼續正常讀寫
- 全部關閉後資料才釋放
Unix 一次消除了兩種錯誤:
- 檔案使用中時 delete 不再失敗
- 不會對正在使用該檔案的行程引發新例外
替代設計(立即刪除並讓所有開啟失效)會製造新錯誤讓行程處理。延遲刪除把錯誤從定義消除。
「Unix 允許行程繼續讀寫一個註定要被刪除的檔案」聽起來很奇怪,但作者從未見過這引發顯著問題。
範例:Java substring#
Java 的 String.substring(begin, end):
- 只要任一索引超出字串範圍 → 拋
IndexOutOfBoundsException
問題:當你想**抓取「字串中與指定範圍重疊的部分」**時——
- 一兩個索引可能超出範圍
- 因為原 API 會拋例外,呼叫端必須自己 clamp 索引
- 一行 API 變成 5 ~ 10 行程式碼
改善版 API#
「回傳所有索引大於等於
beginIndex且小於endIndex的字元(如果有)。」
- 簡潔、自然
- 無論索引是否負數、
beginIndex > endIndex,行為都已定義良好 - API 同時變簡單與更強——方法變得更深
Python 已是這個模式:list slicing 超出範圍只回空。
「讓錯誤消失會不會反而隱藏 bug?」#
有人擔心:拋例外能抓 bug,消除錯誤不會讓 bug 更難發現嗎?
多錯誤的 API 確實可能抓到一些 bug,但也增加複雜度——而複雜度本身會引發新的 bug。
- 開發者得寫額外程式碼避開 / 忽略錯誤
- 忘記寫額外程式碼 → runtime 拋出意外例外
「把錯誤消除」反而讓 API 簡單、需要寫的程式碼變少。
減少 bug 的最佳辦法是讓軟體更簡單。