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

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

使用版本工具,並建立antmake等工具
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

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

沒有留言:

張貼留言

(什麼是留言欄訊息?)