第八章 防禦性程式設計
Defensive Programming
開車有防衛駕駛,寫程式有防禦性程式設計
8.1 保護程式在無效輸入資料的破壞
Protecting Your Program from Invalid Inputs
優秀的程式,無輪輸入什麼內容,都不會輸出垃圾。
a. 檢查所有來自外部的常式的參數
b. 檢查所有來自其它常式的常式的參數→8.5
c. 決定如何處理錯誤的輸入→5.3
最優秀的防禦性程式撰寫
- 重複的設計
- 程式碼前先寫虛擬碼
- 程式碼前先寫測試案例
- 進行低層級設計視察
8.2 判斷提示
Assertions
assert(常式); //若常式為false,則程式崩潰、印出訊息。
用來檢查假設
- 是否滿足定義域和值域
- 檔案或資料流狀態是否開啟(或關閉)
- 檔案或資料流讀寫位置是否在開頭(或結尾)
- 檢查檔案或資料流權限(唯讀、唯寫、可讀寫)。
- 只能輸入的變數,經過一個常式是否有被改變
- 指標是否為null
- 陣列項目數量是否為特定數目
- 初始化的表格,變數是否都初始化了。
- 在常式內的容器,是否為空(或滿)
- 最佳化前後的常式,結果是否一致
- ....(更多更多的假設)
建立自己的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()來驗證前置條件和後置條件檢查輸入 - 前置條件
檢查輸出 - 後置條件
錯誤:
- 可靠的內部來源,檢查定義域外的值←用Assert(定義域)
- 外部來源,檢查錯誤格式←錯誤處理的程式碼
常式通常會用
- Assert()
- 處理錯誤的程式碼
外部輸入 無法預測,也建議使用。
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
「處理錯誤」和「反映錯誤」的副程式(或物件)區分清楚。
低層級的程式碼:反映錯誤
高層級的程式碼:處理錯誤
通知系統錯誤的(東西),會下列其中一種
- 設定狀態變數的值
- 將狀態回傳,作為常式的回傳值
- 用程式語言內嵌的例外處理
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傳播錯誤
- 錯誤記錄到檔案中
- 關閉系統
- 其它
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
遇到了十次相同的問題,第十一次之後受不了了,寫了一個除錯輔助工具之後,再也不會遇到這個問題,心情愉快、工作順利,程式碼都乖乖的呢。為了節省時間,保持開發過程流暢,不影響心情。請儘早使用。
通常會有三種情況,會取得除錯輔助工具
- 遇到了好幾次相同的問題。
- 初次寫除錯輔助程式
- 使用之前專案的除錯輔助工具
使用自殺式程式設計(原意:攻擊式程式設計)
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
防禦性程式不可能沒有缺失,請考慮採取的防禦姿態,再安排設計的優先順序。
沒有留言:
張貼留言
(什麼是留言欄訊息?)