### Code Complete 2 - Chapter 8 防禦性程式設計
### 本章節內容
- 8.1 `保護程式免遭非法輸入資料的破壞`
- 8.2 `斷言(assetion)`
- 8.3 `錯誤處理技術`
- 8.4 `例外`
- 8.5 `隔離程式,包容由錯誤造成的損害`
- 8.6 `輔助除錯的程式碼`
- 8.7 `確定在產品程式碼中該保留多少防禦性程式碼`
- 8.8 `對防禦性程式設計採取防禦的姿態`
---
### 8.1 保護程式免遭非法輸入資料的破壞
<div style="text-align: left;">
<span style="font-size:22px;">
- 概念類似「防衛性駕駛」,你駕駛自己的車子,但是你不能預期別的駕駛人不會違規<br>
- 為了提升自己的駕駛時候的安全,要有「防衛性駕駛」的意識,減低交通意外發生的機率<br>
- 不要在程式碼中「引入錯誤」</span>
</div>
----
#### 主要思維
<div style="text-align: left;">
<span style="font-size:22px;">
- 子程式應該不因傳入錯誤資料而被破壞,就算是由其他的子程式產生的錯誤資料<br>
- 廣義的主要思維:「承認程式都會有問題,都需要被修改」
</span>
</div>
----
#### 檢查來自外部所有的資料值
<div style="text-align: left;">
<span style="font-size:22px;">
- 數值 => 必須確保它在可接受取值範圍內<br>
- 字串 => 確保它不會超過長度<br>
- 如果字串代表是某個特定範圍的資要(ex:金融交易ID、其他類似資料),就必須確保它代表的值是合法的,否則就拒絕接受。
</span>
</div>
----
#### 檢查子程式所有輸入參數的值
<div style="text-align: left;">
<span style="font-size:18px;">
- 主要思維和 「檢查來自外部所有的資料值」是相同的,只是傳入的資料來源是其他子程式,非外部的介面。<br>
- 8.5 節 `隔離程式,包容由錯誤造成的損害` 有提到一種實用方法,可以確定那些子程式需要檢查輸入資料
</span>
</div>
----
#### 決定如何處理錯誤的輸入資料
<div style="text-align: left;">
<span style="font-size:22px;">
- 一旦檢測到非法的參數,要如何處理它。<br>
- 8.3 節 `處理錯誤技術` 有說明相關的處理方式。
</span>
</div>
---
### 8.2 斷言 (assertion)
<div style="text-align: left;">
<span style="font-size:22px;">
- assertion 是指在開發期間,讓程式在運行時進行自我檢查的程式碼(通常是一個子程式或 macro)<br>
- 判斷 asertion 的狀態:<br>
- ture => 代表程式運行正常<br>
- false => 代表在程式碼中發現非預期的錯誤
</span>
</div>
----
### 常見的斷言架構
<div style="text-align: left;">
<span style="font-size:20px;">
1. 通常有 2 個參數<br>
2. 一個是布林運算式,用來描述假設為真時的情況,另一個則是代表當斷言為假時要顯示的訊息
</span>
</div>
```java!=
assert denominator != 0 : "denominator is unexpectedly equal to 0.";
```
----
#### 使用斷言紀錄程式碼中作出的各種假設,並清除不希望出現的情況
<div style="text-align: left;">
<span style="font-size:18px;">
- 輸入參數或輸出參數的值位於預期的範圍內<br>
- 子程式開始(或結束)執行時,檔案或資料流是處於打開(或關閉)的狀態<br>
- 子程式開始(或結束)執行時,檔案或資料流的讀寫位置處於開頭(或結尾)處<br>
- 檔案或資料流已指定採用唯讀,唯寫或可讀可寫的方式打開<br>
- 只用來作為輸入的變數值,沒有被子程式修改<br>
- 指標非空<br>
- 傳入子程式的陣列或其他容器至少能容納 X 個資料元素<br>
- 表格己被初始化,儲存著真實的數值<br>
- 子程式開始(或結束)執行時,某個容器是空的(或滿的)<br>
- 一個經過高度最佳化的複雜子程式的運算結果,和相對緩慢但程式碼清晰的子程式的運算結果,是一致的
</span>
</div>
----
#### 建立自己的斷言機制
```cplus=!
#define ASSERT(conditon, message){
if (!(condition)){
LogError( "Asssetion failed:",
#condition, message );
exit( EXIT_FAILURE);
}
}
```
----
#### 使用斷言的指引
<div style="text-align: left;">
<span style="font-size:20px;">
- 用錯誤處理程式碼來處理預期會發生的情況,用斷言來處理絕不該發生的情況<br>
- 避免把需要執行的程式碼放到斷言中
</span>
</div>
<div style="text-align: left;">
<span style="font-size:20px;">不正確用法:</span></div>
```vb=!
Debug.Assert(PerformAction()) 'Couldn't perfrom action
```
<div style="text-align: left;">
<span style="font-size:20px;">正確用法:</span></div>
```vb=!
actionPerformed = PerformAction();
Debug.Assert(actionPerformed) ' Couldn't perform action
```
----
#### 用斷言來註解並驗證前置條件和後置條件
```vb=!
Private Function Velocity ( _
ByVal latitude As Single, _
ByVal longitude As Single, _
ByVal elevation As Single, _
) As Single
' Preconditions
Debug.Assert(-90 <= latitude And latitude <= 90)
Debug.Assert(0 <= longitude And longitude < 360)
Debug.Assert(-500 <= elevation And elevation <= 75000)
...
' Postconditons
Debug.Assert(0 <= returnVelocity And returnVelocity <= 600)
' return value
Velocity = returnVelocity
End Function
```
----
#### 對於高健全性的程式碼,應該先使用斷言,然後再處裡錯誤
```vb=!
Private Function Velocity ( _
ByVal latitude As Single, _
ByVal longitude As Single, _
ByVal elevation As Single, _
) As Single
' Preconditions
Debug.Assert(-90 <= latitude And latitude <= 90)
Debug.Assert(0 <= longitude And longitude < 360)
Debug.Assert(-500 <= elevation And elevation <= 75000)
// 處理錯誤
If (latitude < -90) Then
latitude = -90
ElseIf (latitude > 90) Then
latitude = 90
End if
If (longitude < 0) Then
longitude = 0
ElseIf (longutude > 360) Then
longitude = 360
...
End Function
```
---
### 8.3 錯誤處理技術
<div style="text-align: left;">
<span style="font-size:20px;">
- 返回中立值<br>
- 換用下一個正確的資料<br>
- 返回與前次相同的資料<br>
- 換用最接近的合法值<br>
- 把警告訊息記錄到日誌當中<br>
- 返回一個錯誤碼<br>
- 呼叫錯誤處理子程式或物件<br>
- 當錯誤發生時顯示錯誤訊息<br>
- 用最妥當的方式在局部處理錯誤<br>
- 關閉程式
</span>
</div>
----
#### 健全性 與 正確性
<div style="text-align: left;">
<span style="font-size:22px;">
- 錯誤處理方式有時偏向正確性(correctness)<br>
- 錯誤處理方式有時偏向健全性(rubustness)
</span>
</div>
---
### 8.4 例外(Exceptions)
<div style="text-align: left;">
<span style="font-size:14px;">
- 用例外通知程式的其他部分,發生了不可忽略的錯誤<br>
- 只有在真正例外的情況下,才拋出例外<br>
- 不能用例外來推卸責任<br>
- 避免在建構涵式和解構函式中拋出例外,除非你在同一個地方將它們捕捉<br>
- 在恰當的抽象層次拋出例外
</span>
</div>
<div style="text-align: left;">
<span style="font-size:16px;">拋出抽象層次不一致的例外的類別</span>
</div>
<div style="text-align: left; font-size: 14px;">
<pre><code>
class Employee {
...
public TaxId GetTaxId() throws EOFException {
...
}
...
}
</code></pre>
</div>
<div style="text-align: left;">
<span style="font-size:16px;">在一致的抽象層次上拋出例外的類別</span>
</div>
<div style="text-align: left; font-size: 14px;">
<pre><code>
class Employee {
...
public TaxId GetTaxId() throws EmployeeDataNotAvailable {
...
}
...
}
</code></pre>
</div>
----
<div style="text-align: left;">
<span style="font-size:18px;">
- 在例外訊息中加入關於導致例外發生的全部資訊<br>
- 避免使用空的 catch 述句 (下方以 Java 舉例)
</span>
</div>
<div style="text-align: left; font-size: 20px;">
<pre><code>
try {
...
// 實作的多行程式碼
...
}catch (AnException exception) {
}
</code></pre>
</div>
<div style="text-align: left;">
<span style="font-size:18px;">- Java 忽略例外的正確做法</span>
</div>
<div style="text-align: left; font-size: 20px;">
<pre><code>
try {
...
// 實作的多行程式碼
...
}catch (AnException exception) {
LogError("Unexpected exception");
}
</code></pre>
</div>
----
<div style="text-align: left;">
<span style="font-size:18px;">
- 了解所用函式庫可能拋出的例外<br>
- 考慮建立一個集中的例外報告機制
</span>
</div>
<div style="text-align: left; font-size: 20px;">
Sub ReportException( _
ByVal className, _
ByVal thisException As Exception _
)
Dim message As String
Dim caption As String
message = _
"Exception:" & thisException.Message & "." & ControlChars.Crlf & _
"Class:" & className & ControlChars.Crlf & _
"Routine:" & thisException.TargetSite.Name & ControlChars.Crlf
caption = "Exception"
MessageBox.Show(message, caption, MessageBoxButtons.OK, _
MessageBoxIcon.Exclamation)
End Sub
</div>
<div style="text-align: left; font-size: 20px;">
Try
...
Catch exceptionObject As Exception
ReportException(CLASS_NAME, exceptionObject)
End Try
</div>
----
<div style="text-align: left;">
<span style="font-size:22px;">
- 把專案對例外的使用標準化<br>
- 考慮例外的替代方案
</span>
</div>
---
### 8.5 隔離程式,包容由錯誤造成的損害
<div style="text-align: left;">
<span style="font-size:22px;">
- 隔欄(barricade) 是一種容損策略(damage-containment strategy)<br>
- 在輸入資料時將其轉換為恰當的類型<br>
- 有點像 middleware<br>
- 對傳入的資料做 Validation
</span>
</div>
---
### 8.6 輔助除錯的程式碼
<div style="text-align: left;">
<span style="font-size:22px;">
- 不要自動地把產品版的限制強加在開發版上<br>
- 儘早引入輔助除錯的程式碼 (我自己是理解為寫測試)
</span>
</div>
----
#### 採用進攻式程式設計
<div style="text-align: left;">
<span style="font-size:22px;">
- 確保斷言述句會使程式終止運行,問題需要被解決,不是被跳過<br>
- 完全填充分配到的所有記憶體,可以讓工程師檢測到記憶體配置錯誤<br>
- 完全填充已分配到的所有檔案和資料流,可以排除檔案格式的錯誤<br>
- 確保每一個 case 述句中的 default 分支或 else 分支都能產生嚴重錯誤(ex:終止程式運行),或至少讓這些錯誤不被忽視<br>
- 在刪除一個物件前,把它填滿垃圾資料
- 讓程式把它的錯誤日誌用 email 通知(目前工作使用 exceptionless)
</span>
</div>
----
#### 計畫移除輔助除錯的程式碼
<div style="text-align: left;">
<span style="font-size:22px;">
- 使用類似像 ant 和 make 的版本工具和 make 工具<br>
- 使用內建的前置處理器<br>
- 編寫自己的前置處理器<br>
- 使用除錯木樁(debuging stubs)<br>
</span>
</div>
---
### 8.7 確定在產品程式碼中該保留多少防禦性程式碼
<div style="text-align: left;">
<span style="font-size:22px;">
- 保留那些檢查重要錯誤的程式碼<br>
- 去掉檢查細微錯誤的程式碼<br>
- 去除會導致程式硬性崩潰的程式碼<br>
- 保留可以讓程式優雅崩潰的程式碼<br>
- 為你的技術支援人員記錄下錯誤訊息<br>
- 確認留在程式碼中的錯誤訊息是友善的
</span>
</div>
---
### 8.8 對防禦性程式設計採取防禦的姿態
<div style="text-align: left;">
<span style="font-size:20px;">
- 過度的防禦性程式設計也會引起問題,如果在每一個能想到的地方都用每一種能想到的方法來檢查由參數傳入的資料<br>
- 且會增加程式複雜度<br>
- 要執行防禦性程式設計的地方,需要考慮過再執行,且需要因地制宜地調整防禦性程式設計的優先級別
</div>
{"title":"Code Complete 2 - Chapter 8 防禦性程式設計","description":"19.1 布林運算式","contributors":"[{\"id\":\"ebb150bd-e3e1-40d8-8710-881a5d563b00\",\"add\":24803,\"del\":15593}]"}