Code Complete II《軟體建構之道 2》#8 讀書心得與整理

第八章 防禦性程式設計
Defensive Programming

開車有防衛駕駛,寫程式有防禦性程式設計


8.1 保護程式在無效輸入資料的破壞
Protecting Your Program from Invalid Inputs


優秀的程式,無輪輸入什麼內容,都不會輸出垃圾。

a. 檢查所有來自外部的常式的參數
b. 檢查所有來自其它常式的常式的參數→8.5
c. 決定如何處理錯誤的輸入→5.3

最優秀的防禦性程式撰寫
  1. 重複的設計
  2. 程式碼前先寫虛擬碼
  3. 程式碼前先寫測試案例
  4. 進行低層級設計視察






8.2 判斷提示
Assertions

assert(常式);  //若常式為false,則程式崩潰、印出訊息。
用來檢查假設
  1. 是否滿足定義域和值域
  2. 檔案或資料流狀態是否開啟(或關閉)
  3. 檔案或資料流讀寫位置是否在開頭(或結尾)
  4. 檢查檔案或資料流權限(唯讀、唯寫、可讀寫)。
  5. 只能輸入的變數,經過一個常式是否有被改變
  6. 指標是否為null
  7. 陣列項目數量是否為特定數目
  8. 初始化的表格,變數是否都初始化了。
  9. 在常式內的容器,是否為空(或滿)
  10. 最佳化前後的常式,結果是否一致
  11. ....(更多更多的假設)


建立自己的Assertion機制
Building Your Own Assertion Mechanism


標準的C++不提供文字訊息
所以,可以改寫成適合自己使用的ASSERT()
//C++ Example of a Assertion Macro
#define Assert( condition, message )\
{\
  if ( !(condition) )\
  {\
    LogError ("Assertion failed: ", #condition, message);\
    exit( EXIT_FAILURE );\
  }\
}

使用Assertions的方針
Guidelines for Using Assertions

  處理錯誤的程式碼 Assert(判斷提示)
處理什麼狀況 預期會發生 永遠不應發生
檢查什麼 鮮少發生的非一般情況 永遠不應發生
用來檢查 檢查錯誤的輸入資料 檢查程式碼中的bug
異常情況→用處理錯誤的程式碼→從容的做出反映
異常情況→用Assert()→重新編譯程式碼

Assert()中,不可以放置發佈版本會執行的程式碼。
//系統預設的code,assert本身並不會在release編譯時成為release版的程式內容
#ifdef _DEBUG
    ASSERT()
#endif
用Assert()來驗證前置條件和後置條件

檢查輸入 - 前置條件
檢查輸出 - 後置條件

錯誤:
  1. 可靠的內部來源,檢查定義域外的值←用Assert(定義域)
  2. 外部來源,檢查錯誤格式←錯誤處理的程式碼
對強壯的程式碼,應先使用Assert再處理錯誤

常式通常會用
  1. Assert()
  2. 處理錯誤的程式碼
通常不會兩個都用,但是若專案很大,人很多,時間很長,則建議兩個都用。
外部輸入 無法預測,也建議使用。

debug版,兩者都有。
release版,剩下錯誤處理程式碼。
Visual Basic Example of Using Assertion to Document Preconditions and Postconditions
private Function Velocity ( _
    ByRef latitude As Single, _
    ByRef longitude  As Single, _
    ByRef elevation  As Single, _
) As Single

' Preconditions'

Debug.Assert( -90<= latitude And latitude <= 90 )
Debug.Assert( ...)
Debug.Assert( ...)
...
' Sanitize input data. Values should be within the ranges asserted above,'
' but if a value is not within its valid range, it will be changed to the closest legal value'
If  ( latitude < -90 ) Then
     latitude = -90
ElseIf ( latitude > 90 ) then
    latitude = 90
...


8.3 錯誤處理技巧
Error-Handling Techniques

Assert()可用來處理程式中永遠不該發生的錯誤。
那預期發生的錯誤呢?

1. 傳回中性的值
    若程式對於「呈現錯誤資料」的容許度很低,就別考慮這個方式了。

2. 以下一個有效值取代
3. 以上一個有效值取代
4. 以最接近的有效值取代

5. 將警告訊息記錄至檔案
    要考慮檔案的安全性、公開性的容許程度。

6. 回傳Error code
    「處理錯誤」和「反映錯誤」的副程式(或物件)區分清楚。
    低層級的程式碼:反映錯誤
    高層級的程式碼:處理錯誤

    通知系統錯誤的(東西),會下列其中一種
  1.     設定狀態變數的值
  2.     將狀態回傳,作為常式的回傳值
  3.     用程式語言內嵌的例外處理
這種情況之下,與「報告錯誤」的機制相比,「決定系統哪些部份要直接處理錯誤,哪些部份只是報告發生的錯誤」比較重要。

7. 呼叫錯誤處理常式或物件
    設計「集中處理錯誤」的常式或物件。
    不使用此方法,會是基於安全性的考量。

8. 顯示錯誤訊息
    處理錯誤的資源,將會在此方法減到最少。
    難以進行UI一致性和當地語系化。
    可能會受到「錯誤訊息探索」的方法攻擊系統。

9. 在常式內以最僅方法處理錯誤
    錯理錯誤的特定方式由程式設計人員決定。
    無法滿足「正確性」或「強壯性」的需求。
    四處都要寫入「使用者介面程式碼」

10. 關機
      對於特續開機會造成失控的風險,考慮關機是不錯的選擇。
(詳情可參考絕命終結站XDD)

比較強壯性與正確性
Robustness vs. Correctness


定義:
正確性(Correctness):永遠不回傳不正確的結果
強壯性(Robustness):永遠嘗試讓軟體能繼續操作

考量:
人身安全相關軟體  正確性>強壯性
消費性應用軟體    強壯性>正確性

高層次設計的錯誤處理啟示
High-Level Design Implications of Error Processing


錯誤處理的方式,是由「架構上」和「高層次設計時」決定。
錯誤處理必須一致。錯誤務必處理,不可忽視。

ex:
高階處理錯誤
低階報告錯誤(回傳Error code)


8.4 例外狀況
Exceptions


例外狀況是一種特定的方法。

常見語言支援情況
例外狀況屬性 C++ Java Visual Basic
支援try-catch 有 有 有
支援try-catch-finally 無 有 有
可throw出什麼 Execption及其衍生物件。
物件指標、物件參照
資料型別(ex: char*, int)
Execption及其衍生物件 Execption及其衍生物件
未攔截的例外狀況時的預設處理方式 呼叫std::unex pected()
再預設呼叫std::terminate()
再預設呼叫abort()
終止執行緒。 終止程式
必須在類別介面中定義throw出的例外狀況 否 是 否
必須在類別介面中定義catch到的例外狀況 否 是 否

用例外狀況通知其程式
例外狀況最大的好處,就是它能以無法忽略的方式,表示錯誤條件
只在需要在真的例外狀況拋出throw
例外狀況只有Exception的try-catch可以處理,別的程式語法是無法取代的,而作用和assert()一樣,不只是處理罕見發生的事件,也處理永遠不應該發生的事。
請勿將例外狀況來推卸責任
如果子程式可以在local程式碼處理例外狀況,請不要在這樣的程式碼throw出Exception,請在這個local程式碼處理掉。
非在建構和解構式中攔截例外狀況,否則請避免在同樣位置中產生例外狀況
在建構式中發生Exception,那麼解構式中會變得複雜許多,否則就會在尚未建構的物件中解構,讓程式出現問題。
而在建構式和解構式中的Exception也會變得很複雜。
以正確的抽象層產生例外狀況
維護概念整體性
在例外狀況訊息內包含造成例外狀況的所有資訊
足夠多的訊息可以幫助閱讀的人了解情況。
避免空的catch區塊
Bad Java Example of Ignoring an Exception
try{
    ...
    //lots of cods
    ...
} catch( AnException exception) {
    //程式執行到這,不會終止,會當作沒有這個例外而繼續執行。
    //可以的話寫個MessageBox或者記錄到檔案中也好。
    //空白的話讓co-work的程式設計不知道這是怎麼一回事
}
了解程式庫程式碼所產生的例外狀況
程式庫(.dll檔)的Exception務必要全盤了解。
考慮建立集中的例外狀況報告程式
報告成文件或顯示訊息,請統一報告的格式。
標準化專案例外狀況的使用
  • 用Exctption類別裝例外狀況拋出的所有物件。增加使用不同程式碼之間的相容性。
  • for專案的個人化Exception
  • 定義在特定的情況中,程式碼可用throw-catch語法處理。
  • 定義在特定的情況中,程式碼可用throw一個不在local程式碼中處理的exception 。
  • 決定是否使用集中的例外狀況報告格式
  • 定義在構建式與解構式中,是否存在例外狀況。
考慮例外 狀況的替代方法
  • 區域程式處理錯誤
  • 使用error code傳播錯誤
  • 錯誤記錄到檔案中
  • 關閉系統
  • 其它
記住要將程式「設計成語言 (programming into a language)」,而不要掉進了「用語言」設計程式(programming in a language)。


8.5 (防火牆)隔離你的程式,避免錯誤造成損毀
Barricade Your Program to Contain the Damage Caused by Errors


概念:船構造的隔艙,讓船在進水時,可以隔離充滿水的船艙。

在輸入時間,將輸入資料轉換為正確的型別
建立「防火牆」讓資料轉態、檢查資料格式、檢查資料內容,直到正確,才進入運算。

防火牆與判斷提示的關係
Relationship between Barricades and Assertions

防火牆外的常式,使用錯誤處理(各種適合的方法),假設資料都不安全
防火牆內的常式,使用assert()。
防火牆內偵測到錯誤的資料,代表程式有錯,而不是資料有錯。


8.6 除錯輔助工具
Debugging Aids


不要在debug版程式直接套用release版程式的規範
Don’t Automatically Apply Production Constraints to the Development Version

release debug
速度 快 不一定要快
資源 愈少愈好 盡量使用
操作的安全性 必須要安全 可使用不安全的操作
最優先的考量:保持開發過程的流暢

儘早採用除錯輔助工具
Introduce Debugging Aids Early

遇到了十次相同的問題,第十一次之後受不了了,寫了一個除錯輔助工具之後,再也不會遇到這個問題,心情愉快、工作順利,程式碼都乖乖的呢。
為了節省時間,保持開發過程流暢,不影響心情。請儘早使用。

通常會有三種情況,會取得除錯輔助工具
  1. 遇到了好幾次相同的問題。
  2. 初次寫除錯輔助程式
  3. 使用之前專案的除錯輔助工具

使用自殺式程式設計(原意:攻擊式程式設計)
Use Offensive Programming

應該以這麼一種方式來處理異常情況:在開發階段將它突顯出來,在產品代碼時它可以自我修復。←就是Offensive Programming

自殺式程式設計方法有幾個原則(看完就知道為什麼我這樣翻譯了)
  • 確定以assert()中止程式
  • 完全填滿記憶體,偵測記憶體配置問題
  • 完全填滿分配的檔案,偵測出檔案格式的錯誤
  • 確定每個「case陳述式的default」或「else子句」的程式碼能產生錯誤(ex: 中止程式)或無法被忽略。
  • 刪除物件前,填入垃圾值,確定它再也用不到。
  • 設定程式將錯誤記錄檔以mail的方式寄給自己,以了解release後的軟體中發生的錯誤種類。

計畫性移除錯輔助工具
Plan to Remove Debugging Aids

為了大小和速度必須拆除鷹架(除錯輔助程式碼)

使用版本工具,並建立ant和make等工具
ant 是java使用的工具,make就是makefile的工具。

使用內嵌的前置處理器
技巧性的使用條件編譯,可以留下鷹架,讓它不會編譯進release版
使用前置處理器的code,讓#ifdef-#endif這樣的程式碼不會讓你的程式變醜醜的。
#define DEBUG
...
#if define( DEBUG )
// debugging code
...
#endif
#define DEBUG
#if define( DEBUG )
#define DebugCode( code_fragment ) { code_fragment }
#else
#define DebugCode( code_fragment )
#endif
...
DebugCode(

    statement 1;
    statement 2;
    statement 3;

);
...
撰寫自己的前置處理器
Java沒有,就自己寫一個吧!
使用除錯虛設常式
虛設的意思就是只剩下殼而沒有內容的常式,原文用stubs(存根)這個字。
相較於前置處理器危險的做法,這種做法可以讓編譯器幫你檢查除錯輔助程式的錯誤,並且有和條件編譯有相同的效果。
void DoSomething(SOME_TYPE *pointer)
{
    //check parameters passed in
    CheckPoint( pointer );
    ...
}
debug版的程式碼,必須全面性的檢查錯誤,秏時但有效
void CheckPinter( void *pointer )
{
    // perform check 1 -- maybe check that it's not NULL
...
}
release版的程式碼,必須追求效率,可以用下面的程式碼取代上面的程式碼
void CheckPinter( void *pointer )
{
    // no code
}


8.7 決定實際實執行所採用的防禦性程式設計程式碼
Determining How Much Defensive Programming to Leave in Production Code

防禦性程式設計的茅盾之處,就是我們希望在開發時突顯錯誤,在發行時隱藏錯誤。
所以,發行之後,還是要保持一些程式碼來對付錯誤,又要保持效率,當中的取捨就是本節的重點。

保留檢查重要錯誤的程式碼
移除檢查次要錯誤的程式碼
移除導致當機的程式碼(考慮是否影響生命)
保留有助於程式妥善當機的程式碼
記錄錯誤提供給技術人員
確保保留的錯誤訊息是友善的


8.8 防禦性程式設計的防禦狀態
Being Defensive About Defensive Programming

防禦性程式不可能沒有缺失,請考慮採取的防禦姿態,再安排設計的優先順序。

沒有留言:

張貼留言

(什麼是留言欄訊息?)