閒談軟體設計:例外處理

閱讀時間約 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,就可以有較好讀的程式。

結語

基本上就是這樣,例外處理是可以在軟體架構設計時就考慮進去,或者說,在軟體架構設計時就該考慮進去,制定方針讓團隊有一個原則可以遵循,透過設計讓例外的處理較容易與一致,最終讓軟體的品質可以更好。最後,想了解更多關於怎麼處理例外,推薦可以看《笑談軟體工程:例外處理設計的逆襲》(好像絕版了,可能得去二手書店找找),文字幽默又有很多更深入的內容。
為什麼會看到廣告
53會員
104內容數
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
留言0
查看全部
發表第一個留言支持創作者!
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
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
美國總統大選只剩下三天, 我們觀察一整週民調與金融市場的變化(包含賭局), 到本週五下午3:00前為止, 誰是美國總統幾乎大概可以猜到60-70%的機率, 本篇文章就是以大選結局為主軸來討論近期甚至到未來四年美股可能的改變
Thumbnail
Faker昨天真的太扯了,中國主播王多多點評的話更是精妙,分享給各位 王多多的點評 「Faker是我們的處境,他是LPL永遠繞不開的一個人和話題,所以我們特別渴望在決賽跟他相遇,去直面我們的處境。 我們曾經稱他為最高的山,最長的河,以為山海就是盡頭,可是Faker用他28歲的年齡...
Thumbnail
......究其根本動畫版與漫畫版的癥結點是相同的,也就是配角人數過多使得情節處理難以做到盡善盡美,其中又以漫畫版的問題更為明顯......
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
關於片名   台灣片名《花漾女子》,原文片名《Promising Young Woman》,台灣譯名將時間定格在悲劇發生前,而原文片名則進一步帶我們看見另一個可能性結果
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作
Thumbnail
那天我問隊友,怎樣才算是一部小說呢?按字數計算嗎? 他說:「故事內起承轉合都有,就算」 所以我大膽地按著他的標準,將自己寫過的故事,粗略整理出一個明細。
纏中說禪,本名李彪,專欄筆名木子,其人是中國股市比較早期的操盤手,所以他比較熟悉a股的市場情況。他以“纏中說禪”為筆名,從2002年開始寫博客,直到2008年癌症病重停更,期間寫下了不少文章。而博客文章中最為著名的就是他的“教你炒股票”系列文章,他在這個系列裡講到的炒股理論和方法被粉絲稱為“纏論
Thumbnail
婚姻是人生大事,對溥儀尤其如此,因為如果皇帝大婚,就代表溥儀可以脫離眾多便宜老媽的束縛而得以親政。 但詭異的是,這個可以讓他脫離便宜老媽掌控的婚姻,卻還是要由便宜老媽進行主導並且居中角力......
Thumbnail
上次我提到:溥儀就是個死小孩。其實這不能全怪溥儀,而要怪詭異的宮廷教育及生活制度......
Thumbnail
近期電影「末代皇帝」重新修復上映。 為了推坑這部經典之作,本人決定以溥儀本身的自傳《我的前半生》為主要基底,和大家談一些電影中礙於篇幅或是藝術改編,而不容易察覺或是沒有呈現的真實歷史。
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
美國總統大選只剩下三天, 我們觀察一整週民調與金融市場的變化(包含賭局), 到本週五下午3:00前為止, 誰是美國總統幾乎大概可以猜到60-70%的機率, 本篇文章就是以大選結局為主軸來討論近期甚至到未來四年美股可能的改變
Thumbnail
Faker昨天真的太扯了,中國主播王多多點評的話更是精妙,分享給各位 王多多的點評 「Faker是我們的處境,他是LPL永遠繞不開的一個人和話題,所以我們特別渴望在決賽跟他相遇,去直面我們的處境。 我們曾經稱他為最高的山,最長的河,以為山海就是盡頭,可是Faker用他28歲的年齡...
Thumbnail
......究其根本動畫版與漫畫版的癥結點是相同的,也就是配角人數過多使得情節處理難以做到盡善盡美,其中又以漫畫版的問題更為明顯......
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
關於片名   台灣片名《花漾女子》,原文片名《Promising Young Woman》,台灣譯名將時間定格在悲劇發生前,而原文片名則進一步帶我們看見另一個可能性結果
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作
Thumbnail
那天我問隊友,怎樣才算是一部小說呢?按字數計算嗎? 他說:「故事內起承轉合都有,就算」 所以我大膽地按著他的標準,將自己寫過的故事,粗略整理出一個明細。
纏中說禪,本名李彪,專欄筆名木子,其人是中國股市比較早期的操盤手,所以他比較熟悉a股的市場情況。他以“纏中說禪”為筆名,從2002年開始寫博客,直到2008年癌症病重停更,期間寫下了不少文章。而博客文章中最為著名的就是他的“教你炒股票”系列文章,他在這個系列裡講到的炒股理論和方法被粉絲稱為“纏論
Thumbnail
婚姻是人生大事,對溥儀尤其如此,因為如果皇帝大婚,就代表溥儀可以脫離眾多便宜老媽的束縛而得以親政。 但詭異的是,這個可以讓他脫離便宜老媽掌控的婚姻,卻還是要由便宜老媽進行主導並且居中角力......
Thumbnail
上次我提到:溥儀就是個死小孩。其實這不能全怪溥儀,而要怪詭異的宮廷教育及生活制度......
Thumbnail
近期電影「末代皇帝」重新修復上映。 為了推坑這部經典之作,本人決定以溥儀本身的自傳《我的前半生》為主要基底,和大家談一些電影中礙於篇幅或是藝術改編,而不容易察覺或是沒有呈現的真實歷史。