[讀書心得]Code Craft - 防禦性編程技巧

  • 19475
  • 0

[讀書心得]Code Craft - 防禦性編程技巧

前言

書籍資訊:Code Craft,簡體中文書名為:編程匠藝,繁體中文書名為:編程創藝

第一章即為『防禦性編程技巧』,這些東西之前充斥在我腦袋中,但卻不知道該如何來給這些想法、技巧,定義一個詞彙來說明,現在知道了,就稱作『防禦性編程』。

這一篇文章,則針對第一章的內容做個摘要,以及個人想法的補充。(因為我看的是簡體版的,所以有一些使用的詞彙會比較偏簡體用法,請見諒。)

 

程式碼種類

基本上程式碼可分成三類:

  1. 能用:在期望的正常輸入情況下不會出錯。
    但輸入特殊資料則可能crash或發生錯誤。
  2. 正確:不管何種輸入,執行不會出錯,錯誤控制得宜。
    但不一定好維護、好擴充。
  3. 健壯(robustness):要兼顧健壯、效能、正確。
    考量到的面向較廣,例如:multi-thread的thread-safe、可維護性、彈性、Scalability。

以最壞的打算為底

在設計程式時,不要做過多的假設,這些假設都應該訴諸於約束。也就是應該額外撰寫約束的檢查,也就是Contract,來避免假設的成立,而是直接驗證其邏輯或前提是否吻合需求。

因為:

一旦有所假設,就一定會發生違反假設的情況。

什麼是假設?例如:

  1. 這一定有資料
  2. 這一定可以轉型成功
  3. 一定不會超出索引範圍
  4. 使用者會按照我們設想的方式來操作系統

防禦性編程的意義

  1. 儘早發現小問題,避免釀成大問題或急迫性問題。
  2. 防止程式出錯的措施,或在錯誤以無法理解的方式出現之前,發現錯誤。
    尤其是因為設計時的假設,所引起的問題。

又大又壞的世界

  1. 不要假設資料是正確的、安全的、沒問題的。
  2. 不要假設使用者是好人,更應該把每個使用者當做壞人
  3. 每個Request都可能是攻擊的行為。

防禦性編程技巧

  1. 好的編碼風格和設計:
    例如使用StyleCop,定義出屬於團隊的coding style、coding rule等等…
  2. 不要倉促地編寫代碼:
    (1)一次只做一件事
    (2)透過草圖來釐清想法
    (3)抽象看系統,用人話描述系統
    (4)透過TDD來思考
  3. 不要相信任何人:
    (1)外部資料
    (2)外部服務
    (3)正常使用者
    (4)惡意使用者
    (5)團隊成員的假設(包括自己)
    任何資料、行為、權限,都應該驗證與約束。
  4. 編碼的目標是清晰,不是簡潔:
    (1)簡單就是一種美,清晰到一目了然,稱為簡單
    (2)寧可選擇與預期較為符合的程式碼,即使不夠優雅。也不要選擇可以達成目的,但簡潔到沒人看的懂的程式碼。
  5. 不要讓任何人做他們不該做的修補工作:
    (1)最小知識原則
    (2)封裝
    (3)該用private的地方,就不要開放更大的存取層級
    (4)把所有變數生命週期保持在最小的範圍內
  6. 編譯時,打開所有警告:
    編譯器的警告可以捕捉許多愚蠢的編碼錯誤,這些錯誤因子在任何時候都可能被引爆。不管什麼情況下,都把警告的開關打開,確保撰寫出的程式碼,可以安安靜靜的完成編譯。
  7. 使用靜態分析工具:
    就像做健康檢查報告一樣意思,透過數據來進行修正,可以讓程式碼越來越健壯。例如FxCop, SourceMonitor, Simian等工具…
  8. 使用安全的數據結構:
    例如.NET使用managed code,以及考慮thread-safe的資料結構。
  9. 檢查所有的回傳值:
    任何回傳都可能是種錯誤,不符合預期的情況,針對這樣的情況,我們必須辨別出回傳的狀態,並做出相對應的錯誤處理(也有可能是商業邏輯錯誤)
  10. 審慎處理記憶體管理,以及其他資源:
    追求最佳效率的作法,例如:
    (1)了解stack與heap的差異
    (2)了解GC的運作原理
    (3)了解boxing/unboxing
    (4)了解reflection優缺點
    (5)了解try/catch effort
    (6)了解using, Dispose與Finalize
  11. 在宣告位置初始化所有變數值:
    C#編譯器會強迫檢查區域變數。
  12. 推遲宣告變數:
    指的是讓變數的宣告位置盡量接近使用它的位置,好維護也較有效率。
  13. 使用標準語言工具:
    避免使用編譯器預設值,例如宣告class,不定義其訪問存取層級。
  14. 使用好的記錄Log工具:
    開發與偵錯時,可能會加入一些幫忙偵錯的code,但發佈時留著可能造成額外問題,或影響效率。如果移除了,下一次偵錯可能又需要加入同樣的code。在.NET中可以透過Debug類別來協助,在Release版本編譯時,被Debug宣告的方法內容將被清空。
  15. 審慎使用強轉型:
    (1)了解is/as/強轉型。
    (2)當可能出錯,它就真的會出錯。
    (3)小心隱含轉換與精準度的差異。
  16. 細則(設計時的小地方):
    (1)提供預設行為。例如switch/case時的default case處理。不帶else子句的if子句,思考是否該控管其預設行為。
    (2)遵循語言習慣。例如.NET的命名,會在介面前加上I。
    (3)檢查數值上下限。例如防呆的設計,是否overflow、index out of range、除以0的錯誤等等…這邊也可以透過code contracts實作。
    (4)正確設定常數。應了解const的定義與使用方式。const效能高,但要注意重新編譯的參考問題,可能會影響線上運作不一致的情況

約束(Contract)

Code contracts基本上分為三類:

  1. 前置條件(pre-conditions)
  2. 後置條件(post-conditions)
  3. 不變條件(object invariants)

可以參考蹂躪的簡介

使用斷言(assert),來確保程式在執行過程中,仍可符合預期,且在該檢查的時機就檢查出問題,避免當出錯時,程式碼額外執行了不該執行的部份。簡單的說,在第一時間把問題抓出來。

約束的內容包括了幾種:

  1. 檢查資料是否都在邊界內
  2. 檢查物件是否應為非Null
  3. 確保函數參數有效性
  4. 在函數回傳結果前,對其進行充分檢查
  5. 操作對象前後,確保沒有違反物件的某些限制

實作的重點則在於:在主要函數中,放置前置條件與後置條件,在關鍵循環中放置不變條件。

並記得在開發與偵錯階段啟用,正式發行環境關閉其作用。

當發現並修正一個bug時,在修正錯誤的地方,加上斷言是一個好習慣,避免錯誤不會重複發生。所以每一次bug的產生,都是一種珍貴的回饋,透過斷言來避免同樣的問題,因為不同的原因而再次發生。

 

結論

這邊只是簡單的做個讀書心得筆記,以及摘要。

也希望可以讓讀者朋友們了解到,程式碼不是會動就好,可以審思一下,我們面對的程式碼,是屬於三類的哪一類?通常都不是健壯那一類…


blog 與課程更新內容,請前往新站位置:http://tdd.best/