### Code Complete 2 - Chapter 19 一般控制問題
### 本章節內容
- 19.1 `布林運算式`
- 19.2 `複合述句 (述句區塊)`
- 19.3 `空述句`
- 19.4 `馴服危險的深層巢狀結構`
- 19.5 `程式設計基礎:結構化程式設計`
- 19.6 `控制結構與複雜度`
---
### 19.1 布林運算式
#### 用 true 和 false 做布林判斷
- 除了最簡單的、要求述句按順序執行的控制結構外,所有的控制結構都依賴於布林運算式的求值(evaluation)
#### 使用含糊的標記作為布林值
```vb=
Dim printerError As Integer
Dim reportSelected As Integer
Dim summarySelected As Integer
...
If printerError = 0 Then InitializePrinter()
If printerError = 1 Then NotifyUserOfError()
If reportSelected = 1 Then PrintReport()
If summarySelected = 1 Then PrintSummary()
If printerError = 0 Then CleanupPrinter()
```
----
#### 使用 true 和 false 代替數值來做判斷 (稍微好的做法)
```vb=
Dim printerError As Boolean
Dim reportSelected As ReportType
Dim summarySelected As Boolean
...
If (printerError = false) Then InitializePrinter()
If (printerError = true) Then NotifyUserOfError()
If (reportSelected = ReportType_First) Then PrintReport()
If (summarySelected = true) Then PrintSummary()
If (printerError = false) Then CleanupPrinter()
```
----
#### 隱式地判斷 true 和 false (更好的做法)
```vb=
Dim printerError As Boolean
Dim reportSelected As ReportType
Dim summarySelected As Boolean
...
If (Not printerError) Then InitializePrinter()
If (printerError) Then NotifyUserOfError()
If (reportSelected = ReportType_First) Then PrintReport()
If (summarySelected) Then PrintSummary()
If (Not printerError) Then CleanupPrinter()
```
----
#### 目前實務上會使用的寫法:
```csharp=!
public static async Task<bool> DoAsync(string parameter)
{
if (string.IsNullOrWhitespace(parameter) is false) // 檢查 null, 空字串, 空白符號
{
return false;
}
// 其他的程式碼
}
```
----
### 簡化複雜的運算式
1. 拆分複雜的判斷並引入新的布林變數
2. 把複雜的運算式做成布林函式
3. 用決策表代替複雜的條件
----
#### 複雜的判斷
```vb=
If ((document.AtEndOfStream) And (Not inputError) And _
(MIN_LINES <= lineCount) And (lineCount <= MAX_LINES) And _
(Not ErrorProcessing())) Then
'do something or other'
...
End If
```
----
#### 將複雜的判斷移入布林函式,用新的中間值使判斷更清新
```vb!
Function DocumentIsValid(_
ByRef documentToCheck As Document, _
lineCount As Integer, _
inputError As Boolean _
) As Boolean
Dim alllDataRead As Boolean
Dim legalLineCount As Boolean
allDataRead = (documentToCheck.AtEndOfStream) And (Not inputError)
legalLineCount = (MIN_LINES <= lineCount) And (lineCount <= MAX_LINES)
DocumentIsValid = allDataRead And legalLineCount And (Not ErrorProcessing())
End Function
```
※ 書中的範例假設 `ErrorProcessing()` 是一個表明目前處理狀態的布林函式
----
#### 重新閱讀簡化複雜判斷的程式碼的主流程
```vb=
If (DocumentIsValid(document, lineCount, inputError)) Then
'do something or other'
...
End If
```
----
#### 編寫肯定形式的布林運算式 (正向表述)
1. 在 if 條件句中,把判斷條件從否定轉換為肯定形式,並互換 if 和 else 子句中的程式碼
2. 用狄摩根定理簡化否定的布林判斷
```java=
if (!displayOK || !printerOK) // 否定型判斷
```
```java=
if (!(displayOK && printerOK)) // 應用狄摩根定理簡化後的判斷
```
----
狄摩根定理的邏輯運算式轉換法則
| 原運算式 | 等價運算式 |
| -------- | -------- |
| not A and not B | not (A or B) |
| not A and B | not (A or not B) |
| A and not B | not (not A or B) |
| A and B | not (not A or not B) |
| not A or not B* | not (A and B) |
| not A or B | not (A and not B) |
| A or not B | not (A and B) |
| A or B | not (not A and not B) |
----
#### 用括號使布林運算式更清晰
```java=
if (a < b == c < d) // 括號少的運算式
...
```
```java=
if ( (a < b) == (c < d) ) // 加了括號的運算式
...
```
- 用簡單的計數技巧驗證括號是否有對稱
```java=
if ( ( (a < b) == (c < d) ) && !done ) // 配對的括號
| | | | | | | |
0 1 2 3 2 3 2 1 0
```
```java=
if ( (a < b) == (c < d) ) && !done ) // 不配對的括號
| | | | | | |
0 1 2 1 2 1 0 -1
```
----
#### 理解布林運算式是如何求值的
```vb=
while ( i < MAX_ELEMENTS and item[i] <> 0 ) // 容易錯誤的判斷
```
```vb=
while ( i < MAX_ELEMENTS ) // 重新安排結構後的判斷語法
if ( item[i] <> 0 ) Then
...
```
#### 按照數線順序編寫數值運算式
```vb=
MIN_ELEMENTS <= i and i <= MAX_ELEMENTS
```
```
MIN_ELEMENTS MAX_ELEMENTS
↓ ↓
<----|----------------------|---->
↑
Valid values for i
```
----
#### 和 0 比較的 原則
```vb=
while ( !done ) // 隱式地比較邏輯變數
```
```vb=
while (balance != 0) // 把 數字 和 0 做比較
while ( balance ) // 這是較不好的用法
```
```C=
while (*charPtr != '\0') // 在 C 語言中 縣式地比較字元和零結束字元 ('\0')
while ( *charPtr )
```
```C=
while (bufferPtr != NULL) // 把指標與 NULL 做比較
while ( bufferPtr ) // 這是較不好的用法
```
----
#### 布林運算式常見問題
- 布林運算式在某些特定的程式語言中會有需要注意的地方
1.在 C 家族語言中,應該把常數放在 **比較** 的 左端 (假設 誤用 =,不是 ==)
```cplus=
if ( MIN_ELEMENTS = i ) // 編譯器會抓到這個錯誤
```
```cplus=
if ( i = MIN_ELEMENTS ) // 編譯器可能不會抓到這個錯誤
```
----
2.在 C++ 中,可以考慮建議前置處理器巨集來取代 &&、||、== (不得已才這麼做)
3.在 Java 中,應理解 `a == b` 和 `a.equals(b)` 之間的差異
- 一般來說,Java 應用程式裡應該使用 `a.equals(b)`
---
### 19.2 複合述句 (述句區塊)
- 先寫出 區塊的 **開始** 和 **結束**,再完成中間的部分; 用括號把條件表達清楚
```
step1:
```
```csharp=
for ( i = 0 ; i < maxlines; i++)
```
step2:
```csharp=
for ( i = 0 ; i < maxlines; i++) {}
```
step3:
```csharp=
for ( i = 0 ; i < maxlines; i++) {
// 編寫 程式碼
}
```
---
### 19.3 空述句
- 範例以 C++ 的空述句為主
```cplusplus!
while ( recordArray.Read( index++ ) != recordArray.EmptyRecord() )
; // 可以替換為 {} 或是 {;}。 或是用 { DoNothing(); }
```
- C++ 中的 while 迴圈後面必須跟著一個述句,但也可是空述句。
1.小心使用 空述句
2.為空述句建立一個前置處理器 DoNothing() 巨集或行內函式
3.考慮如果換成使用一個非空的迴圈體,是否可讓程式碼更清晰(可讀性更高)
---
### 19.4 馴服危險的深層巢狀結構
- 很多研究人員建議避免使用超過 3-4 層的 if 巢狀結構
```cplus=
if ( inputStatus == InputStatus_Success ) {
// lost of code
...
if ( printerRoutine != NULL ) {
// lots of code
...
if ( SetupPage ) {
// lots of code
...
if ( AllocMem( &printData) ) {
// lots of code
...
}
}
}
}
```
----
#### 透過重覆檢測條件中的某一部分
#### 來簡化巢狀結構的 if 述句
```cplus=
if (inputStatus == InputStatus_Success){
// lots of code
...
if (printerRoutine != NULL){
// lots of code
...
}
}
if ((inputStatus == InputStatus_Success) &&
(printerRoutine != NULL) && SetupPage()){
// lots of code
...
if (AllocMem(&printData)){
// lots of code
...
}
}
```
----
#### 用 break 區塊來簡化巢狀結構 if
- 此技巧不常見,所以只有在整個團隊都很熟悉這種技巧,並且團隊可接受專案中可實踐,才使用
```cplus=
do{
if (inputStatus != InputStatus_Success){
break;
}
// lots of code
...
if (printerRoutine == NULL) {
break;
}
// lots of code
...
if (!SetupPage()){
break;
}
// lots of code
...
if (!AllocMem(&printData)){
break;
}
// lots of code
...
} while (FALSE); // end break block
```
----
#### 把巢狀結構 if 轉換成一組 if-then-else 述句
```java=
if (10 < quantity){ // 茂盛的決策樹
if (100 < quantity){
if (1000 < quantity){
discount = 0.10;
}
else{
discount = 0.05;
}
}
else{
discount = 0.025;
}
}
else{
discount = 0.0;
}
```
----
```java=
if (1000 < quantity){ // 將巢狀結構 if 轉換成一組 if-then-else 述句
discount = 0.10;
}
else if (100 < quantity){
discount = 0.05;
}
else if (10 < quantity){
discount = 0.025;
}
else{
discount = 0;
}
```
----
- 可以再進一步修改,閱讀性更好的寫法
```java=
if (1000 < quantity){
discount = 0.10;
}
else if ((100 < quantity) && (quantity <= 1000)){
discount = 0.05;
}
else if ((10 < quantity) && (quantity <= 100)){
discount = 0.025;
}
else if (quantity <= 10){
discount = 0;
}
```
----
#### 把巢狀結構 if 轉換成 case 述句
```vb=
Select Case quantity
Case 0 To 10
discount = 0.0
Case 11 To 100
discount = 0.025
Case 101 To 1000
discount = 0.05
Case Else
discount = 0.10
End Select
```
----
#### 把深層巢狀結構的程式碼抽離出來放進單獨的子程式
- 需要抽離出子程式的巢狀結構
```cplus=
while (!TransactionsComplete()){
// read transaction record
transaction = ReadTransction();
// process transaction depending on type of transaction
if (transaction.Type == TransactionType_Deposit){
// process a deposit
if (transaction.AccountType == AccountType_Checking){
if (transaction.AccountSubType == AccountSubType_Business)
MakeBusinessCheckDep(taansaction.AccountNum, transaction.Amount);
else if (transaction.AccountSubType == AccountSubType_Personal)
MakePersonalCheckDep(transaction.AccountNum, transaction.Amount);
else if (transaction.AccountSubType == AccountSubType_School)
MakeSchoolCheckDep(transaction.AccountNum, transaction.Amount);
}
else if (transaction.AccountType == AccountType_Savings)
MakeSavingsDep(transaction.AccountNum, transaction.Amount);
else if (transaction.AccountType == AccountType_DebitCard)
MakeDebitCardDep(transaction.AccountNum, transaction.Amount);
else if (transaction.AccountType == AccountType_MoneyMarket)
MakeMoneyMarketDep(transaction.AccountNum, transaction.Amount);
else if (transaction.AccountType == AccountType_Cd)
MakeCdDep(transaction.AccountNum, transaction.Amount);
}
else if (transaction.Type == TransactionType_Withdrawal){
// process a withdrawal
if (transaction.AccountType == AccountType_Checking)
MakeCheckingWithdrawal(transaction.AccountNum, transaction.Amount);
else if (transaction.AccountType == AccountType_Savings)
MakeSavingsWithdrawal(transaction.AccountNum, transaction.Amount);
else if (transaction.AccountType == AccountType_DebitCard)
MakeDebitCardWithdrawal(transaction.AccountNum, transaction.Amount);
}
else if (transaction.Type == TransactionType_Transfer){
MakeFundsTransfer(
transaction.SourceAccountType,
transaction.TargetAccountType,
transaction.AccountNum,
transaction.Amount
);
}
else{
// process unknown kind of transaction
LogTransactionError("Unknown Transaction Type", transaction);
}
}
```
----
- 將巢狀結構分解到子程式後
```cplus=
while (!TransactionsComplete()){
// read transaction record
transaction = ReadTransaction();
// process transaction depending on type of transaction
if (transaction.Type == TransactionType_Deposit){
ProcessDeposit(
transaction.AccountType,
transaction.AccountSubType,
transaction.AccountNum,
transaction.Amount
);
}
else if (transaction.Type == TransactionType_Wihdrawal){
ProcessWithdrawal(
transaction.AccountType,
transaction.AccountNum,
transaction.Amount
);
}
else if (transaction.Type == TransactionType_Transfer){
MakeFundsTransfer(
transaction.SourceAccountType,
transaction.TargetAccountType,
transaction.AccountNum,
transaction.Amount
);
}
else{
// process unknown transaction type
LogTransactionError("Unknown Transaction Type", transaction);
}
}
```
----
#### 重新設計深層巢狀結構的程式碼
- 分解後的巢狀程式碼,使用 case 述句
```cplus=
while (!TransactionsComplete()){
// read transaction record
transaction = ReadTransaction();
// process transaction depending on type of transaction
switch (transaction.Type){
case (TransactionType_Deposit):
ProcessDeposit(
transaction.AccountType,
transaction.AccountSubType,
transaction.AccountNum,
transaction.Amount
);
break;
case (TransactionType_Wihdrawal):
ProcessWithdrawal(
transaction.AccountType,
transaction.AccountNum,
transaction.Amount
);
break;
case (TransactionType_Transfer):
MakeFundsTransfer(
transaction.SourceAccountType,
transaction.TargetAccountType,
transaction.AccountNum,
transaction.Amount
);
break;
default:
// process unknown transaction type
LogTransactionError("Unknown Transaction Type", transaction);
break;
}
}
```
----
- 使用更 **物件導向** 的方法 (使用多型機制的程式碼)
```cplus=
TransactionData transactionData;
Transaction *transaction;
while (!TransactionsComplete()){
// read transaction record
transaction = ReadTransaction();
// create transaction object, depending on type of transaction
switch (transactionData.Type){
case (TransactionType_Deposit):
transaction = new Deposit(transactionData);
break;
case (TransactionType_Wihdrawal):
transaction = new Wihdrawal(transactionData);
break;
case (TransactionType_Transfer):
transaction = new Transfer(transactionData);
break;
default:
// process unknown transaction type
LogTransactionError("Unknown Transaction Type", transaction);
return;
}
transaction->Complete();
delete transaction;
}
```
----
- 使用 多型機制 + 程式設計模型的 工廠模式 (Factory Design Pattern)
```cplus=
TransactionData transactionData;
Transaction *transaction;
while (!TransactionsComplete()){
// read transaction record and complete transaction
transactionData = ReadTransaction();
transaction = TransactionFactory.Create(transactionData);
transaction->Complete();
delete transaction;
}
```
----
- Object Factory 的程式碼架構 (可以看作是對 Switch 述句 的簡單改寫)
```cplus=
Transaction *TransactionFactory::Create(
TransactionData transactionData
){
// create transaction object, depending on type of transaction
switch (transactionData.Type){
case (TransactionType_Deposit):
return new Deposit(transactionData);
break;
case (TransactionType_Wihdrawal):
return new Wihdrawal(transactionData);
break;
case (TransactionType_Transfer):
return new Transfer(transactionData);
break;
default:
// process unknown transaction type
LogTransactionError("Unknown Transaction Type", transaction);
return;
}
}
```
---
### 19.5 程式設計基礎:結構化程式設計 (structured design)
- 核心概念:只採用 單一入口、單一出口 (單入單出) 的控制結構
- 在使用 break、continue、throw、catch、return,考慮其他問題時都需要有結構化程式設計的概念
- 主要 3 個組成部分:
- 序列:按照先後順序執行
- 選擇:舉例,常見的 if-then-else 述句
- 迭代:迴圈
---
### 19.6 控制結構與複雜度
- 控制結構用得不好就會增加複雜度,反之則能降低複雜度
- 就直覺而言,程式的複雜度在很大程度上決定了理解程式所需要花費的精力
- 複雜度的重要性
- 與控制流有關的複雜度非常重要,因為它與 `不可靠的程式碼` 和 `頻繁出現的錯誤` 是正相關
----
- 如何度量 複雜度
| 計算子程式中決策點數量 |
| -------- |
| 1. 從 1 開始,用一直往下的路徑閱讀程式 |
| 2. 一旦遇到 if、while、repeat、for、and、or 或同類的詞,就加1 |
| 3. 給 case 述句中的每種期況都加1 |
----
example:
```csharp=
if (
((status = Success) and done)
or
(not done and (numLines >= maxLines))
) then ... // 複雜度是 5
```
----
複雜度的度量結果
| 根據 | 複雜度的度量結果採取的措施 |
| -------- | -------- |
| 0 - 5 | 子程式可能還不錯 |
| 6 - 10 | 需要簡化子程式 |
| 10+ | 需要將子程式中的某部分抽離成另外的子程式並呼叫它 |
---
### 小結論
- 使布林運算式的可讀性提高,可有效提高程式碼的品質
- 多層次的巢狀結構,應該要能夠避免
- 結構化程式設計的概念
- 程式複雜度降低
{"title":"Code Complete 2 - Chapter 19 一般控制問題","description":"Code Complete 2 - Chapter 15 使用條件述句","contributors":"[{\"id\":\"ebb150bd-e3e1-40d8-8710-881a5d563b00\",\"add\":15727,\"del\":403}]"}