整理書中提到一些常見的例外處理 Code Smell,程式碼中的壞味道,以及修正手法。
1. 使用 Return Code 代表例外狀況 (Return Code)
1 2 3 4 5 6 |
function () { if (somethingError) { return -1 } // normal logic } |
缺點:
- 呼叫端可能會忘記處理例外狀況(因為要記得做特殊判斷)
- 正常邏輯和例外邏輯交錯,增加維護的困擾
修正方式:
在有支援例外的語言,用拋出例外來代替回傳值。並思考例外要怎麼設計。(錯誤是屬於design fault 還是 component fault),可以用一些重構的技巧來降低修改風險。
2. 忽略例外 (Ignored Exception)
1 2 3 4 5 6 7 |
function () { try { // working } catch (e) { // do nothing } } |
缺點:
隱藏潛在問題,明明有問題卻無法意識到
修正方式:
至少把例外拋出來,讓其他人注意到。不用擔心沒人處理,我們可以透過在最外層加上try catch的手法來處理 (Avoid Unexpected Termination with Big Outer Try Block)。
3. 未被保護的主程式(Unprotected Main Program)
1 2 3 4 |
function main() { const app = new App() app.start() } |
缺點:
主程式沒有捕捉傳遞到自己身上的例外,會讓程式不預期的終止執行。
修正方式:
在最外層使用try敘述避免意料之外的終止 (Avoid Unexpected Termination with Big Outer Try Block)
1 2 3 4 5 6 7 |
function main () { try { // real work } catch (e) { console.log(e) } } |
4. 虛設的例外處理程序 (Dummy Handler)
這是一種很常見的例外處理方式,印出來然後就不管了。
1 2 3 4 5 6 7 |
function () { try { // working } catch (e) { console.log(e) } } |
缺點:
- 現在的佈署環境中,往往不容易看到std out上的訊息,並不好察覺
- 程式可能處於錯誤狀態(因為沒有處理)
修正方式:
同2,把例外拋出來,再由最外層去處理,就不用在每個地方都要寫log。
5. 巢狀 Try 敘述 (Nested Try Statement)
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function () { try { // working } catch () { // handle exception } finally { try { // release resource } catch () { // handle release resource fail exception } } } |
缺點:
較不易維護
修正方式:
以函式取代巢狀敘述 (Replace Nested Try Statement with Method),關鍵在以意圖取名新函式而非怎麼做取名。
6. 備胎的例外處理程序 (Spare Handler)
把 catch block 當成備案,如果try失敗了,就在catch內執行備案
1 2 3 4 5 |
try { // 主要實作 } catch () { // 替代方案 } |
這種處理方法叫做「採用替代方案重試」,本身沒有問題,但寫在 catch block 會造成只能重試一次,如果要重試多次就會變成 nested try。
缺點:
- 只能重試一次
- 會把「決定錯誤怎麼處理」和「真正的替代方案」這兩個關注點混在一起寫。
修正方式:
引入多才多藝的 try 區塊 (Introduce Resourceful Try Block)
遇到問題的時候,重試是一種很常見的作法。像是網頁連線出問題時,大家第一個反應是手動重新整理。在程式中我們會寫 while 來模擬重試。
1 2 3 4 5 6 7 8 9 10 11 12 |
// 這樣寫比較不好 public User readUser(String name) throws ReadUserException { try { return readFromDatabase(name); // 可能丟出 SQLException } catch (Exception e) { try { return readFromLDAP(name); // 可能丟出 IOException } catch (IOException ex) { throw new ReadUserException(ex); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 這樣寫比較好 public User readUser(String name) throws ReadUserException { final int maxAttempt = 3; int attempt = 1; while (true) { try { if (attempt <= 2) return readFromDatabase(name); else return readFromLDAP(name); } catch (Exception e) { if (++attempt > maxAttempt) throw new ReadUserException(e); } } } |
粗心的資源釋放 (Careless Cleanup)
資源沒有正確地釋放,會導致資源耗盡並降低系統穩定度。
1 2 3 4 5 6 7 8 |
try { fs = new FileInputStream() fs.close() // 不該寫在這裡, 因為要是例外在前一行發生,就無法釋放資源 } catch { // ... } finally { // fs.close() 應該寫在這裡 } |
或是
1 2 3 4 5 6 7 8 |
try { // ... } catch () { // ... } finally { res1.close() // 要是這裡發生例外,那res2就不會正確被關閉 res2.close() } |