l

2013年12月23日 星期一

例外處理實務做法(1):Toward Exception-Handling Best Practices

Dec. 22 12:43~15:03

image

 

寫了幾十篇例外處理的文章,分別介紹了例外處理重要名詞、物件導向語言的例外處理機制、Java checked exception的用意與實際應用上的問題、try-catch-finally責任分配、例外處理為什麼這麼困難、例外處理的強健度等級、例外處理壞味道、例外處理重構等。鄉民們可能以為例外處理設計的議題大概都談得差不多了,沒什麼戲可唱。如果這樣想就太單純了,有關例外處理的議題,還有很多東西可談。接下來花一點時間來談談例外處理實務做法(exception handling practice)

不同的人,不同的派別,對於例外處理,有著不同的建議。今天先介紹Rebecca J. Wirfs-Brock女士2006年發表在IEEE Software上面的一篇文章《Toward Exception-Handling Best Practices and Patterns》中所建議的9點作法,這裡可以下載作者提供的原文。

 

1. 不要處理程式設計錯誤

設計錯誤(coding error或programming error),例如除數為0、null point exception、index out-of-bound,這些都是所謂的design fault(請參考〈Fault、Error、Failure、Exception〉對於fault的解釋),或是一般人認知的程式bug。程式有bug就應該「把程式碼拿出來修正」,而不是用例外處理的方式來解決。例外處理要解決的是component fault,如果要用例外處理來解決design fault,則這種作法已經將例外處理提升到「fault tolerance programming(容錯設計)」的等級,所花費的時間與成本將大幅增加。

 

2. 避免宣告很多例外類別

只有在你認為新的例外將會以不同的方式來被處理的情況下,才宣告(定義)新的例外類別,否則盡量沿用現有的例外。這一條建議是提醒開發人員,避免盲目定義過多例外類別,因為每一個例外類都會占用系統資源,而且可能會干擾其他人的開發工作。

想像一下,一個系統有很多名稱不同,但是處理方法卻幾乎一樣的例外類別,這可能會對開發人員造成不必要的干擾。

 

3. 用哪裡出了問題幫例外命名,而不是用誰丟出了例外來命名

「命名」一直是撰寫易讀、易懂程式的基本功夫,像Kent Beck在《Implementation Patterns》(中文版:Kent Beck的實做模式),Robert C. Martin在《Clean Code》(中文版:無瑕的程式碼)都提到命名的技巧。對於例外類別的命名,應該以「發生什麼問題」來命名。例如一個ATM類別的withdraw()函數,負責實做提款功能。如果存款餘額小於提款金額,則withdraw()的執行會發生錯誤而丟出一個例外。假設鄉民們要設計一個新的例外類別來代表提款錯誤狀況,可能有以下幾個候選名稱:

  1. ATMException
  2. WithdrawException
  3. NotEnoughMoneyException。

前兩者的名稱比較接近「誰丟出了例外」,而NotEnoughMoneyException則是以「哪裡出了問題、出了什麼問題」來命名。如果鄉民覺得NotEnoughMoneyException太過詳細,如果用這種觀點來宣告例外,可能會違反第2條「避免宣告很多例外類別」的原則,那麼也可以設計一個比較一般性的TransactionException來代表交易例外,然後把NotEnoughMoney當成TransactionException的錯誤內容。

 

4. 將低階層的例外轉成高階層所理解的例外

這個實務做法建議鄉民們不要將實作例外(implementation exception)直接跨層(layer)傳遞給上層的人,因為這樣一來收到例外的人會被底層的實作細節給綁住,造成不必要的相依性。假設鄉民們有一個儲存設定檔的函數saveProperty(),這個函數的第一版實作採用檔案來儲存設定檔,所以當存檔失敗之後會丟出IOException:

public void saveProperty() throws IOException

如果有一天實作方法改變,改成用資料庫來儲存設定資料,則saveProperty()儲存失敗之後就不會丟出IOException,而是改丟出SQLException:

public void saveProperty() throws SQLException

上述兩種作法,都是將底層的實作例外直接丟給上層的人,並不是好的做法。所以在這裡可以套用第3條實務做法的建議,改丟一個PropertyManipulationException。

 

5. 伴隨著例外物件提供足夠的脈絡資訊

Teddy在〈例外處理的四種Context(1):Exception Context〉介紹過,例外物件是提供例外處理設計資訊的第一個候選人,舉凡像是丟出例外的函數所接受的參數、錯誤訊息、錯誤可能的原因、其他任何有助於設計錯誤復原的資料,都應該要一併提供。

 

6. 盡可能在接近問題發生處來處理例外

俗話說:「遠水救不了近火」,例外處理通常具有區域性,距離問題發生點愈近的地方,因為擁有local context與object context(請參考〈例外處理的四種Context(2):Object Context與Local Context〉),越有可能提供具體的例外處理或是狀態恢復方法。距離問題點越遠的地方,通常只能提供很一般性的例外處理方法,例如顯示錯誤訊息,或是將錯誤訊息記錄在日誌檔中。

 

7. 將例外處理責任指派給可以做決定的物件

雖然在第6點提到「盡可能在接近問題發生處來處理例外」,但是有時候距離問題很近的人權限或是授權不足,無法做決定。因此這一條實務做法建議,將例外處理責任指派給可以做決定的物件。其實這就是Teddy在〈例外處理的四種Context(3):Application Context〉所提到的,從軟體架構的角度來分析,看哪個物件有足夠的資訊可以做決定,就把例外交給這個物件來處理。

 

8.僅使用例外來發出緊急事件

這的實務做法的意思是只用例外來表示不預期的狀況,不要用例外作為改變程式控制流程的方法。這個關念Teddy在〈找不到資料要傳回Null還是丟出Exception?〉也介紹過,到資料庫去找學生的資料,如果找不到要傳回null或是Exception?因為找不到學生資料是很正常的一件事,所以在這個例子裡面,就不需要設計一個StudentNotFoundException來代表這個狀況。

 

9.不要反覆重丟相同的例外

如果捕捉例外僅僅只是為了寫入日誌檔,然後再把這個例外原封不動的重複丟出(rethrow),則這種作法其實沒有意義,也會傷害程式的效能。只需要在程式的最外面統一用一個Big Out Try Block來處理所有未被捕捉的例外即可,不需要在整個call chain裡面重複捕抓、丟出相同的例外。請參考〈敏捷式例外處理設計 (6):我到底哪裡做錯之 unprotected main program〉。

***

友藏內心獨白:實務做法都是從基本原理衍生而來的。

1 則留言: