本篇收集觀看Effective Java針對Exceptions章節後所整理的心得。
Use checked exceptions for recoverable conditions and runtime exceptions for programming errors
這個Item在強調例外的使用時機,包含
- Checked Exceptions應用在recoverable conditions
- Unchecked(Runtime) Exception應用在programming errors
下一個問題就是怎麼定義recoverable conditions與programming errors。我分享笑談軟體工程:例外處理設計的逆襲中的觀點:
- Programming errors: design fault造成的,通常代表是bug;因為是bug,所以retry 1億次也還是錯的。
- Recoverable conditions: component fault造成的,可能是軟體元件、硬體元件突然發生故障,或與其溝通的過程中發生故障而導致的暫態缺陷,透過retry是有機會修復的。如果是永久缺陷,可能就需要排除原因才有機會執行成功。
根據上面的定義,就可以知道什麼時候該拋checked exception,什麼時候該拋unchecked exception了。對應到REST API的回應結果,unchecked exception預設通常都會是5xx錯誤,而checked exception就需要交給client去識別,是屬於需要user介入處理的4xx錯誤,還是server暫時的5xx錯誤。
除此之外,這個Item還有提到幾個設計要注意的事情:
- 按照慣例,Error應保留給JVM使用,用以表明程序無法繼續執行的情況。所以開發人員通常不該去Extend Error。
- 例外本身是有著設計上的代表意義,可以在類別上定義能供給Client做進一步處理的介面。不建議Client使用例外本身的字串訊息去做例外處理,因為字串訊息說變就變,程式容易出問題。
Reference:
- Effective Java, 3/e, Item 70。
- 搞笑談軟工 - Checked or unchecked exceptions (1)
Favor the use of standard exceptions
與之前Favor the use of standard functional interfaces強調概念類似,盡量reuse現有東西。書中的範例主要都是針對unchecked exception,去說明適用情境:
- NullPointerException: 不允許輸入參數為null時,就會使用它。可以直接使用Objects.requireNonNull去做檢查。
- IndexOutOfBoundsException: 輸入參數超過可接受的範圍大小,就會使用它。
- IllegalArgumentException: 輸入參數不合法,就會使用它,但請撇除上面2個情境。
- llegalStateException: 當method在不適當的時候被呼叫,就會使用它。其實以物件命名想表達的是物件可能處於error state而無法正常工作,因而拋出llegalStateException。有的時候,這個例外我也有看過被用在絕對不可能發生的情況下去拋出llegalStateException;不過應該要盡量避免這種情境,因為單元測試可能寫不出來。StackOverflow有人對javadoc和effective java描述的使用情境認為有所不同,有興趣可以看這裡,我認為effective java描述的情境可應用的場合會比javadoc廣泛拉。
- UnsupportedOperationException: 呼叫了不支援的method,就會使用它。我想這個例外,應該也適用於某一個method接收http method,但不支援PUT的時候,client卻傳了PUT的情境吧?
- ConcurrentModificationException: 如果某個method偵測到multiple threads存取時,就有可能會拋這個例外。書中是說,這個是提示意味不一定準確,而我也沒拋過這個例外,因為通常會去避免發生這種情境,而不會做複雜的檢查。
最後就是這個item也有提到不要直接使用Exception、Throwable、RuntimeException或Error,我認為其實是要呼應Item 73: Throw exceptions appropriate to the abstraction。
Reference:
- Effective Java, 3/e, Item 72。
Throw exceptions appropriate to the abstraction
這個Item講了幾個重要概念:
- 要將例外從某個物件拋出去的時候,應該拋出與這個物件相襯的例外,而不要直接將底層來的例外拋出去。這個是要避免client與實作細節直接相依而很容易造成改變。除非這個底層例外對於更上層來說,也是合適的例外。
- 將例外轉換且重新包裝(exception wrapping)的時候,要將底層例外設為新例外的cause,以利於client去做完整的log或其它處理。
- 勿濫用這個重新包裝機制,盡量消除會造成發生底層例外的原因。例如先做precheck,避免呼叫底層API;或者是在原先API發生問題的時候,進行例外處理,讓服務不會受到中斷,並且log以便之後備查。
第一點與第二點非常同意。以第一點來說,通常我們以不同模組、階層的角度去設計對應的例外,不同模組、階層以Clean Architecture來說,如Gateway、Domain、UseCase layer等,會設計不同的Checked Exception並搭配Homogeneous Exception(同質例外)降低介面演進的機會。關於Homogeneous Exception(同質例外)可以參考搞笑談軟工提及的內容。
第三點的話,我就會看情況而決定。假如我需要多做precheck或者是在API發生問題的時候去讓服務可以正常使用,這些勢必會額外增加開發與維護成本。
Reference:
- Effective Java, 3/e, Item 73。
Strive for failure atomicity
這個Item主要在宣導「在工作失敗時,要努力讓物件的狀態完好如呼叫前」。如果method滿足這個條件,就可以稱為具有failure-atomic的method。(Note.要考慮的不只有待操作的物件,應還有做為輸入參數的物件)
第一個方法是操作Immutable object。因為它不會被改變,自然而然就滿足所謂的failure-atomic。依照過往經驗,如果要回傳一個Collection,盡量讓它是Immutable object只用於計算,並避免直接的修改造成side effect。
第二個方法就是在造成物件狀態改變之前,做precheck。假如不做檢查讓物件進入了錯誤的狀態,反而可能會衍生更多問題,且不好debug。
第三個方法就是先把物件複製一份在操作。就算發生問題,也不會影響到原本的物件狀態。
最後一個方法,就是要實做一段recovery code,好讓發生問題時,可以rollback回原本的狀態。像是使用資料庫的Transaction,或操作檔案前先複製一份,有問題在還原檔案等等。
使用這些方法,都要注意一下“代價”與“多執行緒存取”的問題。像要複製物件或準備環原點,都會存在著性能開銷,且程式碼往往會複雜一些些;比較低成本的Immutable object不一定適用於各種情況;precheck則大部分情況都有可能要注意多執行緒的存取,假如是依賴目標物件的狀態做precheck的話。
另外,書中有建議,如果無法滿足failure-atomic的method,“建議”要寫註解讓用的人知道。
Reference:
- Effective Java, 3/e, Item 76。
Don't ignore exceptions
這個是大家都知道的“不該忽略例外”,但卻都會忽略例外的處理,或者是亂處理。如果有裝像SonarLint檢查工具,在你沒處理例外時,它一定會通知你。沒去處理例外最大的問題,就是當它發生問題的時候,你完全不會知道;針對這種Bad Smell可以參考link內容去做對應的重構。除了沒處理的情況外,我這裡也列一些可能亂處理的情況供大家參考:
Reference:
- Effective Java, 3/e, Item 77。
- 敏捷式例外處理設計 (3):我到底哪裡做錯之 ignored checked exception
留言
張貼留言