# 重構 - Chapter 10 簡化條件邏輯 [TOC] ## 1. 分解條件邏輯 (Decompose Conditional) **動機:** 凸顯意圖、降低閱讀複雜度 **作法:** 將分支中的條件邏輯抽成 函式 (Extract Function) From ```javascript! if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd)) charge = quantity * plan.summerRate; else charge = quantity * plan.regularRate + plan.regularServiceCharge; ``` To ```javascript! if (summer()) charge = summerCharge(); else charge = regularCharge(); ``` To ```javascript! const charge = summer() ? summerCharge() : regularCharge() ``` ## 2. 合併條件式(Consolidate Conditional Expression) **動機:** 當檢查不同、底下卻做相同的事時,代表 1. 檢查可結合、 2. 可抽出函式 (Extract Function)凸顯意圖 (封裝、隱藏細節,從說 how 變成說 why) **作法:** 1. 確保條件式中沒 Side Effect (否則請先執行 Separate Query from Modifier) 2. 將兩個條件式中,用邏輯運算子結合 (or -> 依序、 and -> 巢狀) 3. 測試 4. 將結合完的這個條件式與下個條件結合,直到只剩一個條件式 5. 考慮將此條件式抽成 Function From ```javascript! function disabilityAmount (anEmployee) { if (anEmployee.seniority < 2) return 0; if (anEmployee .monthsDisabled > 12) return 0; if (anEmployee.isPartTime) return 0; // 計算 disability amount ... } ``` To ```javascript! function disabilityAmount (anEmployee) { if (isNotEligableForDisability()) return 0; //計算 disability anount ... function isNotEligableForDisability() { return ((anEmployee.seniority < 2) || (anEmployee.monthsDisabled > 12) || (anEmployee.isPartTime)); } } ``` From ```javascript! if(anEmployee.onVacation) if (anEmployee.seniority > 10) return 1; return 0.5; ``` To ```javascript! if ((anEmployee.onVacation) && (anEmployee.seniority > 10)) return 1; return 0.5; ``` ## 3. 將巢狀條件式換成防衛敘句(Replace Nested Conditional with Guard Clauses) **動機:** 使入口和出口更“清晰”! 防衛敘句能指出某情況非功能“核心”,發生了就採取行動離開。有意識到思考你的條件分支是屬於正常 or 異常,(2組都正常,用 if/else <= 權重相同;有一組為異常 <= 提早離開) **作法:** 1. 先將最外層需要替換的條件轉為 Guard Clauses 2. 測試 3. 重複該行為 4. 最後如果 Guard Clauses 都回傳相同結果,可使用二:合併條件式 From ```javascript! function getPayAmount () { let result; if (isDead) result = deadAmount(); else { if (isSeparated) result = separatedAmount(); else { if (isRetired) result = retiredAmount(); else result = normalayAmount(); } } return result; } ``` To ```javascript! function getPayAmount () { if (isDead) return deadAmount() ; if (isSeparated) return separatedAmount(); if (isRetired) return retiredAmount(); return normalPayAmount (); } ``` From ```javascript!= function adjustedCapital (anInstrument) { let result = 0; if (anInstrument.capital > 0) { if (anInstrument.interestRate > 0 && anInstrument.duration > 0){ result = (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor; } } return result; } // 條件反轉、簡化過程 + if (anInstrument.capital <= 0) return result + if (!(anInstrument.interestRate > 0 && anInstrument.duration > 0)) return result + if (anInstrument.interestRate <= 0 || anInstrument.duration <= 0) return result + 合併 12,14 行 + 移除多餘的變數 result ``` 註解:狄摩根定律 `(!a) && (!b) === !(a || b)` 和 `(!a) || (!b) === !(a && b)` ![](https://pic2.zhimg.com/50/v2-88bfea40e6a7dbc88b52dfb13d097062_720w.jpg?source=1940ef5c) To ```javascript! function adjustedCapital (anInstrument) { if ( anInstrument.capital <= 0 || anInstrument.interestRate <= 0 || anInstrument.duration<= 0 ) return 0; return (anInstrument.income / anInstrument .duration) * anInstrument .adjustmentFactor ``` ## 4. ~~將條件式換成多型(Replace Conditional with Polymorphism)~~ ## 5. ~~加入特例(Introduce Special Case)~~ ## 6. 加入斷言(Introduce Assertion) **動機:** 凸顯意圖、寶貴的溝通工具、除錯的好幫手 **作法:** 當你見到一個條件為真的狀況時,加入斷言來說明此事 From ```javascript! if(this.discountRate) base = base - (this.discountRate * base) ``` To ```javascript! assert(this.discountRate >= 0) // 斷言失敗代表程式碼有錯 if(this.discountRate) base = base - (this.discountRate * base) // assert 大致會長得像下面這樣,nodejs 有提供 API function assert(condition, message) { if (!condition) { throw new Error(message || "Assertion failed"); } } ``` ## Discucsion 1. 盡量使用正向表述,人腦比較好理解 ```javascript! disabled = !isEligible || !isActive disabled = !(isEligible && isActive) enabled = isEligible && isActive ``` 2. 在單元測試、TypeScript 型別斷言都還滿常見 assert 的概念 ```javascript! expect(add(5, 5)).toBe(10); ``` ```typescript let str: unknown = "geeksforgeeks"; let len: number = (str as string).length; ``` 3. IIFE 變數,來替代巢狀三元運算式 ```javascript const content = (() => { switch (status) { case 'loading': return <LoadingBar />; case 'loaded': return <Table />; default: return null; } })(); return ( <Container> {content} </Container> ``` 4. 用 switch case true 來做多重判斷 https://seanbarry.dev/posts/switch-true-pattern ```javascript const user = { firstName: "Seán", lastName: "Barry", email: "my.address@email.com", number: "00447123456789", }; switch (true) { case !user: throw new Error("User must be defined."); case !user.firstName: throw new Error("User's first name must be defined"); case typeof user.firstName !== "string": throw new Error("User's first name must be a string"); // ...lots more validation here default: return user; } ``` 5. 用 object literal 取代 switch case https://ultimatecourses.com/blog/deprecating-the-switch-statement-for-object-literals ```javascript! // from var drinks; switch(type) { case 'coke': drink = 'Coke'; break; case 'pepsi': drink = 'Pepsi'; break; default: drink = 'Unknown drink!'; } // to function getDrink (type) { var drinks = { 'coke': 'Coke', 'pepsi': 'Pepsi', 'lemonade': 'Lemonade', 'default': 'Default item' }; return 'The drink I chose was ' + (drinks[type] || drinks['default']); } ``` ## 參考資料 1. 重構:改善既有程式的設計 1. [Code の 斷捨離 — 重構 (Refactoring)-ch9](https://medium.com/@duidae/code-%E3%81%AE-%E6%96%B7%E6%8D%A8%E9%9B%A2-%E9%87%8D%E6%A7%8B-refactoring-ch9-6768dae8a6ba) 2. [What is “assert” in JavaScript?](https://stackoverflow.com/questions/15313418/what-is-assert-in-javascript#:~:text=There%20is%20no%20assert%20in%20JavaScript.%20However%2C%20there,line%20argument%20-enableassertions%20%28or%20its%20shorthand%20-ea%20%29%2C)