iThome

有寫過程式的鄉民們大概都會同意,例外處理是一件很困難的工作。但是,有沒有人想過,為什麼例外處理會這麼難?到底難在何處?唯有先了解問題,才有機會找出合適的解決方案。

其實說破了道理也很簡單:因為「例外處理」牽扯到好幾個相關但卻不相同的觀點或是面向,例外處理要做得好,這些觀點都必須要被關注到。Teddy 認為例外處理至少涵蓋了以下五個觀點:

●      Usage View(用途觀點)

●      Design View(設計觀點)

●      Handling View(處理觀點)

●      Tool-Support View(工具支援觀點)

●      Process View(流程觀點)

例外處理的「設計」觀點

在2013 年4 月29 日,Teddy 看到一則新聞,大意是說對岸駭客可能會將入侵目標轉向台灣,對台灣國防、政治、經濟安全造成危害。記者電話訪問了某警察單位。

記者:對於對岸駭客可能會入侵台灣一事,警察單位有什麼因應之道?

警察:主要還是要看有沒有人報案,有人報案我們就會處理。

Teddy 內心獨白:靠……過來一點。「有人報案我們就會處理」是哪招,會不會太消極了一點?

****************************************************************************************************

專案經理:對於我們產品品質不佳、bug 很多一事,團隊有何因應之道?

開發人員:主要還是要看有沒有人回報bug,有人回報我們就會處理。

Teddy 內心獨白:好熟悉的對話內容,好像在哪裡聽過。

以上所述和例外處理有何關係?對於例外處理的看法,其實開發人員和警察杯杯的想法很像:「只要有人報案,我們就會處理」。在程式中,不同的時間點會有不同的報案方法。如果是消極的等到軟體上市之後,由使用者發現問題才來「報案」,使用者會留下軟體品質不良的印象,此時再來處理已經稍嫌太晚,而且成本也高出很多。

要提升使用者的滿意度,減低「辦案成本」,最好能夠把發現問題的時間點往前移到設計階段(design phase),讓程式能夠以例外的方式來「報案(回報問題)」,讓開發人員能夠在程式中處理這些異常狀況,以提高產品的品質。從設計的角度來看,例外的回報有以下兩種形式:

●      公告例外(declared exception): 將例外宣告在元件的介面規範中,又稱為anticipated 或expected exception(預期例外),用來代表component fault。

●      非公告例外(undeclared exception):例外沒有被宣告在元件的介面規範中,又稱為unanticipated 或unexpected exception(不預期例外),代表design fault。

看一個Java 程式的例子,IllegalArgumentException 在列表23-1 中屬於undeclared exception,因為它沒有被宣告在deposit函數的介面上。

列表23-1:非公告例外範例

反之,列表23-2程式碼的IllegalArgumentException 則屬於declared exception,因為它被宣告在deposit 函數的介面上。

列表23-2:公告例外範例

就好像沒有人報案,警察就不知道有犯罪案件發生是一樣的道理,唯有將例外宣告在介面上,或是以某種形式存在程式碼或文件當中(例如微軟公司所提供的MSDN 線上文件),開發人員才有機會在設計階段,便知道要如何預備與因應可能會遭遇到的異常狀況。

例外處理的「用途」觀點

例外不就是例外嗎?還能有什麼其他用途?如果鄉民們這樣想就弱掉了。雖然理論上,例外是用來表示failure,但事實上,例外還有可能被用來當做resultclassification(結果分類)與monitoring(監控)。當例外被當做resultclassification 與monitoring 時,代表的並不是一種異常狀況,而是用來表達一種狀態通知。因此,通常不需要加以特別處理。

以上用白話文來闡述就是:「寫程式遇到別人丟出例外時,請先判斷這個例外是用來代表failure,還是用來表示狀態通知(result classification與monitoring)」。請看列表22-1 的例子大家會比較清楚一點:「請問Java 語言裡面的InterruptedException 要如何處理?」

列表22-1:遇到InterruptedException 要如何處理?

多年以前,當Teddy 第一次用Java 撰寫多執行緒程式時,被這個問題困擾許久:「InterruptedException 明明是一個例外,可是為什麼捕捉之後卻不需要特別處理?」多年之後,Teddy 陰錯陽差投入例外處理研究之後才慢慢弄清楚,原來InterruptedException在這裡(列表22-1)並不是用來表達failure(某人辦事不力),而只是一種狀態通知,用來告訴等待或是執行中的執行緒,有其他人把你給中斷了,請識相一點不要硬撐著不下台,趕快拍拍屁股收工閃人了。這也是為什麼上面這個例子在捕捉到InterruptedException 之後什麼都沒做的原因,在這裡如果硬要設計什麼「例外處理程式碼」來「拯救」被中斷的執行緒,被中斷之後繼續死賴著不結束,反倒會造成程式錯誤。

以上內容如果鄉民們能夠理解,恭喜你踏入了例外處理設計的大門。然而,世間事如果都那麼簡單、黑白分明,事情就好辦了。要特別將「例外用途」提出來當成一個觀點來討論的原因,就是因為開發人員通常無法直接透過例外物件本身,來判斷這個例外是屬於哪種用途,還必須搭配其他context(例如local context 與application context)才能決定例外的用途。

例外處理的「處理」觀點

從設計觀點可以學到一個重點,要做例外處理,首先必須先知道一個元件可能會丟出哪些例外,接著以用途觀點來判斷這個例外是否真的代表failure 或只是一種狀態通知,兩者的例外處理方法各不相同。所謂的處理觀點,則是從實作例外處理程式角度來探討例外處理的問題。因為這關係到要如何實作例外處理的細節,處理觀點是「例外處理的4+1 觀點」裡面最複雜的觀點,需考慮下列兩個因素:

●      可回復性(recoverability):針對例外所造成的錯誤狀態,捕捉例外的人是否有能力可以將其回復(recoverable)或是沒有能力回復(unrecoverable,又稱為irrecoverable)。在預設情況下,recoverable exception 隱含著代表component fault 的語意, 而unrecoverable exception 則代表design fault。但實際上,很多時候僅僅依據例外類別是不足以判斷例外狀況是否可以回復,必須同時考慮到其他觀點。

●      例外處理實作:有哪些例外處理策略可以使用?例如,是否需要將例外往外傳遞、如何執行狀態恢復動作、如何確保函數在例外發生時能夠繼續提供服務?在所選擇的程式語言中,要如何實作這些策略?假設採用Java 語言,則要如何將程式的正常與例外處理邏輯,妥善地分配到try、catch、finally 這三個結構之中。另外,還要考慮是否有足夠的輔助工具或函式庫,例如,日誌檔(logging)機制、統一的錯誤回報框架、自動程式更新機制等,可以用來支援例外處理實作。

先來談談可恢復性。一個例外狀況可不可修、要不要修,必須同時考量被呼叫者(callee)與呼叫者(caller)的情況。首先看看被呼叫者在拋出例外之後,是否依然處於正確的狀態,再判斷呼叫者是否有足夠的context 來處理這個例外。理想狀態下,被呼叫者應該確保例外發生之後自己並沒有造成系統狀態錯誤,否則會增加呼叫者例外處理的負擔;呼叫者除了要擦自己的屁股(維持正確狀態),還要幫被呼叫者擦屁股。在真實世界中,幫別人擦屁股是一件非常不愉快的工作,在程式當中也是如此。尤其是當被呼叫者會改變系統的全域狀態或是外部狀態的時候,要幫它擦屁股不但是一件非常困難的工作,有時候甚至辦不到。

舉個例子,如圖24-1 所示,假設你用了某個Java 函數,它同時會更新五個資料庫表格,但是這個函數在更新資料的時候並沒有做交易處理(transaction)。一旦在更新過程中發生SQLException,最後資料庫處在資料不一致的狀態。身為呼叫者的你,要如何挽救這種狀況(考慮例外的可回復性)?

圖24-1:分析例外可回復性

從Java 語言的角度來看,SQLException 是一種可回復例外(recoverable exception)。最不傷腦筋的作法是先將資料庫複製一份,當例外發生的時候再用之前的備份將其復原。然而,這種作法不但耗時,而且在同時有很多人操作資料庫的情況下,也不切實際。也就是說,如果某函數丟出例外的時候系統的狀態已經不正確了,就算這個例外本身的語意是屬於可恢復例外,接受例外的人,很可能愛莫能助,沒有能力將系統狀態復原。這也是Teddy一再強調的觀念:「無法單獨依據例外類別來判斷例外處理方式。」

以上問題的癥結在於,該函數發生例外之後並沒有讓整個系統保持在正確狀態,如果它可以負責任一點,確保自己執行失敗之後仍將資料庫維持在正常狀態,那麼,你所要考慮的例外處理工作就輕鬆許多,只需要恢復自己對系統造成的狀態修改即可。

例外處理的「工具支援」觀點

工具支援觀點是從開發環境提供例外處理支援的角度,來探討為什麼例外處理會這麼困難。舉凡編譯器、整合開發環境(Integrated Development Environment;IDE,例如Eclipse 或Microsoft Visual Studio)與外掛程式(plugin或add-on)、第三方廠商所開發的工具程式等都屬於開發環境的範疇。

在這個觀點之中,例外分成兩大類:reminded(提醒)與unreminded(不提醒)。屬於前者的例外,開發環境會幫忙檢查每一個函數呼叫是否會拋出reminded 類型的例外。檢查的結果將以某種方式通知開發人員,例如將結果顯示在IDE 畫面上或產出報表,以協助開發人員設計例外處理。

從工具支援觀點來看,Java 的checked exception 其實就是一種程式語言內建的reminded exception,負責執行檢查與提醒的工具不是IDE,也不是外掛程式或第三方軟體,而是Java 編譯器。檢查的規則稱為「處理或宣告原則」,沒有滿足這條規則的程式會直接被Java 編譯器判定為語法錯誤。

看到這裡,鄉民們可能出現疑問:「Java 的unchecked exception 不會被編譯器檢查,所以屬於unreminded exeption ?還有像是C# 這類只支援runtime exception(unchecked exception)的語言,全部的例外也都屬於unreminded exception ?」

答案是不一定,要看情況。程式語言的編譯器只是一種工具,鄉民們還是可以透過像是IDE 外掛程式或第三方廠商所提供的工具,自訂檢查規則來區分reminded 與unreminded exception。舉個例子,SWT1 是用Java 語言開發的圖形介面工具,但是它並不喜歡Java 的checked exception。因此,SWT自己設計了繼承自RuntimeException 與Error 的兩種類外類別:SWTException 與SWTError,分別用來代表可回復(recoverable)與不可回復(unrecoverable)的異常狀況(也就是component fault 與design fault)。RuntimeException 與Error 都屬於unchecked exception,所以從「Java 編譯器」的角度來看,它們都是unreminded exception。

但是,鄉民們可以在Eclipse 裡面自行開發外掛程式,來檢查採用SWT所開發的應用程式在哪些地方會丟出SWTException,並藉此提醒使用者去處理這些例外狀況。如果有了這個外掛程式,則SWTException 就是一種reminded exception。

C# 語言雖然只支援unchecked exception,但是在Visual Studio 開發環境當中,當你將滑鼠移到某個函數,Visual Studio 會顯示該函數可能拋出的例外,如圖25-1 所示。

圖25-1:Visual Studio 顯示函數拋出的例外

C# 編譯器會去分析寫在程式中的<exception cref="member">description</exception>這個特殊格式的文件註解,以便顯示這些例外訊息,如列表25-1所示。

列表25-1:C# 函數丟出例外的文件式註解範例

從這個角度來看,所有寫在<exception> 標籤裡面的例外也可被視為reminded exception。但是這個標籤的目的,只是單純用來標註一個函數所有可能拋出的例外,並不像Java 的checked 和unchecked exception,除了Java 編譯器是否會檢查的差別以外,同時還有「語意上」的不同。也就是說,checked exception 用來代表可回復例外或component fault,而uncheckekd exception 用來代表不可回復例外或design fault。

如果鄉民們在C# 語言想要透過例外類別來區分可回復與不可回復的例外, 比較接近的作法是,將Exception 與其子類別( 不包含SystemException)視為可回復例外,將SystemException 與其子類別視為不可回復例外。如果想用C# 語言模仿Java 的「處理或宣告原則」,特別提醒開發人員程式中有哪些未被處理或宣告的Exception 與其子類別(不包含SystemException),鄉民們也可以自行開發Visual Studio 外掛程式來達到這個目的。

為了提高軟體的強健度,開發人員需要一個提醒機制,告知哪些操作有可能產生例外,否則,開發人員很容易便會忽略例外處理,只能等程式執行期間發生錯誤再回頭修補(而且還需要遇到有良心的開發人員,才會回頭修補)。至於這個提醒機制,像是Java 語言的作法,強制由編譯器將例外區分為checked 與unchecked,只是一種實作的方式。很多人不喜歡Java 這種「強迫中獎」的提醒方式,但提醒機制的存在,對於提升開發人員對於系統強健度的關注與警覺心,還是很有幫助的。如果開發環境對於「那些函數有可能產生例外」的提醒支援不足,將造成開發人員輕忽例外處理的必要性,以及增加例外處理設計的負擔。(摘錄整理自第四章)

 

作者簡介

陳建村(Teddy Chen)

泰迪軟體(Teddysoft)創辦人,畢業於台北科技大學機電科技研究所(資訊組)博士班,有19年以上軟體開發經驗,從事敏捷開發顧問、教育訓練、軟體工具導入等服務。著有《笑談軟體工程:敏捷開發法的逆襲》。

Teddy的部落格:teddy-chen-tw.blogspot.tw。圖片提供/悅知文化

 

笑談軟體工程:例外處理設計的逆襲

陳建村(Teddy Chen)/著

悅知文化出版

售價:580元

熱門新聞

Advertisement