異常處理是軟件系統中最糟糕的復雜性來源之一。處理特殊情況的代碼天生就比處理正常情況的代碼更難編寫,而且開發人員經常在定義異常時沒有考慮如何處理它們。本章討論了異常對復雜性的不成比例的貢獻,然后展示了如何簡化異常處理。本章的主要教訓是減少必須處理異常的地方;在許多情況下,可以修改操作的語義,使正常行為可以處理所有情況,並且不需要報告任何異常情況(這就是本章的標題)。
10.1 異常增加復雜性的原因
我使用術語異常來指改變程序中正常控制流的任何不尋常的情況。許多編程語言都包含一個正式的異常機制,該機制允許底層代碼拋出異常並通過封裝代碼捕獲異常。但是,即使不使用正式的異常報告機制,也可能發生異常,例如當一個方法返回一個特殊值,表明它沒有完成正常行為。所有這些形式的異常都增加了復雜性。
一段特定的代碼可能會遇到幾種不同的異常:
- 調用者可能提供錯誤的參數或配置信息。
- 被調用的方法可能無法完成請求的操作。例如,I/O操作可能失敗,或者所需的資源可能不可用。
- 在分布式系統中,網絡數據包可能丟失或延遲,服務器可能無法及時響應,或者對等節點可能以無法預料的方式通信。
- 代碼可能會檢測出bug、內部不一致或無法處理的情況。
大型系統必須處理許多異常情況,特別是當它們是分布式的或者需要容錯的時候。異常處理占系統中所有代碼的很大一部分。
異常處理代碼天生就比正常情況下的代碼更難寫。異常中斷了正常的代碼流;它通常意味着某事沒有像預期的那樣工作。當異常發生時,程序員可以用兩種方法處理它,每種方法都很復雜。第一種方法是向前推進並完成正在進行的工作,盡管存在例外。例如,如果一個網絡數據包丟失,它可以被重發;如果數據損壞了,也許可以從冗余副本中恢復。第二種方法是中止正在進行的操作,向上報告異常。但是,中止可能很復雜,因為異常可能發生在系統狀態不一致的地方(數據結構可能已經部分初始化);異常處理代碼必須恢復一致性,例如通過撤銷發生異常之前所做的任何更改。
此外,異常處理代碼為更多的異常創造了機會。考慮重新發送丟失的網絡包的情況。也許包裹實際上並沒有丟失,只是被耽擱了。在這種情況下,重新發送數據包將導致重復的數據包到達對等點;這引入了一個新的異常條件,對等方必須處理。或者,考慮從冗余副本中恢復丟失的數據的情況:如果冗余副本也丟失了怎么辦?在恢復期間發生的次要異常通常比主要異常更微妙和復雜。如果通過中止正在進行的操作來處理異常,則必須將此異常作為另一個異常報告給調用者。為了防止異常的無休止級聯,開發人員最終必須找到一種方法來處理異常,而不引入更多的異常。
對異常的語言支持往往冗長而笨拙,這使得異常處理代碼難以閱讀。例如,考慮以下代碼,它使用Java對對象序列化和反序列化的支持從文件中讀取tweet集合:
try (
FileInputStream fileStream =new FileInputStream(fileName);
BufferedInputStream bufferedStream =new BufferedInputStream(fileStream);
ObjectInputStream objectStream =new ObjectInputStream(bufferedStream);
)
{
for (int i = 0; i < tweetsPerFile; i++) {
tweets.add((Tweet) objectStream.readObject());
}
}
catch (FileNotFoundException e) {
...
}
catch (ClassNotFoundException e) {
...
}
catch (EOFException e) {
// Not a problem: not all tweet files have full
// set of tweets.
}
catch (IOException e) {
...
}
catch (ClassCastException e) {
...
}
但是,基本的try-catch樣板代碼比正常情況下的操作代碼行數更多,甚至不考慮實際處理異常的代碼。很難將異常處理代碼與正常情況代碼聯系起來:例如,在哪里生成每個異常並不明顯。另一種方法是把代碼分成許多不同的try塊;在極端情況下,可以嘗試生成異常的每一行代碼。這將使異常發生的地方變得清晰,但是try塊本身會破壞代碼流,使其更難讀取;此外,一些異常處理代碼可能會在多個try塊中重復。
很難確保異常處理代碼真正有效。有些異常,比如I/O錯誤,在測試環境中很難生成,因此很難測試處理它們的代碼。異常在運行的系統中不經常發生,所以很少執行異常處理代碼。bug可能很長一段時間都無法檢測到,當最終需要異常處理代碼時,它很可能無法工作(我最喜歡的說法之一是:“未執行的代碼無法工作”)。最近的一項研究發現,在分布式數據密集型系統中,超過90%的災難性故障是由錯誤處理引起的。當異常處理代碼失敗時,很難調試問題,因為它發生的頻率很低。
10.2 例外情況太多
程序員通過定義不必要的異常而加劇了與異常處理相關的問題。大多數程序員都被告知檢測和報告錯誤很重要;他們通常將其解釋為“檢測到的錯誤越多越好”。這導致了一種過度防御的風格,任何看起來有點可疑的東西都會被異常拒絕,這導致了不必要的異常的擴散,增加了系統的復雜性。
在設計Tcl腳本語言時,我自己也犯了這個錯誤。Tcl包含一個未設置的命令,可用於刪除變量。我定義了unset以便在變量不存在時拋出錯誤。當時我認為,如果有人試圖刪除一個不存在的變量,那么它一定是一個bug,所以Tcl應該報告它。然而,unset最常見的用途之一是清理以前操作創建的臨時狀態。通常很難准確地預測創建了什么狀態,特別是在操作中途中止的情況下。因此,最簡單的方法是刪除可能已經創建的所有變量。unset的定義使得這種情況很尷尬:開發人員最終會在catch語句中封裝對unset的調用,以捕獲並忽略unset拋出的錯誤。回顧過去,unset命令的定義是我在Tcl設計中犯下的最大錯誤之一。
使用異常來避免處理困難的情況是很有誘惑力的:與其找出一個干凈的方法來處理它,不如拋出一個異常並把問題推給調用者。有些人可能會認為這種方法賦予了調用者權力,因為它允許每個調用者以不同的方式處理異常。然而,如果你在特定情況下不知道該怎么做,很有可能打電話的人也不知道該怎么做。在這種情況下生成異常只會將問題傳遞給其他人,並增加系統的復雜性。
類拋出的異常是其接口的一部分;具有大量異常的類具有復雜的接口,並且它們比具有較少異常的類要淺。異常是接口中特別復雜的元素。它可以在被捕獲之前向上傳播幾個堆棧級別,因此它不僅影響方法的調用者,還可能影響更高級別的調用者(及其接口)。
拋出異常很容易,處理它們很困難。因此,異常的復雜性來自於異常處理代碼。減少異常處理造成的復雜性損害的最佳方法是減少必須處理異常的地方的數量。 本章的其余部分將討論減少異常處理程序數量的四種技術。
10.3 定義不存在的錯誤
消除異常處理復雜性的最佳方法是定義api,這樣就沒有異常需要處理:定義不存在的錯誤。 這可能看起來有些褻瀆,但在實踐中卻非常有效。考慮前面討論的Tcl unset命令。當unset被要求刪除一個未知變量時,它應該簡單地返回,而不是拋出一個錯誤。我應該稍微修改一下unset的定義:unset應該確保一個變量不再存在,而不是刪除一個變量。對於第一個定義,如果變量不存在,unset就無法執行其任務,因此生成異常是有意義的。對於第二個定義,使用不存在的變量的名稱來調用unset是非常自然的。在這種情況下,它的工作已經完成,所以它可以簡單地返回。不再需要報告錯誤情況。
10.4 示例:在Windows中刪除文件
文件刪除提供了另一個如何定義錯誤的例子。如果文件在進程中打開,Windows操作系統不允許刪除該文件。對於開發人員和用戶來說,這是一個持續的沮喪之源。為了刪除正在使用的文件,用戶必須在系統中搜索,找到打開該文件的進程,然后殺死該進程。有時用戶會放棄並重新啟動他們的系統,只是為了刪除一個文件。
Unix操作系統更優雅地定義了文件刪除。在Unix中,如果文件在刪除時打開,Unix不會立即刪除該文件。
它將文件標記為刪除,然后刪除操作成功返回。該文件名已從其目錄中刪除,因此其他進程無法打開舊文件,並且可以創建具有相同名稱的新文件,但現有的文件數據將持續存在。已經打開文件的進程可以繼續正常地讀取和寫入文件。一旦文件被所有訪問進程關閉,它的數據就會被釋放。
Unix方法定義了兩種不同的錯誤。首先,刪除操作不再返回一個錯誤,如果文件當前正在使用;刪除成功,文件最終將被刪除。其次,刪除正在使用的文件不會為使用該文件的進程創建異常。解決這個問題的一種可能的方法是立即刪除文件,並標記所有打開的文件來禁用它們;其他進程讀取或寫入刪除文件的任何嘗試都將失敗。但是,這種方法會為那些要處理的進程創建新的錯誤。相反,Unix允許它們繼續正常地訪問文件;延遲文件刪除定義了不存在的錯誤。
Unix允許進程繼續讀寫一個命中注定的文件,這似乎有些奇怪,但我從未遇到過這種情況,它會導致嚴重的問題。對於開發人員和用戶來說,Unix下的文件刪除定義要比Windows下的定義簡單得多。
10.5 示例:Java子字符串方法
最后一個例子是Java String類及其子String方法。給定一個字符串中的兩個索引,substring返回從第一個索引給出的字符開始並以第二個索引之前的字符結束的子字符串。但是,如果其中一個索引超出了字符串的范圍,則子字符串將拋出IndexOutOfBoundsException。此異常是不必要的,並使此方法的使用復雜化。我經常遇到這樣的情況,其中一個或兩個索引可能在字符串的范圍之外,我希望提取字符串中與指定范圍重疊的所有字符。不幸的是,這需要我檢查每一個指標,把它們四舍五入到0或到字符串的末尾;一個單行的方法調用現在變成了5-10行代碼。
如果Java子字符串方法自動執行此調整,那么它將更容易使用,以便實現以下API:“返回索引大於或等於beginIndex而小於endIndex的字符串字符(如果有的話)。這是一個簡單而自然的API,它定義了IndexOutOfBoundsException異常。即使一個或兩個索引是負的,或者beginIndex大於endIndex,該方法的行為也已經定義好了。這種方法簡化了方法的API,同時增加了它的功能,因此使方法更加深入。許多其他語言都采用了無錯誤的方法;例如,Python為超出范圍的列表片返回一個空結果。
當我主張定義不存在的錯誤時,人們有時反駁說拋出錯誤會捕獲bug;如果錯誤被定義為不存在,那么這是否會導致bug生成?也許這就是為什么Java開發人員決定子字符串應該拋出異常的原因。這種錯誤的方法可能會捕獲一些bug,但也會增加復雜性,從而導致其他bug。在錯誤的方法中,開發人員必須編寫額外的代碼來避免或忽略錯誤,這增加了錯誤的可能性;或者,他們可能忘記編寫額外的代碼,在這種情況下,可能會在運行時拋出意外的錯誤。相反,定義不存在的錯誤簡化了api,並減少了必須編寫的代碼量。
總的來說,減少錯誤的最好方法是使軟件更簡單。
10.6 屏蔽異常
減少必須處理異常的位置數量的第二種技術是異常屏蔽。 使用這種方法,可以在系統的較低級別上檢測和處理異常情況,這樣較高級別的軟件就不必知道該情況。異常屏蔽在分布式系統中特別常見。例如,在網絡傳輸協議(如TCP)中,可以由於各種原因(如損壞和擁塞)丟棄數據包。TCP通過在其實現中重新發送丟失的包來掩蓋包丟失,因此所有數據最終都能通過,而客戶端並不知道丟失的包。
NFS網絡文件系統中出現了一個更具爭議性的屏蔽示例。如果NFS文件服務器崩潰或由於任何原因沒有響應,客戶端會不斷地向服務器重新發出請求,直到問題最終得到解決。客戶機上的低級文件系統代碼不向調用應用程序報告任何異常。正在進行的操作(以及應用程序)只是掛起,直到操作成功完成。如果掛起持續的時間較長,那么NFS客戶機將在用戶的控制台打印“NFS服務器xyzzy沒有響應,仍然在嘗試”的消息。
NFS用戶經常抱怨他們的應用程序在等待NFS服務器恢復正常操作時掛起。許多人建議,NFS應該在異常情況下中止操作,而不是掛起。然而,報告異常只會使事情變得更糟,而不是更好。如果一個應用程序失去了對其文件的訪問權,那么它就無能為力了。一種可能性是應用程序重試文件操作,但這仍將把應用程序,並且更容易執行重試在NFS層在一個地方,而不是在每個文件系統調用在每個應用程序(編譯器不應該擔心這個)。另一種方法是應用程序中止並將錯誤返回給調用者。調用方也不太可能知道該做什么,所以它們也會中止,從而導致用戶的工作環境崩潰。當文件服務器關閉時,用戶仍然無法完成任何工作,而且一旦文件服務器恢復正常,他們將不得不重新啟動所有應用程序。
因此,最佳的替代方案是NFS屏蔽錯誤並掛起應用程序。使用這種方法,應用程序不需要任何代碼來處理服務器問題,一旦服務器恢復正常,它們就可以無縫地恢復。如果用戶厭倦了等待,他們總是可以手動中止應用程序。
異常屏蔽並非在所有情況下都有效,但在它有效的情況下,它是一個強大的工具。它會產生更深層的類,因為它減少了類的接口(減少了用戶需要注意的異常),並以代碼的形式增加了掩蓋異常的功能。異常屏蔽是降低復雜性的一個例子。
10.7 異常聚合
第三種減少異常復雜性的技術是異常聚合。異常聚合背后的思想是用一段代碼處理許多異常;與其為許多單獨的異常編寫不同的處理程序,不如使用單個處理程序在一個地方處理它們。
考慮如何處理Web服務器中丟失的參數。Web服務器實現一個url集合。當服務器接收到傳入的URL時,它將發送到特定於URL的服務方法來處理該URL並生成響應。URL包含用於生成響應的各種參數。每個服務方法將調用一個較低級別的方法(讓我們將其稱為getParameter)來從URL中提取所需的參數。如果URL不包含所需的參數,則getParameter拋出異常。
當軟件設計類的學生實現這樣一個服務器時,他們中的許多人將每個不同的getParameter調用包裝在一個單獨的異常處理程序中,以捕獲NoSuchParameter異常,如圖10.1所示。這導致了大量的處理程序,所有的處理程序本質上都做相同的事情(生成錯誤響應)。
圖10.1:頂部的代碼分派給Web服務器中的幾個方法中的一個,每個方法處理一個特定的URL。每個方法(底部)都使用來自傳入HTTP請求的參數。在這個圖中,每個對getParameter的調用都有一個單獨的異常處理程序;這會導致重復的代碼。
更好的方法是聚合異常。不捕獲各個服務方法中的異常,而是讓它們向上傳播到Web服務器的頂級分派方法,如圖10.2所示。此方法中的單個處理程序可以捕獲所有異常並為丟失的參數生成適當的錯誤響應。
聚合方法可以在Web示例中更進一步。除了在處理Web頁面時可能出現的參數丟失之外,還有許多其他錯誤;例如,參數可能沒有正確的語法(服務方法期望的是一個整數,但是值是“xyz”),或者用戶可能沒有請求操作的權限。在每種情況下,錯誤應該導致錯誤響應;錯誤只在響應中包含的錯誤消息中有所不同(“URL中不存在參數‘quantity’”或“quantity”參數的“bad value’xyz”;必須是正整數”)。因此,導致錯誤響應的所有條件都可以使用一個頂級異常處理程序來處理。可以在拋出異常時生成錯誤消息,並將其作為變量包含在異常記錄中;例如,getParameter將生成“URL中不存在參數‘quantity’”消息。頂級處理程序從異常中提取消息並將其合並到錯誤響應中。
圖10.2:這段代碼在功能上與圖10.1相同,但是異常處理已經聚合:dispatcher中的一個異常處理程序從所有url特定的方法捕獲所有NoSuchParameter異常。
從封裝和信息隱藏的角度來看,上述聚合具有良好的特性。頂級異常處理程序封裝了關於如何生成錯誤響應的知識,但它對特定的錯誤一無所知;它只使用異常中提供的錯誤消息。getParameter方法封裝了有關如何從URL提取參數的知識,並且還知道如何以人類可讀的形式描述提取錯誤。這兩條信息是密切相關的,所以把它們放在一起是有道理的。但是,getParameter對HTTP錯誤響應的語法一無所知。隨着新功能被添加到Web服務器,像getParameter這樣的新方法可能會創建它們自己的錯誤。如果新方法以與getParameter相同的方式拋出異常(通過生成從相同超類繼承的異常,並在每個異常中包含一條錯誤消息),它們可以插入到現有的系統中,而不需要進行其他更改:頂級處理程序將自動為它們生成錯誤響應。
此示例演示了用於異常處理的通用設計模式。如果系統處理了一系列請求,那么定義一個異常來中止當前請求、清理系統狀態並繼續下一個請求是很有用的。異常捕獲在系統請求處理循環頂部附近的單個位置。此異常可在處理請求的任何時刻拋出,以中止請求;可以為不同的條件定義異常的不同子類。這種類型的異常應該與對整個系統致命的異常明確區分開來。
如果異常在處理之前在堆棧上向上傳播了幾個級別,則異常聚合工作得最好;這允許在同一個地方處理來自更多方法的更多異常。這與異常掩蔽相反:掩蔽通常在用低級方法處理異常時工作得最好。對於掩蔽,低級方法通常是許多其他方法使用的庫方法,因此允許異常傳播將增加處理它的位置的數量。屏蔽和聚合的相似之處在於,這兩種方法都將異常處理程序放置在能夠捕獲最多異常的位置,從而消除了許多需要創建的處理程序。
另一個異常聚合的例子發生在用於崩潰恢復的RAMCloud存儲系統中。RAMCloud系統由一組存儲服務器組成,這些服務器保存每個對象的多個副本,因此系統可以從各種故障中恢復。例如,如果服務器崩潰並丟失了所有數據,RAMCloud將使用存儲在其他服務器上的副本來重新構建丟失的數據。錯誤也可能在較小的范圍內發生;例如,服務器可能發現某個對象已損壞。
對於每種不同類型的錯誤,RAMCloud沒有單獨的恢復機制。相反,RAMCloud將許多較小的錯誤“提升”為較大的錯誤。原則上,RAMCloud可以通過從備份副本中恢復一個損壞的對象來處理這個損壞的對象。然而,它並不這樣做。相反,如果它發現一個損壞的對象,它會使包含該對象的服務器崩潰。RAMCloud使用這種方法是因為崩潰恢復非常復雜,而且這種方法最小化了必須創建的不同恢復機制的數量。為崩潰的服務器創建恢復機制是不可避免的,因此RAMCloud對其他類型的恢復也使用相同的機制。這減少了必須編寫的代碼量,而且這還意味着服務器崩潰恢復將更頻繁地被調用。因此,恢復中的bug更有可能被發現和修復。
將損壞的對象升級到服務器崩潰的一個缺點是,它大大增加了恢復的成本。這在RAMCloud中不是問題,因為對象損壞非常罕見。然而,錯誤提升對於頻繁發生的錯誤可能沒有意義。舉個例子,當一個服務器的網絡數據包丟失時,它不可能崩潰。
考慮異常聚合的一種方法是,它用一種能夠處理多種情況的通用機制替代了幾個專門用於特定情況的機制。這又一次說明了通用機制的好處。
10.8 事故?
降低異常處理復雜性的第四種技術是使應用程序崩潰。 在大多數應用程序中都會有一些不值得處理的錯誤。通常,這些錯誤很難或不可能處理,而且不經常發生。為響應這些錯誤,最簡單的方法是打印診斷信息,然后中止應用程序。
一個例子是在存儲分配期間發生的“內存不足”錯誤。考慮C中的malloc函數,如果它不能分配所需的內存塊,它將返回NULL。這是一種不幸的行為,因為它假設malloc的每個調用者都將檢查返回值,並在沒有內存時采取適當的操作。應用程序包含大量對malloc的調用,因此在每次調用后檢查結果會增加很大的復雜性。如果程序員忘記了檢查(這是很有可能的),那么如果內存耗盡,應用程序將取消對空指針的引用,從而導致掩蓋真正問題的崩潰。
此外,當應用程序發現內存耗盡時,它也無能為力。原則上,應用程序可以尋找不需要的內存來釋放,但是如果應用程序有不需要的內存,它可能已經釋放了內存,這將在一開始就防止內存不足的錯誤。今天的系統有如此多的內存,以至於內存幾乎永遠不會用完;如果是,通常表示應用程序中有bug。因此,嘗試處理內存不足的錯誤很少有意義;這造成了太多的復雜性,而得到的好處卻太少。
更好的方法是定義一個新的方法ckalloc,它調用malloc,檢查結果,如果內存耗盡,則用錯誤消息中止應用程序。應用程序從不直接調用malloc;它總是調用ckalloc。
在較新的語言(如c++和Java)中,如果內存耗盡,新的操作符會拋出異常。捕獲這個異常沒有多大意義,因為異常處理程序很可能也會嘗試分配內存,這也會失敗。動態分配內存是任何現代應用程序的基本元素,如果內存耗盡,應用程序繼續運行是沒有意義的;一旦檢測到錯誤,最好立即崩潰。
還有許多其他的錯誤示例,崩潰應用程序是有意義的。對於大多數程序,如果在讀取或寫入打開的文件時發生I/O錯誤(例如磁盤硬錯誤),或者無法打開網絡套接字,應用程序無法進行太多的恢復,因此使用明確的錯誤消息中止是一種明智的方法。這些錯誤並不常見,因此不太可能影響應用程序的整體可用性。如果應用程序遇到內部錯誤(如不一致的數據結構),也可以使用錯誤消息中止。這樣的條件可能表明程序中存在bug。
崩潰是否可以接受取決於應用程序。對於復制的存儲系統,由於I/O錯誤而中止是不合適的。相反,系統必須使用復制的數據來恢復丟失的任何信息。恢復機制將為程序增加相當大的復雜性,但是恢復丟失的數據是系統向用戶提供的價值的重要組成部分。
10.9 設計不存在的特殊情況
定義錯誤使其不存在是有意義的,同樣,定義其他特殊情況使其不存在也是有意義的。特殊情況會導致代碼中充斥着if語句,這使得代碼難以理解並導致bug。因此,應盡可能消除特殊情況。實現這一點的最佳方法是,以一種無需任何額外代碼就能自動處理特殊情況的方式來設計正常情況。
在第6章描述的文本編輯器項目中,學生必須實現一種選擇文本和復制或刪除選擇的機制。大多數學生在他們的選擇實現中引入了一個狀態變量來表示選擇是否存在。他們之所以選擇這種方法,可能是因為有時在屏幕上看不到選擇,所以在實現中表示這種概念似乎是很自然的。然而,這種方法導致了大量的檢查來檢測“無選擇”條件,並對其進行特殊處理。
通過消除“沒有選擇”的特殊情況,可以簡化選擇處理代碼,使選擇始終存在。當在屏幕上沒有可見的選擇時,可以用一個空的選擇在內部表示它,它的起始位置和結束位置是相同的。使用這種方法,可以編寫選擇管理代碼,而不需要檢查“沒有選擇”。復制選擇時,如果選擇為空,則將在新位置插入0字節(如果實現正確,則不需要作為特殊情況檢查0字節)。類似地,應該可以設計用於刪除選擇的代碼,以便在不進行任何特殊情況檢查的情況下處理空的情況。考慮在單行上進行選擇。要刪除所選內容,請提取所選內容之前的行部分,並將其與所選內容之后的行部分連接起來,以形成新行。如果選擇為空,則此方法將重新生成原始行。
這個例子也說明了第7章中“不同的層,不同的抽象”的思想。“無選擇”的概念對於用戶如何考慮應用程序的接口是有意義的,但這並不意味着它必須在應用程序內部顯式地表示。有一個總是存在的選擇,但有時是空的,因此是不可見的,結果是一個更簡單的實現。
10.10 做過了頭
定義異常或在模塊內部屏蔽異常,只有在模塊外部不需要異常信息時才有意義。本章中的示例也是如此,比如cl unset命令和Java子字符串方法;在調用者關心由異常檢測到的特殊情況的罕見情況下,可以通過其他方式獲取此信息。
然而,這種想法可能會走得太遠。在一個用於網絡通信的模塊中,一個學生團隊屏蔽了所有的網絡異常:如果發生了網絡錯誤,模塊捕獲它,丟棄它,然后繼續處理,就好像沒有問題一樣。這意味着使用該模塊的應用程序無法查明消息是否丟失或對等服務器是否故障;沒有這些信息,就不可能構建健壯的應用程序。在這種情況下,即使異常增加了模塊接口的復雜性,模塊也必須公開異常。
與軟件設計中的許多其他領域一樣,對於例外,您必須確定什么是重要的,什么是不重要的。不重要的事情應該隱藏起來,越多越好。但當某件事很重要時,它必須被曝光。
10.11 結論
任何形式的特殊情況都會使代碼更難理解,並增加bug的可能性。 本章重點討論異常,它是特殊情況代碼最重要的來源之一,並討論了如何減少必須處理異常的地方。最好的方法是重新定義語義來消除錯誤條件。對於無法定義的異常,您應該尋找機會在較低的層次上屏蔽它們,這樣它們的影響就有限了,或者將幾個特殊情況處理程序聚合到一個更通用的處理程序中。總之,這些技術可以對整個系統的復雜性產生重大影響。