例外處理設計筆記(3) – 例外處理的 Code smell
整理書中提到一些常見的例外處理 Code Smell,程式碼中的壞味道,以及修正手法。
1. 使用 Return Code 代表例外狀況 (Return Code)
function () {
if (somethingError) {
return -1
}
// normal logic
}
缺點:
- 呼叫端可能會忘記處理例外狀況(因為要記得做特殊判斷)
- 正常邏輯和例外邏輯交錯,增加維護的困擾
修正方式:
在有支援例外的語言,用拋出例外來代替回傳值。並思考例外要怎麼設計。(錯誤是屬於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)
}
}
缺點:
- 現在的佈署環境中,往往不容易看到std out上的訊息,並不好察覺
- 程式可能處於錯誤狀態(因為沒有處理)
修正方式:
同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。
缺點:
- 只能重試一次
- 會把「決定錯誤怎麼處理」和「真正的替代方案」這兩個關注點混在一起寫。
修正方式:
引入多才多藝的 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()
}