防御式編程


在軟件開發過程中,不可避免的會遇到錯誤處理,而且這部分對於整個軟件的健壯性有非常大的作用,它是軟件除了功能性以外最重要的指標了,一個軟件成功與否與其健壯性有很大的聯系。我在以前的開發中也時常思考錯誤處理,因為這部分代碼邏輯比較不容易梳理清楚。以異常的處理為例,以前通常就采用比較簡單粗暴的處理方式:用try..catch加Exception把所有異常都包起來,這樣簡單省事,寫的代碼最少,相信很多童鞋曾經跟我一樣寫過這樣的代碼,很明顯,這樣寫有很大的問題,最主要的問題在於:

  • Exception會吃掉所有可以處理的異常,使得對於某些我們關心的異常無法捕獲,因為對於不同的異常我們可能需要做不同的處理,有些可以在本函數內處理掉,有些需要提示用戶(例如文件不存在,網絡無法訪問),有些需要告訴上一層代碼該如何處理,所有這些在直接用Exception處理異常時都無法做到,簡而言之就是無法做到異常的精細化處理

那么怎么做才好呢?這部分代碼真不少,雖然無關軟件功能性,但是確是健壯性的基礎。具體如何處理這些沒有完全標准的答案,軟件設計本來就是一項帶有藝術色彩的智力勞動,沒有一勞永逸的解決方案,最關鍵的在於掌握好基礎知識,因地制宜地采取措施。下面主要談談實現健壯性的基本技術,基本的實現軟件健壯性的技術有以下幾種:

  • 斷言
  • 錯誤處理
  • 異常
  • 從設計上簡化異常處理的技術;隔離程序
  • 輔助調試的代碼(print打印之類的小段函數)

1 斷言

斷言是個非常常用的軟件設計技術,基本上現代的高級程序設計語言都支持它,那么斷言到底是什么?很簡單,就是一個判斷一個布爾表達式的語句,如果這個布爾表達式為真,不會有任何效果,但是如果為假,根據不同實現技術會出現不同的效果,但是基本上都是會告訴程序員(注意,不是用戶),這里有一個斷言,去看一下。

上面是一個簡單的形式化的定義,從定義中我們不難看到,斷言是給程序員看的,為什么?用來查找bug。所以我們應該在內部邏輯的問題上使用斷言去檢查一些理論上不可能發生的情況,因為如果發生了就說明內部邏輯有問題,也就是有bug了。

舉個簡單的例子,例如某個函數有一個參數,這個參數是某數據流,這個數據流是軟件下層通過讀取文件傳進來的,調用這個函數的時候,內部邏輯已經確定是正確讀取到了文件,否則是不會調用這個函數的,那么,一般會在函數開頭,對這個參數用斷言加以檢查,如果不幸,出現問題,就說明內部邏輯錯了(讀取失敗仍然調用?內存被意外析構?),這就是典型的通過斷言查找程序bug的例子。

有一點需要注意,斷言是用來檢測程序內部邏輯的,如果是和外部有數據交流,就不是斷言的范疇,因為外部的情況,程序是不能假定的,既然不能假定,就無法設斷言了,那應該是錯誤處理或者異常的范疇了,因此,理解斷言的關鍵點在於,作用於內部邏輯,用來查找bug。通常,現代編譯工具都會在編譯release版本軟件的時候去掉異常,因為異常是給程序員看的。

2 錯誤處理

錯誤處理可以說是軟件健壯性的核心,程序員在編寫軟件的時候,應該盡可能的預測到可能發生的錯誤,並對這些錯誤進行處理,正常情況下要對這些錯誤進行分類,

  • 重大錯誤,這類錯誤一般不可恢復,通常的做法都是報告后直接退出,類似windows中的藍屏,普通程序在遇到堆棧溢出,內存不足等錯誤時也是會這樣做;
  • 無關用戶的一般性錯誤,這類錯誤一般情況下不會導致程序退出,而且和用戶沒有直接的聯系,這時最好的做法是能自動恢復並解決,如果不行,可以寫入日志,以便以后進行排查,不過通常情況下需要用相對抽象的語言告訴用戶(例如,程序遇到問題,可能是某些文件找不到),只是為了讓用戶知道這個操作沒有成功,具體的技術原因可以寫入日志。
  • 與用戶相關的一般性錯誤,這類錯誤通常是由於用戶輸入錯誤數據引起,例如本來程序UI需要用戶輸入年齡,結果用戶輸錯,填入的不是整數。這個時候,通常需要告訴用戶,讓用戶重新輸入,以達到自動恢復的作用。所以通常的做法,都是彈出對話框(有UI)或者輸出提示到標准輸出(無UI);

理解錯誤處理的關鍵在於分清楚項目需要處理錯誤的類型,以及如何處理(集中處理?寫入日志嗎?通過網絡提交錯誤報告?),要根據項目的類型設計好采取的策略(例如Service一類的通常都是只記入日志(會有各種日志,函數調用日志,錯誤日志,性能日志等等),因為不直接和用戶打交道),具體情況具體分析地設計錯誤處理策略,並對不同的錯誤采取恰當的處理方式。

3 異常

異常是指程序無法預料到的情況引發的錯誤,通常本函數不知道這種錯誤該如何處理需要讓調用方決定(例如系統庫函數,像.NET的庫函數都會有拋出異常的列表)。這通常是由語言支持的,在遇到異常而又沒有捕獲時,會中斷本函數的執行去查看調用方是否處理,這就有了一種直接中斷函數處理的方式,有人會說為什么不直接return呢?是的,return可以達到中斷函數執行,但是卻無法像異常那樣讓調用方針對特定的異常做出特定處理,畢竟return的東西有限,無法表示錯誤的類型,通常都只能返回一個false。

以.NET的CLR對異常處理機制(兩輪遍歷)為例:

  • 發生異常后,CLR先去在引發異常的那一層搜索catch語句,看看有沒有兼容此類型異常的處理代碼,如果沒有,則跳到上一層去搜索,如果還沒有,則再上一層,直到應用程序的最頂層,此即為第一輪,查找合適的異常處理程序。
  • 如果在某一層找到了異常處理處理程序,CLR不會馬上執行,而是回到事故現場再次進行第二輪遍歷,執行所有中間層次的finally語句塊。

可見,異常的出現使得我們對於無法在本函數(局部)處理的錯誤提供了一種強大的手段,使得我們能夠清楚的告訴函數調用鏈的上層,某函數發生錯誤了,需要處理。所以,理解異常,就要知道它是處理無法在本函數處理的錯誤,同時,一般情況下不要用Exception吃掉所有的異常,而要對異常進行精細化處理。但是也不是完全不用它,因為沒有處理的異常通常會導致程序直接崩潰,這對用戶非常不友好,所以處理異常要特別謹慎,我通常會在函數調用鏈的頂層使用Exception,並計入日志,以防止這一情況的發生。

4 隔離程序以簡化錯誤處理

這是一種在設計上簡化錯誤處理的策略,事實上,如果所有的代碼都做異常和錯誤處理,會使代碼變得臃腫,可讀性下降,我們需要在高層次上面避免這種情況的發生,這個思想來自代碼大全,不過實際開發中也已經用到了,這里做個總結。我比較同意這種設計思想,本質上,它是將錯誤和異常處理集中化,通常的軟件設計實際上都是對數據進行處理和再加工,以及展現,很大一部分的錯誤都是由於不正確的數據設置導致的,那么我們可以把數據的錯誤處理專門用一層來處理以使得內部的邏輯可以不用對數據進行檢測,見下圖:

 

image

上圖很清晰的說明了這一過程,簡而言之,就是專門增加了一層來專門處理數據,以解放內部邏輯,這樣結構更加清晰

5 總結

我始終認為軟件的好壞與其健壯性有很大的聯系,所有的軟件開發人員都要對它有足夠的重視,從一點一滴開始做起,不要忽視任何的細節,不能盲目依賴測試去發現bug,而是以測試驅動編程,不斷地思考可能發生的問題以進行預防,這才是防御式編程,在這里做個記錄,與諸君共勉,馬上又要重構代碼去了:)。

 

參考文獻

《代碼大全》第二版  第八章 防御式編程

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM