### 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}]"}
    56 views