錯誤處理


錯誤處理很重要,但如果它模糊了原本程式碼的邏輯,那就不對了。


使用例外事件而非回傳錯誤碼

拋出一個例外事件比回傳錯誤碼好的原因,在於原先糾纏在一起的兩件事情,現在被分開處理了。你現在可以分別看待各個事情,然後瞭解它們。

在開頭寫下你的 Try-Catch-Finally 敘述

try 區塊就像是種交易處理 (transaction)。try-catch-finally 敘述能夠幫助你定義,不論 try 區塊裡發生什麼意外,什麼該是使用者預見的結果。

使用不檢查型例外 (Use Unchecked Exception)

檢查型例外:
  • 違反開放閉合原則 (Open/Closed Principle )
  • 必須替拋出例外與 catch 之間的所有方法,在方法署名處都加上該例外的宣告
這代表,如果你在較底層的地方作修改,會迫使必須修改其一連串高層次函式的署名,而被影響而修改的模組必須重新建立及部署。
例外的設計原意是允許你在較遠處才處理錯誤,但可恥的是,檢查型例外卻會因此而打破封閉性原則。

提供發生例外的相關資訊

產生有益的錯誤訊息,隨著例外訊息一起傳遞。
包含:
  • 哪個操作發生錯誤
  • 錯誤型態

從呼叫者的角度定義意外類別

我們最關心的是,它們是如何被捕捉的。包裹(wrap)呼叫的 API,並確保它只會回傳共同的例外型態,可以大幅簡化程式。事實上,包裹第三方 API 是非常好的實作技巧。

以下為一個包裹前的例子:

ACMEPort  port = new ACMEPort(12);
try  {
port.open();
} catch (DeviceResponseException e) {
reportPortError(e);
logger.log("Device response exception",e);
} catch (ATM1212UnlockedException e) {
reportPortError(e);
logger.log("Unlock exception",e);
} catch (GMXError e) {
reportPortError(e);
logger.log("Device response exception");
} finally {
...
}

包裹後:

LocalPort port = new LocalPort(12);
try  {
port.open();
} catch (PortDeviceFailure e) {
reportError(e);
logger.log(e.getMessage(), e);
} finally {
....
}

LocalPort 是一個簡單的包裹類別,幫我們捕捉翻譯從 ACMEPort 類別所拋出的例外事件:


public class LocalPort {

private ACMEPort innerPort;
public LocalPort (int portNumber) {
innerPort = new ACMEPort (portNumber);
}
public void open() {
try {
innerPort.open();
} catch (DeviceResponseException e) {
throw new PortDeviceFailure (e);
} catch (ATM1212UnlockedException  e) {
throw new PortDeviceFailure (e);
} catch (GMXError e) {
throw new PortDeviceFailure (e);
}
}
....
}

優點:
  • 減少對它的依賴
  • 測試自己的程式時,有助於模擬第三方呼叫
  • 不會被某個廠商的 API 設計給侷限,可以定義自己覺得舒適的 API

 

定義正常的程式流程

有時候,你並不希望中止運算,可以使用SPECIAL CASE PATTERN[Fowler] (特殊情況模式),建立一個類別或物件,讓它能夠替你處理特殊情況。這樣做,客戶端的程式碼就不必處理例外行為,因為例外行為被包裹在特殊情況物件裡。
修改前:
try {
MealExpenses expenses = expenseReportDAO.getMeals ( employee.getID() );
m_total += expenses.getTotal();
} catch (MealExpensesNotFound e) {
m_total += getMealPerDiem();
}
修改 ExpenseReportDAO,讓它總是回傳一個 MealExpense 物件 :
MealExpenses expenses = expenseReportDAO.getMeals ( employee.getID() );
m_total += expenses.getTotal();

public class PerDiemMealExpenses implements MealExpenses {
public int getTotal () {
//return the per diem default
}
}

不要回傳 null (空值)

回傳 null 時,是在給自己增加額外的工作量,也是在給呼叫者找麻煩,只要有一處忘記檢查 null ,就會導致應用程序進入混亂無法控制的狀態。
在程式頂端捕捉 NullPointerException,或沒有捕捉,兩種作法都很糟糕,因為你不知道要如何處理這個從應用程式深層所拋出的 NullPointerException 例外。
如果你要讓方法回傳 null ,那不如試著拋出一個例外事件或回傳一個 SPECIAL CASE 物件(特殊情況物件)來取代回傳 null。
如果呼叫一個會回傳 null 的第三方 API 方法,試著將這個方法用另一個新方法包裹起來,由新方法幫你拋出例外事件,或回傳一個 SPECIAL CASE 物件。
優點:
  • 可將 NullPointerException 機率降到最低
  • 程式更簡潔

不要傳遞 null

大部分的程式語言並沒有一個很好的方法,來處理意外將 null 傳遞給函式的情況。理性的作法是預設禁止傳遞 null 給函式。

總結

將錯誤處理看作是另一件重要的事,將之處理成獨立於主要邏輯的可讀程式,使得能獨立處理它,維護性也邁進一大步。


參考來源: Clean Code – A Handbook of Agile Software Craftsmanship  by Robert C. Martin

  • Share:

You Might Also Like

0 意見