例外處理設計筆記(2) – 常見的例外設計問題

常見的例外設計問題

直接顯示過於底層的Exception

在例外處理設計的逆襲中,提到一個案例(p.38)。

使用Android手機拍照,突然有一天顯示錯誤訊息「IO存取錯誤」,使用者往往看了會一頭霧水,到底是什麼原因導致錯誤?後來經過 Terry 的調查後,發現是記憶卡滿了。

那為什麼錯誤訊息不顯示「記憶卡已滿,請清除記憶卡空間」呢?因為懶惰的開發者直接把最底層的例外往上拋,中間沒有做更多包裝處理。

越底層、共用程度越高的元件、Library在設計時,並沒辦法事先考慮這個Library會被用在哪些情境。因此它們的例外類別往往會更為通用。站在設計者的角度來說,檔案系統可能在不同的硬體上實作,一律視為IOException能簡化設計,避免整個系統中存在太多例外類別。

另一個面向是:「越底層的元件,被重複使用的機會越高,如果設計的時候就針對某一個特定的應用情境做最佳化,會導致該元件在其他情境重複使用的機會減少。」(p.65)

通常要到應用端,才會有足夠的context知道「怎麼處理」,因此會依賴開發者「站在使用者角度思考」,這個例外代表什麼狀況。

找不到資料要回傳 NULL 還是丟出 Exception?

這是書中 p.55 的案例。設計一個依據學號到資料庫中查詢學生資料的函數,找不到符合條件的資料,該丟出例外嗎?

對於回傳List<Student>的函數,其實回傳一個空List即可。

對於回傳單一Student的函數

這時候就要問:「依據Id找學生資料,最後找不到任何一筆符合,算正常狀況還是異常狀況?」

通常我們會認為這是正常狀況,因為日常生活中,本來就有可能發生依據某些條件去找資料,但是卻什麼都找不到。所以我們不會針對這種狀況丟出例外。我們可以使用 null 代表找不到、或是用 NullObject設計模式。

在例外設計中,Exception 可以表示 error 或是 failure,error 代表目前可能處在不正確的狀態,failure 代表函數沒辦法照預期的規格執行。而找不到資料,我們會認定成「找資料的函數正確的執行完畢,只是剛好沒有找到符合條件的資料」,既不是錯誤、也不是失效,這種狀況我們就設計好函數介面,回傳代表null的值就好。

元件沒有盡自己的職責

例外的處理非常取決於當下的情境。舉例來說,今天收到 EOF(End of File)例外,有可能是正常情況(通知讀到盡頭了),也有可能是錯誤情況(資料長度不足),完全取決於當下讀的情境是什麼。

另外一種常見的問題是,當前無法處理該例外,但也沒有攜帶更多執行期資訊,就直接把例外往上拋。接收到的人會一頭霧水,不知道該怎麼處理。舉例來說,使用端收底層的IOException,卻沒有存取的路徑資訊,根本不知道該怎麼辦。這個問題可以透過建立巢狀的自定義例外類別解決。

讓每個抽象層級做好自己該做、能做的事情,例外處理才會變得簡單易懂。

把例外處理過度設計成容錯設計

p. 83 談到例外處理與容錯設計

當遇到 NullPointerException時,通常是變數忘了初始化,很多人會很直覺地 catch 例外並恢復狀態

但這種狀況,其實並不是在做例外處理,而是在做更高層次的容錯設計。

當系統出現不正確的狀態時,錯誤處理可以分成兩類

  • 例外處理(Exception Handling):負責處理 component fault,可預期的錯誤狀況。
  • 容錯設計(fault-tolerant programming):負責處理 design fault,是預期之外的錯誤狀況。

真的要說,其實每一個使用物件的敘述都可能發生 NullPointerException。但九成九不會發生,NullPointerException 可以當成是預期之外的錯誤狀況。較為輕鬆的處理方式應該是「在 constructor就確保 list 已經被正確的初始化」,就可以省掉catch NullPointerException。一開始這個錯誤就不該存在,既然是預期之外,也不用特地去處理。

容錯設計通常是指,「就算程式有錯、系統有錯,也要正常執行」,通常是用在飛機、軍事、醫療、衛星太空用途,在這種需要非常高可靠性的系統,才會強調容錯。大多數的商業應用如果硬要套用容錯設計,通常是一開始就有東西搞錯了,不去解決一開始的問題而是用例外處理去做容錯設計,會讓設計變得非常複雜。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *