防禦性程式設計與瘋狂偏執性程式設計

2015-03-25    分類:其他、程式設計開發、首頁精華5人評論發表於2015-03-25
啊,這裡要小心!   ——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,但是,由於防禦性程式碼用於處理異常,所以想要給它做測試並且確保它能夠有效工作就會顯得非常非常難。理解檢查條件和明確需要怎麼樣的防禦程式碼是需要經驗積累的,處理產品中的程式碼並預見現實世界中會出現的問題,同樣如此。

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

相關文章