閒談軟體設計:例外處理

更新於 發佈於 閱讀時間約 11 分鐘
本文源起同事曾在 FB Messenger 問起:為什麼 BeanGoException (沒想到過了幾年,前同事又變回同事,但以前開發的產品 BeanGo! 已經更名了,還好新聞稿還在,啊~過去的青春) 是繼承 RuntimeException 而不是 Exception?這是一種設計選擇,但這樣的選擇是好是壞就可以討論了。

不同語言中的例外

在設計軟體架構時,常常被忽略的就是例外或錯誤的處理,原因不外乎在設計時並不會知道會拋出什麼例外或以什麼形式回報錯誤,特別是還未決定或熟悉第三方套件前,因此,當真正遇到第三方套件例外拋出時,常會受限於既有的介面規範 (method signature) 只好將例外攔截下來然後無視它,於是這個錯誤就沒有人注意到了,等到產品上線後被忽略的例外會以別的形式讓產品當掉。這情況在 Java 生態特別明顯:
至於採用 Java 語言作為例子的原因很簡單:「因為它是當代流行的商業語言裡面,例外處理機制最困難 (或是說最討厭) 的語言。」搞懂了 Java 的例外處理,再應用到其他物件導向語言,就好像喝開水一樣,變得非常容易。 — 《笑談軟體工程:例外處理設計的逆襲》序言
會說 Java 特別難搞,原因是當初設計者希望 Java 是安全的語言,因此將例外設計成 checked 和 unchecked 二種,一旦拋出 checked 類型的例外,編譯器就會強制要求宣告成為 method signature 的一部份,若呼叫會拋出 checked 例外的函式,就一定要使用 try-catch-finally 處理或轉拋出去,簡言之,Java 設計者強制要求例外一定要有「人」處理,但是由誰處理就讓開發者自己決定,下場往往就是像下面這種啞巴處理:
這種有強迫症的語言大概只有 Java 和最近誕生的 Swift 了,C++ 和 C# 有拋出例外的機制,但不強迫開發者宣告或捕捉,是否處理外全憑開發者的良心 (是的,寫作良心唸作時間,有多少良心做多少事)。所以大部份語言有提供一些機制,簡化因例外拋出要處理的資源釋放機制,例如 C++ 的 RAII (Resource acquisition is initialization),簡單說,由物件的建構子取得資源,解構子釋放資源,物件的生命週期隨著例外消逝的話,C++ 就能確保資源能確實地被釋放。C# 有 using 的語法,Java 則在 Java 7 之後有 try-resources 的語法,避免開發者如上例那樣忘記在 finally 區塊釋放資源。
Objective C 雖然有@try@catch@finally 關鍵字與拋出 exception 的機制,但個人好像沒有用到哪個 API 是以例外的方式處理錯誤,這麼說不太精確,runtime 還是會拋出例外,但都是像 NullPointerException 這種類型的例外,本來就不是用捕捉的方式處理,實際上官方也是建議例外只用在 runtime 錯誤上:
You should reserve the use of exceptions for programming or unexpected runtime errors such as out-of-bounds collection access, attempts to mutate immutable objects, sending an invalid message, and losing the connection to the window server. You usually take care of these sorts of errors with exceptions when an application is being created rather than at runtime. — 《Introduction to Exception Programming Topics for Cocoa》
其他則以 NSError 物件的方式處理:
為了與 Objective C 的 runtime 相容,Swift 錯誤處理機制大致與 Objective C 相同,不過,Swift 和 Java 一樣希望有更安全的語言,所以和 Java 一樣可以在 function 上加上 throws 的宣告,但不用宣告拋出的例外類型,因此也不用擔心介面演變 (interface evolution):
而呼叫宣告會拋出例外的 function,編譯器會強制開發者要進行處理,不論是用 do-try-catch,還是用 try? 或 try! 處理,以及用 defer 清除資源 (這裡就不說明這之間的差異,對細節有興趣可以參考官方文件),總之,不處理編譯就會錯誤,個人覺得動機很好,但這機制其實常常讓開發人員在寫程式的當下,因為沒有足夠的資訊可以處理,所以只好選擇忽略,就好像生命會自己找到出路一樣,例外也是 (逃離處理機制)。

回歸問題

傳遞錯誤的機制而非拋出例外中斷目前執行緒的機制,個人認為有一部份的原因是非同步 (asynchronous) 機制,要處理例外需要有足夠的 context (請參考《笑談軟體工程:例外處理設計的逆襲》第 9 章),會發生錯誤的程式執行在另一個執行緒,發生時並沒有任何有用的資訊可以處理,但又不能中斷目前的執行緒 (通常這個執行緒還需要執行 task queue 的其餘 task),只好將錯誤以 callback 的方式 (例如先前 Objective C 的例子) 傳遞給當初的請求者,讓該執行緒可以執行,不過 callback 也常會造成 callback 地獄就是了 (已經有語言開始著手讓這邊能更優雅地被處理)。
繞了一大圈,回到前同事的問題,其實一開始 BeanGoException 確實是繼承 Exception,是 checked exception,這個設計決定比要符合 Java 原本的風格,不過,checked exception 所造成的介面演變馬上就出現了,雖然個人也是比較偏好顯式的介面演變,但並不是所有的介面都是可以讓我們修改的,例如 Runnable 介面,正好也是在當初設計非同步機制時,希望有跨 Android 與 PC 平台的 AsyncTask,當時遇到 checked exception 讓處理變複雜,只好繞過:
個人不覺得這種繞過是一個很好的設計,因為這更容易讓開發者忽略 cleanup actions,但權衡之後仍是用了這樣的設計,並且在考慮各種情況下的處理機制後,參考 Spring framework (大多數的 exception 都是 unchecked),在一些配套設計下 (是的,變更設計一般都需要有配套),將 BeanGoException 改繼承 RuntimeException。不過,不代表每種專案我都會是這樣的選擇,一般來說還是會根據專案的類型和需求搭配軟體架構一起考慮和做選擇,但大多有一些原則:
  • 決定例外處理的最基本的策略
  • 一律轉成 domain model exception
  • 決定 exception 的非同步處理機制

決定例外處理的最基本的策略

《笑談軟體工程:例外處理設計的逆襲》一書提到了三種強健度等級:(等級 1) 錯誤回報、(等級 2) 狀態恢復和 (等級 3) 行為恢復,每往上一個等級,要付出的成本就高出許多,所以,一般不會全部的程式都要求做到行為恢復等級,但如果在思考軟體架構時,完全不要求,大多數的情況會變成什麼都沒有處理。因此,一般來說,我會要求最起碼要到錯誤回報,不論是透過使用者介面回報或是 log 記錄下來,若是設計 Web API,exception 的回報還包含思考怎麼搭配 HTTP Status Code,什麼情況下該用 4xx 的錯誤碼,什麼情況下該用 5xx 的錯誤碼,那些例外 server 應該處理,處理到哪個層級,那些例外該由 client 處理,處理到甚麼層級等等。
一般會再根據模組的類型,決定模組的處理策略,例如處理資料庫存取的 Repository 就會規範至少要到狀態恢復等級,也就是說除了釋放資源外,還會將資料庫還原到先前的狀態,雖然說這好像是處理資料庫時的慣用方法,但若是在設計模組時就處理好,狀態恢復的程式就不會四散在使用模組的上層程式中。一般來說,個人是用設計來處理例外,而不是在個別的函式呼叫處理,像是 processor-chain 搭配重試機制等等,因此,會提前思考處理的策略。

一律轉成 domain model exception

為了避免因 exception 引起的介面演變,通常會將專案的 root exception 設計成 XException,X 是專案名稱 (若有公司內部的函式庫,那 X 可能會遵循內部函式庫管理的規則),各個模組內可能拋出的例外都會被攔截下來並處理,若無法處理要往上層拋時,一定會轉成繼承 XException 的例外,例如 將 SQLException 轉成 RepositoryException,若之後模組內部的實作更換了,也可以用同樣的方式轉成相容的例外,像是將 MongoException (也是 unchecked exception) 轉成 RepositoryException,又例如 Android 的 SQLiteDatabase 並不會拋出例外,而是回傳錯誤碼,所以也可以轉成 RepositoryException,如此,模組的介面就不會因此被破壞或被迫改變。
轉成 domain model exception 另一層意義是提供更明確的語意,讓呼叫者清楚知道該怎麼處理,至於 XException是 checked 或 unchecked 還要考量其他因素來決定。但轉成 unchecked exception 倒是有一個好處,可以告訴團隊成員:若不知道怎麼處理例外,最起碼用 XException 包起來再轉拋出去,如此一來,可以避免啞巴把當下的例外吃掉,若例外真的發生還可以靠其他設計捕捉轉拋出來的例外。

決定 exception 的非同步處理機制

若是 Objective C,基本上處理機制是固定的,不過還是盡量會根據專案有統一的處理機制,若原生的 API 不相容,會進行轉換,讓處理方式盡可能一樣。若是 Java 語言就比較討厭一點,偏偏 Android 又不允許在 UI thread 呼叫網路,所以蠻多情況下都是用 Android 原生的 AsyncTask 中呼叫網路,然後直接在裡面處理因網路問題而拋出來的例外,這沒什麼問題,只是程式挺醜的又不好讀,因此有蠻多套件將這樣的機制都轉成 callback 的形式,若搭配 Java 8 的 method reference Lambda,就可以有較好讀的程式。

結語

基本上就是這樣,例外處理是可以在軟體架構設計時就考慮進去,或者說,在軟體架構設計時就該考慮進去,制定方針讓團隊有一個原則可以遵循,透過設計讓例外的處理較容易與一致,最終讓軟體的品質可以更好。最後,想了解更多關於怎麼處理例外,推薦可以看《笑談軟體工程:例外處理設計的逆襲》(好像絕版了,可能得去二手書店找找),文字幽默又有很多更深入的內容。
為什麼會看到廣告
avatar-img
53會員
104內容數
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
Spirit的沙龍 的其他內容
設計時的考量主要有:(1) App 是 Internet App,在考量 UI 體驗和網路頻寬的消耗,多數資料需以某種形式儲存部分資料在行動裝置上;(2) 因此會需要同步伺服器端和行動裝置端之間資料狀態;(3) 但行動裝置網路的穩定性不如一般網路可靠,要有足夠的自動化測試驗證正常的流程與異常的流程。
程式開發有趣的地方,同樣的目標,不同的團隊會因不同的因素做出不同的設計抉擇。而這往往也是為什麼一個資深的工程師在開發速度上不一定比較快的原因之一,一個越是資深的工程師,思考的因素會更多,不過,不是考慮得越多就結果就一定越好,有時還會變成 over design 較糟的結果。
唸研究所開始當助教,偶而會有學弟妹問:怎樣寫好程式?老實說,這是個大哉問,連我學開發軟體這麼久,我也只能回答他們:多培養自己釐清問題、拆解問題、解決問題與抽象化的能力。但他們通常只會一臉狐疑看著我,感覺我說的話好抽象。
在履歷中常常看到導入 MVVM,然後問為什麼要導入 MVVM 時,最常聽到的答案是這樣不會有很肥大的 view controller,但如果再問 view controller 是 MVC 的那一個部分,很多人卻回答不出個所以然,所以想聊聊這個很多種說法的 MVC pattern。
為什麼是煮拉麵呢?主題是來自前同事在問我為什麼有人的程式好像常常會歪掉,或是變得難維護,後續的討論中,他用的例子就是拉麵,所以... 今天就用程式來煮拉麵吧!
設計時的考量主要有:(1) App 是 Internet App,在考量 UI 體驗和網路頻寬的消耗,多數資料需以某種形式儲存部分資料在行動裝置上;(2) 因此會需要同步伺服器端和行動裝置端之間資料狀態;(3) 但行動裝置網路的穩定性不如一般網路可靠,要有足夠的自動化測試驗證正常的流程與異常的流程。
程式開發有趣的地方,同樣的目標,不同的團隊會因不同的因素做出不同的設計抉擇。而這往往也是為什麼一個資深的工程師在開發速度上不一定比較快的原因之一,一個越是資深的工程師,思考的因素會更多,不過,不是考慮得越多就結果就一定越好,有時還會變成 over design 較糟的結果。
唸研究所開始當助教,偶而會有學弟妹問:怎樣寫好程式?老實說,這是個大哉問,連我學開發軟體這麼久,我也只能回答他們:多培養自己釐清問題、拆解問題、解決問題與抽象化的能力。但他們通常只會一臉狐疑看著我,感覺我說的話好抽象。
在履歷中常常看到導入 MVVM,然後問為什麼要導入 MVVM 時,最常聽到的答案是這樣不會有很肥大的 view controller,但如果再問 view controller 是 MVC 的那一個部分,很多人卻回答不出個所以然,所以想聊聊這個很多種說法的 MVC pattern。
為什麼是煮拉麵呢?主題是來自前同事在問我為什麼有人的程式好像常常會歪掉,或是變得難維護,後續的討論中,他用的例子就是拉麵,所以... 今天就用程式來煮拉麵吧!
你可能也想看
Google News 追蹤
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
Thumbnail
本章介紹了 PHP 中的例外處理技術,包括其語法、常見異常類型以及如何主動觸發異常訊息。我們還學習了如何自定義異常類別,以便更好地管理和處理不同類型的異常情況。通過使用例外處理,可以提高程式碼的穩定性、可讀性和可維護性,並提供更優雅的錯誤信息處理機制。
Thumbnail
本章節旨在介紹Java程式語言中的「例外處理」概念。透過各個小節,讀者將學習到何謂例外處理、為何要使用它、如何在Java中實現例外處理,以及如何正確地捕獲和處理各種類型的異常。此外,本章節還提供了如何主動觸發異常,以及如何創建和使用自定義異常的實例。
Thumbnail
這篇文章主要講解Kotlin的例外處理。內容包括例外處理的目的、`try-catch` 和 `finally` 的用法、常見的異常類型,以及如何定義和觸發自定義的異常訊息。
Thumbnail
本章節為Swift程式語言的異常處理介紹,說明了為何需要進行異常處理以及如何進行異常處理。提供了使用do、try、catch和throw關鍵字進行異常處理的基本語法並展示了其在實際程式中的應用。同時也說明了Swift中的一些常見異常類型,並且教導了如何主動觸發異常訊息和定義自己的異常類型。
Thumbnail
本章節的目的是介紹在TypeScript中如何進行例外處理。涵蓋了例外處理的重要性、語法、常見異常類型以及如何主動觸發異常訊息及用戶自定義異常訊息。為讀者提供了全面而深入的了解,以提高程式的可靠性、提供更好的反饋、增加程式的容錯性以及改善程式的可讀性。
Thumbnail
EAFP(Easier to Ask for Forgiveness than Permission)是Python提倡的防禦性程式碼風格,鼓勵工程師直接撰寫主要業務邏輯,不需要多做檢查,真的出現異常再處理就好。這種風格的程式碼可讀性優於LBYL風格,並且在多進程/多線程場景下表現更好。
Thumbnail
當你在開發程式時,難免會遇到各種錯誤和異常情況。這些錯誤可能是因為代碼中的錯誤、外部資源無法訪問或其他不可預期的狀況。為了提高程式的可靠性、穩定性和可維護性,我們使用「例外處理」來處理這些異常情況。
Thumbnail
本章節介紹C#的「例外處理」,包括使用try-catch語法處理錯誤,finally關鍵字的使用,以及如何主動引發和自定義異常。
Thumbnail
例外處理是Python中的重要概念,用於控制並處理程序異常,防止程序崩潰和數據損失。它包括try, except, else和finally等語法結構,可用於對特定錯誤進行處理,或主動觸發和自定義異常。
Thumbnail
本文介紹Python程式設計中處理異常的try, except, else, finally語句,並提供程式範例來更深刻理解使用方法。
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
Thumbnail
本章介紹了 PHP 中的例外處理技術,包括其語法、常見異常類型以及如何主動觸發異常訊息。我們還學習了如何自定義異常類別,以便更好地管理和處理不同類型的異常情況。通過使用例外處理,可以提高程式碼的穩定性、可讀性和可維護性,並提供更優雅的錯誤信息處理機制。
Thumbnail
本章節旨在介紹Java程式語言中的「例外處理」概念。透過各個小節,讀者將學習到何謂例外處理、為何要使用它、如何在Java中實現例外處理,以及如何正確地捕獲和處理各種類型的異常。此外,本章節還提供了如何主動觸發異常,以及如何創建和使用自定義異常的實例。
Thumbnail
這篇文章主要講解Kotlin的例外處理。內容包括例外處理的目的、`try-catch` 和 `finally` 的用法、常見的異常類型,以及如何定義和觸發自定義的異常訊息。
Thumbnail
本章節為Swift程式語言的異常處理介紹,說明了為何需要進行異常處理以及如何進行異常處理。提供了使用do、try、catch和throw關鍵字進行異常處理的基本語法並展示了其在實際程式中的應用。同時也說明了Swift中的一些常見異常類型,並且教導了如何主動觸發異常訊息和定義自己的異常類型。
Thumbnail
本章節的目的是介紹在TypeScript中如何進行例外處理。涵蓋了例外處理的重要性、語法、常見異常類型以及如何主動觸發異常訊息及用戶自定義異常訊息。為讀者提供了全面而深入的了解,以提高程式的可靠性、提供更好的反饋、增加程式的容錯性以及改善程式的可讀性。
Thumbnail
EAFP(Easier to Ask for Forgiveness than Permission)是Python提倡的防禦性程式碼風格,鼓勵工程師直接撰寫主要業務邏輯,不需要多做檢查,真的出現異常再處理就好。這種風格的程式碼可讀性優於LBYL風格,並且在多進程/多線程場景下表現更好。
Thumbnail
當你在開發程式時,難免會遇到各種錯誤和異常情況。這些錯誤可能是因為代碼中的錯誤、外部資源無法訪問或其他不可預期的狀況。為了提高程式的可靠性、穩定性和可維護性,我們使用「例外處理」來處理這些異常情況。
Thumbnail
本章節介紹C#的「例外處理」,包括使用try-catch語法處理錯誤,finally關鍵字的使用,以及如何主動引發和自定義異常。
Thumbnail
例外處理是Python中的重要概念,用於控制並處理程序異常,防止程序崩潰和數據損失。它包括try, except, else和finally等語法結構,可用於對特定錯誤進行處理,或主動觸發和自定義異常。
Thumbnail
本文介紹Python程式設計中處理異常的try, except, else, finally語句,並提供程式範例來更深刻理解使用方法。