多疑到剛剛好:防御性編程
英文鏈接:Defensive Programming: Being Just-Enough Paranoid
每當程序員突然遇到某個bug并不知道怎么改的時候,他們會添加一些“防御性代碼”來使編碼更安全并且更容易找到問題的原因。有時這樣做可以消 除錯誤。他們加強了數據的有效性驗證——檢驗輸入框、輸出框和返回值的內容;審查并改善錯誤處理——可能會添加一些針對于出現“異常”情況的驗證代碼;添 加一些有用的日志和診斷。換句話說,就是最好一開始就應該出現在那兒的代碼。
事先預料到無法預料的情況
防御性編程的關鍵在于未雨綢繆,防患于未然。
—— Steve McConnell, 《Code Complete》(中文版:《代碼大全》)
在Steve McConnell的經典編程書籍,《Code Complete》里的幾個簡單章節里講到了防御性編程的幾條基本規則:
1.
保護你的代碼不要受“外界”的無效數據影響,“外界”影響有很多種情況。外部系統的數據,某個用戶的操作或模型/組件外面的數據。任何在控制范圍之外的東
西都是危險的,而任何在控制范圍之內的都是安全的,所以要設立“安全區”。在安全區域的代碼會驗證所有的輸入數據:檢查所有輸入參數的類型,長度和取值范
圍。可以通過雙擊來檢測有沒有溢出。
2. 當檢驗到了錯誤的數據后,可以考慮如何處理它。防御性編程并不意味著要忍受錯誤或是避開錯誤。它意味著要從健壯性(如果遇到你能處理的問題時能保持程序運行)和正確性(不會返回錯誤的結果)之間權衡最合適的處理方式。可以選擇一種策略來處理錯誤的數據:報錯并立刻停止程序(快速結束),返回一個替代的數據值,等等,總之要確保這個策略是明顯一致的。
3. 不要以為在代碼外進行函數調用和方法調用會像你所想的那般順利。你要明白這一點,并在外部的API和庫里測試你的錯誤處理。
4. 在開發和測試的情況下,可以使用斷言來假設某種“可能出現”的條件并特別顯示出來。這對于需要不同的人在各個時間進行維護的高可靠性大型系統來說尤其重要。
5. 添加診斷代碼可以智能記錄并追蹤代碼,它可以解釋運行時當前的情況,尤其在遇到某個問題時它的幫助會更大。
6. 錯誤處理需要標準化。要考慮遇到“一般錯誤”、“預料中錯誤”和警告時的各種處理方式,決定好之后就不要再改了。
7. 只有在你需要的時候,并且你對編程語言的異常處理極為熟悉才可以使用異常處理。
在一般的錯誤處理中使用異常處理的程序會引起可讀性和可維護性的代碼問題。
——《The Pragmatic Programmer》(中文版:《程序員修煉之道:從小工到專家》)
我想再加兩條規則。一個是Michael Nygard的Release It!中提到的,絕對不要去不斷地等待某個外部的調用,尤其是遠程調用。如果什么地方出現問題了,時間會非常漫長。使用暫停/重試的邏輯方法和他的 Circuit Breaker穩定方案可以解決遠程問題。
另一個規則是,對于像C和C++的語言,防御性編程也包括使用安全函數調用來避免緩沖區溢出和其他常見的代碼錯誤。
不同類型的質疑
The Pragmatic Programmer把 防御性編程描述為“防御性質疑”。它保護你的代碼不受其他人的錯誤或你自己的錯誤影響。如果懷疑數據的有效性,可以檢驗數據的一致性和完整性。你不能測試 所有的錯誤,所以要使用斷言和異常處理來對付“發生了預料之外”的事情。在程序的測試中你會學到一些知識,如果程序出錯了,去找找還有什么地方會出錯。著 重注意最核心的重要代碼。
合理的質疑編程是一種正確的編程習慣。不過質疑太過分了可能過猶不及。在Clean Code(中文版:《代碼整潔之道》)里關于錯誤處理的章節里,Michael Feathers提醒道:
(error handling)錯誤處理的代碼可能會制約許多代碼的本質意義
許多錯誤處理代碼不僅會混淆代碼的主要流程(也就是代碼實際要做什么),還會混淆錯誤處理本身的邏輯——這樣很難做到正確,很難審查和測試,很難在更改代碼后不引起錯誤。代碼不再靈活安全了,它實際上會變得更脆弱,容易引發問題。
防御性編程可以采取的,有合理的質疑方法,也有過分的質疑方法,還有近于瘋狂的質疑方法。
我第一個接觸的世界級系統是一個在跨越了美國加拿大的服務器上“Store and Forward”網路控制系統(也被稱為微型電腦)。它在網路上的分布式系統,計劃作業,和坐標報告之間分享數據。它被設計為遇到網路問題時能靈活處理, 在遇到操作失誤時會自動恢復和重啟。這在當時是非常具有技術性挑戰的。
最初維護這個系統的程序員并不相信網路,系統和操作會是永遠正常的,也不相信其他人的代碼,甚至是自己的代碼是毫無破綻的。他是一個從化學專業 自學轉到系統工程師的,他喜歡在晚上很晚的時候喝很多酒,并在那種狀態下寫幾千行松散的FORTRAN或匯編的代碼。代碼里充滿了錯誤檢查、自我診斷和錯 誤校正,文件和數據包都有它們自己的校驗和、文件級密碼和隱藏的控制標簽,也有很多代碼可以控制計算異常和計時問題——代碼幾乎所有時間都在運行。如果代 碼遇到什么無法分析的問題,程序會崩潰并報告一個“退出標簽”并且轉儲變量的內容——就像現在所說的堆棧跟蹤。你理論上可以利用這些信息來檢查代碼并查出 里面到底發生了什么。這些看起來都不是在學校里可以學到的。閱讀和運行代碼不會再覺得受限制。
如果遇到難以修改的bug,使代碼不能繼續運行。他會找一個辦法來處理bug使系統可以保持運行。在他離開公司之后,如果代碼遇到某個網路上的 bug,我就可以通過那些“錯誤校驗”代碼找到并修改這個bug。當我解決完問題之后,就可以安全地移除這些“保護代碼”,這樣清理錯誤處理代碼可以使我 在維護系統時不會刪掉重要的東西。我設立了安全區——其實我也不知道怎么稱呼比較好——來分析什么數據是有效的,而什么是無效的。做到這點就能簡化防御性 代碼以便我更改代碼也不會引起系統本身的出錯,并能保護核心代碼不受無效數據、代碼中某些錯誤或操作失誤的影響。
維護代碼安全很簡單
防御性編碼的要點是讓代碼更安全并減少維護代碼的人的工作。防御性代碼和普通的代碼一樣都有bug,因為防御性代碼是用來處理異常的,所以測試 尤其困難,也很難保證在代碼運行時能正常工作。理解哪些條件下要使用防御性代碼并使用多少防御性編碼,需要在實際編程工作中多觀察來積累經驗。
許多涉及到設計和建立安全靈活系統的工作都是技術難以實現的或是花費消耗極大的。而防御性編程兩者都沒有——它有些像防御性駕駛,也就是所有人都很容易去理解的。它需要規范和意識,對細節加以注意,若我們想讓代碼變得安全,就都會用到它的。 原文鏈接