核心觀點#

錯誤處理是軟體開發中不可或缺的一部分,但如果處理不當,
錯誤處理的程式碼往往會支配並模糊了主要的業務邏輯。

Martin 強調:錯誤處理很重要,但如果它讓主要邏輯變得模糊不清,那就是錯誤的做法。
整潔的程式碼應該將「處理錯誤的邏輯」與「正常的業務邏輯」分離。

  • 使用例外 (Exceptions) 取代錯誤碼 (Error Codes)
  • 別回傳 Null,也別傳遞 Null
  • 將錯誤處理視為一個獨立的關注點(Concern)

錯誤處理的結構策略#

1. 使用例外而非錯誤碼#

早期的程式語言常用回傳「錯誤碼(Error Flags/Codes)」的方式來處理異常,
這會導致呼叫者必須在每次呼叫後立即檢查錯誤,導致主流程充滿了 if (err != OK) 的巢狀結構。

使用例外(Exceptions)可以讓主邏輯保持乾淨,將錯誤處理交由專門的 catch 區塊負責。

2. 先寫 Try-Catch-Finally#

try 區塊某種程度上像是一種交易(Transaction)
無論在 try 過程中發生什麼事,catch 區塊都應該讓程式維持在一致的狀態。

  • 實踐建議: 當你撰寫可能拋出例外的程式碼時,先寫下 try-catch-finally 結構。
    這能幫你定義該區塊的使用者(Caller)發生問題時應預期的狀態

3. 使用不檢查例外 (Unchecked Exceptions)#

這是個針對 Java 等強型別語言的建議。
雖然「檢查型例外(Checked Exceptions)」強迫開發者處理錯誤看似安全,
但它違反了開放閉合原則 (Open/Closed Principle)

  • 代價: 如果底層函式修改了它拋出的 Checked Exception,
    則所有上層呼叫它的函式都必須修改簽章(Signature),這破壞了封裝性
  • 建議: 大多數情況下,使用 Unchecked Exceptions(Runtime Exceptions)
    能讓程式碼結構更穩定

優化錯誤的使用體驗#

1. 提供情境資訊 (Context)#

當例外發生時,不要只拋出一個 Stack Trace。
你拋出的例外應該包含足夠的上下文,讓維護者能判斷錯誤發生的原因與位置。

  • 建議包含:操作失敗的意圖、失敗時的相關數據

2. 依據「呼叫者的需求」定義例外類別#

在封裝第三方 API 或設計模組時,
不要直接將底層的所有例外(如 SocketTimeout, ConnectionRefused…)全部拋給上層。

  • 包裝(Wrapping): 應該將這些底層錯誤包裝成對呼叫者有意義的通用例外(例如 PortDeviceFailure)。這樣不僅簡化了呼叫者的依賴,也讓替換底層 Library 變得更容易

3. 定義正常流程 (Define the Normal Flow)#

有時候,為了處理「特殊情況」而中斷業務邏輯去寫 try-catch 會讓程式碼變得很醜。
這時可以使用 特例模式 (Special Case Pattern)

範例:特例模式 (Special Case Pattern)

重構前 (充滿防禦性檢查): 我們須不斷檢查是否有午餐紀錄,沒有則顯示預設值。

try:
    meal = expense_report_dao.get_meals(employee_id)
    total += meal.get_total()
except MealNotFoundError:
    total += get_per_diem_default() # 處理例外邏輯

重構後 (使用特例物件): 修改 get_meals,當找不到資料時,回傳一個「特例物件(PerDiemMeal)」,該物件本身就知道如何回傳預設值。

# 呼叫端不需要知道是否有例外,流程更順暢
meal = expense_report_dao.get_meals(employee_id)
total += meal.get_total()

關於 NULL 的詛咒#

1. 別回傳 NULL (Don’t Return Null)#

回傳 null 基本上就是給呼叫者製造工作(強迫他們寫 if check)。
只要漏掉一個檢查,應用程式就會失控。

  • 替代方案:
    • 如果回傳型別是 List,請回傳 Empty List [] 而非 null。
    • 使用 Optional 物件或上述的「特例模式」物件。

2. 別傳遞 NULL (Don’t Pass Null)#

在參數中傳遞 null 比回傳 null 更糟糕。
除非 API 明確設計為可接受 null,否則應盡量避免。這會導致函式內部充滿無意義的防禦性檢查。

如果發現程式碼充滿了 if (item != null) 的檢查,這通常代表您的 API 設計有問題,
或者是對這段程式碼的錯誤處理機制規劃不足。


結論#

整潔的錯誤處理邏輯能讓程式碼更健壯。

  1. 將錯誤處理隔離,不要讓它污染主要邏輯
  2. 利用例外而非錯誤碼
  3. 利用特例模式和空集合來減少 Null 檢查與例外捕捉的頻率