防御性編程與瘋狂偏執性編程

jopen 9年前發布 | 5K 次閱讀 編程

防御性編程與瘋狂偏執性編程

啊,這里要小心!   ——Sergeant Esterhaus,《每日簡報》

當程序員遇到意想不到又不能修復的 bug 時,他們會“添加一些防御性的代碼”,這不但可以使得代碼更安全,還更容易發現問題。有時候這樣的行為甚至可以直接消滅問題。開發人員還會進行數據驗證 ——確保檢查輸入和輸出域和返回值;審查和改進錯誤處理——可能會圍繞一些“不可能”的條件做一些檢查;添加一些有用的日志記錄和診斷。換句話說,問題代 碼優先。

期待意外

防御性編程的整體要點就是防范你不想要出現的錯誤。——Steve McConnell,《Code Complete》

Steve McConnell 的經典編程之書——《Code Complete》,用一個短篇解釋了防御性編程的一些基本規則:

1、保護你的代碼遠離來自“外部”的無效數據,無論這個“外部”的概念被定 位為什么。它可以是來自于外部系統、用戶、文件的數據,也可以是模塊/組件以外的數據,由你決定。樹立“路障”、“安全區”或“信任邊界”——在邊界之外 的一切都是危險的,界限之內的所有都是安全的。關于“路障”代碼,需要驗證所有的輸入數據:檢查所有輸入參數的類型、長度和值域是否正確。還要加倍檢查限 制和界限。

2. 當我們檢查出錯誤數據后,還需要決定如何處理它。防御性編程不會掩蓋錯誤,也不會隱藏 bug。這需要在健壯性(如果問題可以處理那就繼續運行)和正確性(不返回不準確的結果)之間做權衡。選擇好策略來應對錯誤數據:返回錯誤就馬上停止,返 回中性值就替換數據值……確保策略明確且一貫。

3. 不要將代碼外部的函數調用或方法調用想得太過美好。請確保你調用外部的 API 和庫之前理解并測試了錯誤。

4. 至少在開發和測試階段,要使用斷言記錄假設,并高亮“不可能”的條件。這在大型系統中顯得尤為重要,因為隨著時間的推移,將會有不同的程序員用高度可靠的代碼來維護這些大型系統。

5. 添加診斷代碼,智能地記錄和跟蹤以幫助解釋在運行時發生的事情,尤其是當你遇到問題的時候。

6. 標準化的錯誤處理。想好如何處理“正常錯誤”、“預期錯誤”以及警告,并對此習以為常。

7. 只有當你真的需要的時候,才使用異常處理,并確保你得徹底理解該編程語言的異常處理程序。

如果一個程序將異常作為正常進程的一部分,那就會飽受所有經典的可讀性和可維護性問題導致的代碼混亂不堪的困擾。

–《The Pragmatic Programmer》(程序員修煉之道)

此外,我還想補充幾點,來自于 Michael Nygard 的《Release It》:

  • 千萬不要等著外部調用,尤其是遠程調用。因為一旦出現問題,就會耗費你很長的時間。
  • 使用超時/重試邏輯以及斷路器穩定模式來處理遠程故障。
  • 對于像C和 C++ 這類的編程語言,防御性編程還包括使用安全的函數調用,以避免緩沖區溢出和常見的編碼錯誤。

偏執的不同種類

在《The Pragmatic Programmer》一書中將防御性編程形容為“務實的偏執”。保護你的代碼避免受到別人和自己錯誤的侵襲。有疑問,就驗證。檢查數據的一致性和完整 性。由于我們不能測試每一個錯誤,所以使用斷言和異常處理程序來應對“不應該發生”的事情。從測試和產品失敗中學習 ——出現失敗,就找找看還有哪里也會失敗。關注代碼的關鍵部分——核心,執行目的的那部分代碼。

健康的偏執型編程是正確的編程形式。但偏執程度卻可大可小。在《Clean Code》的錯誤處理章節,Michael Feathers 告誡說,

“在很多代碼庫中,錯誤處理占據了主導地位。”   –Michael Feathers,《Clean Code》

如果代碼中有太多的錯誤處理,那么不僅會掩蓋代碼的主路徑(代碼的實際目標),也會遮蔽錯誤處理本身的邏輯——以至于很難糾正、很難審查和測試,也很難不犯錯誤地更改,最后只能束手無策。這非但不會讓代碼更有彈性和更安全,實際上還會導致代碼更容易出錯和更脆弱。

有健康的偏執,有錯誤檢查過度的偏執,還有瘋狂而有害的偏執——以及防御性編程這四種。

我搞的第一個真正意義上的全球性系統是為服務器(當時還被叫做小型機)跨越美國和加拿大研發的“Store and Forward”網絡控制系統。它在分布式系統、調度作業以及協調整個網絡報告之間分享數據。它的設計目的為可適應網絡問題,并且面對操作失誤可以自動恢 復和重啟。這在那時可謂是史無前例的,但卻是技術人員的噩夢和地獄。

此系統的原有程序員不信任網絡,不信任O/S,不信任操作運算,不信任別人的代碼,甚至也不信任他自己的代碼——理由振振有詞。他曾是一名化學 工程師,自學成為系統程序員——熬夜寫代碼的時候會喝很多酒,然后在酒精的影響下寫下成千上萬行非結構化的 FORTRAN 和 Assembler 代碼。代碼中充滿了錯誤檢查、自我診斷和糾錯碼,文件和數據包有各自的校驗和、文件級密碼和隱藏的控件標簽,并有大量的代碼來處理序列記錄異常和關于時序 的問題。如果出現問題,它就無法恢復,程序也會崩潰,同時報告“label of exit”并清空變量內容——有點像今天的堆棧跟蹤。理論上你可以使用這些信息追溯代碼來弄清楚到底發生了什么。但是所有這一切和我在學校里學到的完全不 同。閱讀和使用這些代碼,感覺能讓人徹底瘋掉。

這個頑固的系統程序員,即使是沒法修復的 bug,也不會阻礙他前進的腳步,因為他會找到一種方法來解決這些 bug,保持系統的運行。然后,在他離開公司以后,我接手了這個系統。我又發現了 bug,特別開心自己修復了它,然而卻不小心在其他地方毀壞了一些“糾錯”代碼,事實上,這些“糾錯”代碼其實依賴于網絡中的 bug 而生存。所以,當我終于理清各種關系之后,我會先盡可能安全地將這些“保護傘”移除,并清理錯誤處理,這樣我就可以放心大膽地去維護系統了。我為代碼設置 了信任邊界——當然那個時候我還不知道應該這樣叫——用來決定什么樣的數據不能被信任,而什么樣的數據是可以信任的。這樣做了之后,我發現我簡化了防御代 碼,這不但有助于在做出修改的同時不引起系統混亂,同時又能保護核心代碼免受不良數據、剩余代碼錯誤以及操作問題的干擾。

讓代碼更安全其實很簡單

防御性編碼的要點就是讓代碼更安全,并幫助其他人維護和支持代碼——而不是使得程序員的工作更為困難。不過,防御性代碼也是代碼——只要是代碼 就會有 bug,但是,由于防御性代碼用于處理異常,所以想要給它做測試并且確保它能夠有效工作就會顯得非常非常難。理解檢查條件和明確需要怎么樣的防御代碼是需 要經驗積累的,處理產品中的代碼并預見現實世界中會出現的問題,同樣如此。

想要設計出一種長效的系統不但是個技術老大難而且成本非常高。防御性編程卻兩者皆非——因為每個人都能理解并辦到。只是,它需要磨練和警覺,需要我們能夠做到注重細節。但是如果我們想要讓世界變得更安全,那么這是我們必須要走的獨木橋。

來自: www.codeceo.com

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!