錯誤處理

錯誤處理

古早時代,當時還沒有例外事件的概念,當時在處理錯誤以設定錯誤旗幟(flag)或回傳錯誤碼,讓呼叫者循線去做處理。

但過去的作法會造成程式碼的雜亂,畢竟 "原先要做的事情" 和 "處理錯誤" 是兩件事情。而處理錯誤的程式碼,通常比較長(各種不同錯誤),導致更加不易閱讀。

而利用例外處理把這兩件事情分開,可以讓我們的程式碼乾淨許多。

在開頭寫下Try-Catch-Finally

Try-Catch-Finally 會在程式裡定義一個視野(Scope),代表Try內的程式隨時會中斷,跑去執行Catch的區塊。

最好養成只要程式碼有可能拋出例外,就在開頭加上Try-Catch-Finally的習慣。

使用檢查型例外的代價

使用檢查型例外的代價是違反了開放閉合準則(Open/Closed Principle)

如果從方法中拋出一個檢查型例外,而catch卻寫在三層外的方法,那麼就必須將這之間的所有方法,在方法署名處都加上該例外的宣告。(假如對低層次的進行修改,可能會被迫將一連串中~高層次的函式署名進行修改,即使那些函式內部的程式碼都沒有被改動)

最終的結果,就是一連串的修改!函式的密封性被破壞了...

建議

如果正在寫一個關鍵的函式庫,那麼檢查型例外有時候滿好用的,因為本來就必須捕捉例外事件。

但對於一般的程式,檢查型例外的相依性會耗費太多成本,不太划算。

提供發生例外的相關資訊

每一個拋出的例外都應該提供足夠的資訊,讓使用者判斷、追查錯誤發生的原因、位置。

在Java裡,可以從任何一個例外事件去獲得堆疊追蹤的訊息,但無法知道原先的意圖。

產生一個有益的錯誤訊息,讓它隨著傳遞。訊息中應包含哪個操作發生錯誤、錯誤的型態是什麼,如果有紀錄執行過程的相關資訊,那就請傳遞足夠的資訊給catch區塊,使那些資訊可以被記錄下來。

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

在大多數的例外處理中,不論真正造成例外的原因是什麼,讓我們做的事都是一些標準化流程的處理事項。我們必須將錯誤記錄下來,並確保能繼續執行程式。

在這種情況下,我們只要包裹(wrap)呼叫的API,並確保它只會回傳共用的例外型態,就可以大幅簡化程式。

(利用高層次的例外型態(EX:Exception)去抓取例外,並將例外物件e,丟到一個專門處理例外訊息的方法,達到簡化程式的目的)

定義這種包裹(Wrapper)是非常有用的。 事實上,包裹第三方API是非常有用的實作技巧

當包裹一個第三方的API,就可以減少依賴。將來在更換另一個不同的函式庫時,可以節省許多力氣。而在測試時,也會比較方便。

包裹的另外一個優點,是可以設計自己習慣的API,而不會綁死在原先函式庫的API設計。

?通常,對於程式碼的某個區域而言,單一的例外事件類別是比較有幫助的。伴隨例外事件而發出的資訊,可用來分辨錯誤種類。如果你想捕捉某一個例外事件,並允許其它例外事件通過時,那就使用不同的例外類別。? (看不懂)

定義正常的程式流程

如果遵循上述的建議,最終應能成功分離程式裡的商業法則及錯誤處理,大量的程式看起來像簡潔簡單的演算法。

在大部分的狀況是,這會是很好的安排,不過有時候,你可能會不想要終止運算。

下面這段程式碼取自某個記帳程式的加總消費功能。

try{
    MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
    m_total += expenses.getTotal();
}catch(MealExpensesNotFound e){
    m_total += getMealPerDiem();
}

在這個商業法則裡,如果肉類被消費,那肉類消費會加總到總消費裡。如果該日未消費的話,員工可以得到一日總量的伙食補貼。 這個例外搞亂了程式的邏輯,如果我們不需要處理特殊情況的話,不是更好嗎?

我們可以修改 expenseReportDAO,讓它總是回傳一個MealExpenses 物件。如果未消費肉類,就直接回傳一個伙食補貼的MealExpense 物件。

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

這樣的寫法稱作 SPECIAL CASE PATTERN[Fowler] (特殊情況模式)。 你建立一個類別或設定一個物件,去特殊處理某些情況,利用這種方法讓程式碼不必處理例外事件,因為例外的行為被包裹在特殊情況物件裡。

不要回傳 null

可以回傳null的方法,就代表使用方法之後,要檢查是否為null,不然很容易造成 NullPointerException。 與其這樣,不如試著讓其拋出一個例外事件,或回傳一個SPECIAL CASE物件(特殊情況物件)來取代回傳null。

如果正要呼叫一個會回傳null的第三方API方法,試著將這個方法用另一個新方法包裹起來,新方法會幫忙拋出例外事件或回傳SPECIAL CASE物件。

在大部分情況下,特殊情況物件能簡單地進行補救措施。狀況如下:

List<Employee> employees = getEmployees();
if(employees != null) {
    for(Employee e : employees) {
        totalPay += e.getPay();
    }
}

現在,getEmployees 可以回傳null,所以必須檢查它。 倘若我們修改getEmployees 函式,使得它回傳一個空白串列(empty list),我們就可以將程式碼整理成:

List<Employee> employees = getEmployees();
for(Employee e : employees) {
    totalPay += e.getPay();
}

以Java來說,當沒有東西的時候,就回傳Collections.emptyList()。 倘若用這種方式來寫程式,就可以將發生NullPointerException的機率降至最低。

不要傳遞 null

除非使用的API有預期會接收到null,否則應該避免傳遞null。

雖然自己在寫API時,可以去檢查參數是否為null,但其實還是需要耗費力氣去寫去判斷,最好的做法還是避免去傳遞null。

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

因此最好預設禁止傳遞null給函式,大家共同遵守這個規範,因為大家知道,null出現在參數,代表潛在的風險。 而以這樣的經驗來撰寫程式,也可以大幅降低出錯的可能。

總結

Clean Code是易讀的,但它也必須是耐用的,這兩者並無衝突。

當我們將錯誤處理看成一件重要的事情,並將之處理成獨立於主邏輯的可讀程式,代表我們寫出了整潔耐用的程式碼。

當作到這個地步,我們就能獨立的處理它,在程式的可維護性又前進了一大步。

Last updated