在Google學術上搜到一篇有意思但也很有爭議的文章,題為《Best Practices for Exception Handling》O'Reilly Media。翻譯並解讀一下作者想表達的內容。
文章的最初始部分先闡釋了checked和unchecked異常的繼承關係。看:
而checked和unchecked的含義其實就是是否需要進行特定的檢查,即“當一個exception發生時,是否需要用catch塊將其接收消化或者用throws出去讓別人接收處理”。如果不用特意檢查,那麼就屬於unchecked類型的異常,如果需要檢查就是checked類型的異常。
Java的checked類型異常是爭議比較多的東西。因為在常見的OO語言中,Java是唯一擁有checked類型異常的語言,而C++和C#都是只有unchecked類型的異常而沒有checked類型的異常。
文中作者還提到了如何設計一個優良的API。
在java中,耦合的評價標準不只包括資料介面,還包括介面間的異常傳遞關係。請看文中給出的一個例子:
public List getAllAccounts() throws FileNotFoundException, SQLException{ ...}
可以看到getAllAccounts()方法向其調用者強制要求處理兩個異常,分別為FileNotFoundException和SQLException,這就產生了一種exception上的耦合。
1、什麼時候該使用checked類型的異常,什麼時候又該使用unchecked類型的異常呢?
文章的作者Gunjan Doshi 給出了一個判斷的依據,原文如下:
When deciding on checked exceptions vs. unchecked exceptions, ask yourself, "What action can the client code take when the exception occurs?"
If the client can take some alternate action to recover from the exception, make it a checked exception. If the client cannot do anything useful, then make the exception unchecked. By useful, I mean taking steps to recover from the exception and not just logging the exception. To summarize:
| Client's reaction when exception happens |
Exception type |
| Client code cannot do anything |
Make it an unchecked exception |
| Client code will take some useful recovery action based on information in exception |
Make it a checked exception |
作者認為,當API的調用者可以使用一系列步驟恢複程式的運行狀態時,就拋出checked異常給API的調用者處理。但是,如果這個異常是API的調用者無法恢複的,那麼久應該以unchecked的方式提出。
2、如何保護介面的封裝性呢?
作者的建議是不要讓有特定類型的異常上升到更高的層次上。比如不應該讓SQLException上升到商務邏輯層,而應該在資料介面層就將其處理掉。對於SQLException的處理,文章的作者認為可以這樣處理:
(1)將SQLException轉換成其他的checked異常。這種情況適用於用戶端代碼(即調用API的代碼)希望並且有能力處理這個SQL異常。
(2)將SQLException轉換成unchecked異常。這種情況適用於用戶端對SQL異常無能為力的情況。
在這兩種方式之間,文章的作者更傾向於後者,即封裝成unchecked異常。因為,封裝成unchecked異常拋出後,用戶端代碼不需要寫特定的catch塊進行SQL處理,並且封裝成unchecked異常後,可以讓系統將這個錯誤記錄在日誌中。另外,如果catch塊中希望知道這個異常發生的原因,可以調用exception對象的getCause()方法來獲得。文中,作者還提到大部分的時候,使用第二種方式便可以取得令人滿意的效果。原文:
If you are confident that the business layer can take some recovery action when SQLException occurs, you can convert it into a more meaningful checked exception. But I have found that just throwing RuntimeException suffices most of the time.
3、不要輕易建立自己的異常類
當你自己定義的異常類不提供任何可以擷取更多資訊的方法時,不要建立自己的類,使用標準異常類足矣。如果要定義自己的異常類,那麼可以在自己的異常類中增加一些可以提供輔助資訊的方法,如文中作者的樣本:
public class DuplicateUsernameException extends Exception { public DuplicateUsernameException (String username){....} public String requestedUsername(){...} public String[] availableNames(){...}}
可以看到,在定製的DuplicateUsernameException中增加了擷取請求的使用者名稱以及可用使用者名稱的方法。DuplicateUsername意味著重複的使用者名稱,即請求的這個使用者名稱已經被別人使用過了。那麼,在這個情況下,可以為使用者提供幾個與其所請求的使用者名稱相近的幾個可用的使用者名稱作為建議。但是,我個人認為提供建議的使用者名稱應該屬於商務邏輯的內容,不應該參雜在異常處理的類中來完成。異常處理應該專註於恢複異常、記錄異常以及顯示提示資訊。
但是像文中作者提出的提供建議使用者名稱的方式,如果可以協助從這個異常中恢複出來,那麼也屬於異常處理的範疇。這就要看業務塊的劃分了。究竟是“完成一次註冊”算是一個使用者操作塊,還是“成功註冊”算是一個使用者操作塊。如果是前者,那麼提示建議使用者名稱的方法就可以放在異常處理的代碼塊中。如果是後者,那麼“註冊失敗”就和“註冊成功”是同等的操作塊,而“提示建議使用者名稱”就是另一個與其相同層次上的操作,就不應該放到異常處理中了。
所以,業務塊、操作塊的粒度劃分也會影響到異常處理的職責設定。
4、為你的異常撰寫說明
在Java中,可以用Javadoc的 @throws 標籤撰寫異常的說明。但是文中作者更推薦用單元測試的方式來說明一個異常。如:
public void testIndexOutOfBoundsException() { ArrayList blankList = new ArrayList(); try { blankList.get(10); fail("Should raise an IndexOutOfBoundsException"); } catch (IndexOutOfBoundsException success) {}}
通過撰寫單元測試的方式來說明一個異常有兩個好處,一是可以讓調用API的人員更清楚這個異常產生的機制,另一個原因是可以使用這個單元測試來測試此異常,以增加代碼的魯棒性。
使用Exception的最佳範例
1、做好代碼的清理工作
如果你的代碼中使用了一些資源,如資料庫連接、網路連接等,那麼應該及時的關閉這些串連。關閉串連的代碼應該放在finally塊中,這樣可以保證不論是否有異常發生,資源串連都可以進行關閉。如文中給出的範例:
public void dataAccessCode(){ Connection conn = null; try{ conn = getConnection(); ..some code that throws SQLException }catch(SQLException ex){ ex.printStacktrace(); } finally{ DBUtil.closeConnection(conn); }}class DBUtil{ public static void closeConnection (Connection conn){ try{ conn.close(); } catch(SQLException ex){ logger.error("Cannot close connection"); throw new RuntimeException(ex); } }}
2、不要使用exception來進行流程式控制制
查看文中給出的一個例子:
public void useExceptionsForFlowControl() { try { while (true) { increaseCount(); } } catch (MaximumCountReachedException ex) { } //Continue execution}public void increaseCount() throws MaximumCountReachedException { if (count >= 5000) throw new MaximumCountReachedException();}
可以看到例子中使用了MaximumCountReachedException來處理計數達到一定數量後的情況。這樣做有兩個壞處:一是使代碼不易理解,二是將異常處理混入到了演算法邏輯中。異常處理應該只使用在需要處理異常的情況下,而不應該越界到商務邏輯的範圍內。
3、不要壓制(suppress)或忽略exception
當一個API要求你處理某個exception時,說明它希望你能採取一些措施以應對這個異常。如果你也對這個異常束手無策的話,不要只是catch它然後讓它在catch塊中被忽略掉,而是至少將它轉換成一個unchecked異常,然後拋出。
4、不要捕獲底層的異常
不管是unchecked類型的異常還是checked類型的異常,都是繼承自Exception這個基類。如果在代碼中,直接catch這個基類的話,那麼所有的unchecked異常也都被捕獲了。另外,如果在捕獲Exception基類時,不做任何處理,反而會影響到unchecked異常的處理流程,因為作為Exception的基類,unchecked類型的異常也被捕獲了。
5、一個異常不要進行多次記錄
一個異常如果進行多次記錄會讓程式員在追蹤異常線索時很混亂。
-------------------
這是一篇03年的文章,文章下面的評論處有很多贊成也有很多反對的聲音。現在過了近10年,作者提出的“在catch塊中關閉資源串連”、“一個異常不進行多次記錄”的建議似乎已經得到業內的共識。但是別的部分還是有爭議。
下一次解讀 Tim McCune 的《Exception-Handling Antipatterns》。該文是《Best Practices for Exception Handling》的反對者比較推崇的文章,大致看了一下,確實是比這篇解讀的文章更周密更有趣,有興趣的朋友可以先一讀。