Skip to main content

例外處理設計筆記(3) – 例外處理的 Code smell

整理書中提到一些常見的例外處理 Code Smell,程式碼中的壞味道,以及修正手法。

1. 使用 Return Code 代表例外狀況 (Return Code)

function () {
if (somethingError) {
return -1
}
// normal logic
}

缺點:

  1. 呼叫端可能會忘記處理例外狀況(因為要記得做特殊判斷)
  2. 正常邏輯和例外邏輯交錯,增加維護的困擾

修正方式:

在有支援例外的語言,用拋出例外來代替回傳值。並思考例外要怎麼設計。(錯誤是屬於design fault 還是 component fault),可以用一些重構的技巧來降低修改風險。

2. 忽略例外 (Ignored Exception)

function () {
try {
// working
} catch (e) {
// do nothing
}
}

缺點: 隱藏潛在問題,明明有問題卻無法意識到

修正方式:

至少把例外拋出來,讓其他人注意到。不用擔心沒人處理,我們可以透過在最外層加上try catch的手法來處理 (Avoid Unexpected Termination with Big Outer Try Block)。

3. 未被保護的主程式(Unprotected Main Program)

function main() {
const app = new App();
app.start();
}

缺點: 主程式沒有捕捉傳遞到自己身上的例外,會讓程式不預期的終止執行。

修正方式:

在最外層使用try敘述避免意料之外的終止 (Avoid Unexpected Termination with Big Outer Try Block)

function main() {
try {
// real work
} catch (e) {
console.log(e);
}
}

4. 虛設的例外處理程序 (Dummy Handler)

這是一種很常見的例外處理方式,印出來然後就不管了。

function () {
try {
// working
} catch (e) {
console.log(e)
}
}

缺點:

  1. 現在的佈署環境中,往往不容易看到std out上的訊息,並不好察覺
  2. 程式可能處於錯誤狀態(因為沒有處理)

修正方式:

同2,把例外拋出來,再由最外層去處理,就不用在每個地方都要寫log。

5. 巢狀 Try 敘述 (Nested Try Statement)

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內執行備案

try {
// 主要實作
} catch () {
// 替代方案
}

這種處理方法叫做「採用替代方案重試」,本身沒有問題,但寫在 catch block 會造成只能重試一次,如果要重試多次就會變成 nested try。

缺點:

  1. 只能重試一次
  2. 會把「決定錯誤怎麼處理」和「真正的替代方案」這兩個關注點混在一起寫。

修正方式:

引入多才多藝的 try 區塊 (Introduce Resourceful Try Block)

遇到問題的時候,重試是一種很常見的作法。像是網頁連線出問題時,大家第一個反應是手動重新整理。在程式中我們會寫 while 來模擬重試。

// 這樣寫比較不好
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);
}
}
}
// 這樣寫比較好
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)

資源沒有正確地釋放,會導致資源耗盡並降低系統穩定度。

try {
fs = new FileInputStream()
fs.close() // 不該寫在這裡, 因為要是例外在前一行發生,就無法釋放資源
} catch {
// ...
} finally {
// fs.close() 應該寫在這裡
}

或是

try {
// ...
} catch () {
// ...
} finally {
res1.close() // 要是這裡發生例外,那res2就不會正確被關閉
res2.close()
}