# Ch11 Refactoring APIs (重構API) ## Separate Query from Modifier (將查詢函式與修改函式分離) **Motivation** 1. What is most valueable function? > No side effect **Before** ```javascript function getTotalOutstandingAndSendBill() { const result = customer.invoices.reduce((total, each) => each.mount + total, 0); return () => sendBill(); } ``` **After** ```javascript function getTotalOutstandingAndSendBill() { const result = customer.invoices.reduce((total, each) => each.mount + total, 0); sendBill(); return result; } function totalOutstanding() { return customer.invoices.reduce((total, each) => each.amount + total, 0); } function sendBill() { emailGateway.send(formatBill(customer)); } ``` **Example** ```javascript function alertForMiscreant(people) { for (const p of people) { if (p === 'Don') { setOffAlarms(); return 'Don'; } if (p === 'John') { setOffAlarms(); return 'John' } return ''; } } ``` ```javascript function findMiscreant(people) { for (const p of people) { if (p === 'Don') return 'Don'; if (p === 'John') return 'John'; return ''; } } function alertForMiscreant(people) { if (findMiscreant(people) !== '') setOffAlarms(); } ``` --- ## Parameterize Function (將函式參數化) ```javascript function tenPercentRaise(aPerson) { aPerson.salary = aPerson.salary.multiply(1.1); } function raise(aPerson) { aPerson.salary = aPerson.salary.multiply(1.05); } ``` ```javascript function raise() { aPerson.salary = aPerson.salary.multiply(1 + factor); } ``` **Motivation** > 多個函式擁有相似的邏輯 > 1. 擁有相似的邏輯,但使用不同的常值 **How to do that on complexity structure** > 1. 有範圍的程式,從中間開始切開並進行重構。 ```javascript function baseCharge(usage) { if (usage < 0) return usd(0); const amount = bottomBand(usage) * 0.03; + middleBand(usage) * 0.05 + topBand(usage) * 0.07; return usd(amount); } function bottomBand(usage) { return Math.min(usage, 100); } function middleBand(usage) { return usage > 100 ? Math.min(usage, 200) - 100 : 0 } function topBand(usage) { return usage > 200 ? usage - 200 : 0; } ``` ```javascript function withinBand(usage, bottom, top) { return usage > bottom ? Math.min(usage, top) - bottom : 0; } function baseCharge(usage) { if (usage < 0) return usd(0); const amount = withinBand(usage, 0, 100) * 0.03 + withinBand(usage, 100, 200) * 0.05 + withinBand(usage, 200, Infinity) * 0.07; return usd(amount); } ``` --- ## Remove Flag Argument (移除旗標引數) **Motivation** 大家在以前寫程式時,一定會有個if...else..來當拆開邏輯,例如if (isXXX),但這個對於caller而言,他不知道parameters是什麼,都要進去函式去逐一查每個arguments代表的是什麼意義。 ```javascript function bookConcert(aCustomer, isPremium) { if (isPremium) { // ....do premium things } else { // ...normal things } } ``` **Motivtion** 1. 大家可以看到下面這些call function意義不明,it's not readable。 bookConcert(aCustomer, true); bookConcert(aCustomer, CustomerType.PREMIUM); bookConcert(aCustomer, 'premium'); > 在主程式已經將branch切分的很清楚的情況下,我們可以將被呼叫的程式中的flag上移到上一層。 --- ## Preserve Whole Object (保留整個物件) **Motivation** 1. 如果未來function需要從arguments取得更多的資料,勢必需要取得更多參數內容,進而導致需要更改傳進去的parameters -> 這有臭臭的感覺。 ```javascript // caller const low = aRoom.daysTempRange.low; const high = aRoom.daysTempRange.high; if (!aPlan.withinRange(low, high)) { alerts.push('room temperature went outside range'); } class HeatingPlan { constructor() { this._temperatureRange = { low: 100, high: 200, } } withinRange(bottom, top) { return (bottom >= this.temperatureRange.low) && (top <= this._temperatureRange.high); } } ``` 解決方案 1. 過渡期 ```javascript // caller if (!aPlan.xxxNewWithinRange(aRoom.daysTempRange)) { alerts.push('room temperature went outside range'); } class HeatingPlan { constructor() { this._temperatureRange = { low: 100, high: 200, } } withinRange(bottom, top) { return (bottom >= this.temperatureRange.low) && (top <= this._temperatureRange.high); } // it's a temp function to replace withinRange xxxNewWithinRange(aNumberRange) { return this.withinRange(aNumberRange.low, aNumberRange.high); } } ``` 2. 最後 ```javascript // caller if (!aPlan.withinRange(aRoom.daysTempRange)) { alerts.push('room temperature went outside range'); } class HeatingPlan { constructor() { this._temperatureRange = { low: 100, high: 200, } } withinRange(aNumberRange) { return aNumberRange.low >= this._temperatureRange.low && aNumberRange.high <= this._temperatureRange.high; } } ``` 衍伸 ```javascript // caller const low = aRoom.daysTempRange.low; const high = aRoom.daysTempRange.high; if (!AnimationPlaybackEvent.withinRange(low, high)) { alerts.push("room temperature went outside range"); } // caller v2 const low = aRoom.daysTempRange.low; const high = aRoom.daysTempRange.high; const isWithRange = aPlan.withinRange(low, high); if (!isWithRange) { alerts.push('room temperature went outside range'); } // caller v3 const tempRange = aRoom.daysTempRange; const low = tempRange.low; const hight = tempRange.high; const isWithRange = aPlan.withinRange(low, high); if (!isWithRange) { alerts.push('room temperature went outside range'); } // caller v4 const tempRange = aRoom.daysTempRange; const isWithRange = xxxNewWithinRange(aPlan, tempRange); if (!isWithRange) { alerts.push('room temperature went outside range'); } function xxxNewWithinRange(aPlan, tempRange) { const low = tempRange.low; const high = tempRange.high; const isWithRange = aPlan.withinRange(low, high); return isWithRange; } ``` --- ## Replace Parameter with Query (將參數換成查詢程式) > 名言:當function有parameters時,values是caller的責任,否則就是function全權負責。 > > 老闆說他偏好這個技巧,而不是replace query with parameters > 使用時機: 希望使用了這個技巧不會造成function內部與外部變數有依賴關係就是有沒有referential transparency(參考透明性),使用同樣的參數傳進來,會不會得到相同的結果。 1. v1 ```javascript class Order { constructor() { this.quantity = 'I\'m number as number' ; } get finalPrice() { const basePrice = this.quantity * this.itemPrice; let discountLevel; if (this.quantity > 100) discountLevel = 2; else discountLevel = 1; return this.discountPrice(basePrice, discountLevel); } discountPrice(basePrice, discountLevel) { switch(discountLevel) { case 1: return basePrice * 0.95; case 2: return basePrice * 0.9; } } } ``` 2. v2 ```javascript class Order { constructor() { this.quantity = 'I\'m number as number' ; } get finalPrice() { const basePrice = this.quantity * this.itemPrice; return this.discountPrice(basePrice, this.discountLevel); } get discountLevel() { return this.quantity > 100 ? 2 : 1; } discountPrice(basePrice, discountLevel) { switch(discountLevel) { case 1: return basePrice * 0.95; case 2: return basePrice * 0.9; } } } ``` 3. v3 ```javascript class Order { constructor() { this.quantity = 'I\'m number as number' ; } get finalPrice() { const basePrice = this.quantity * this.itemPrice; return this.discountPrice(basePrice); } get discountLevel() { return this.quantity > 100 ? 2 : 1; } discountPrice(basePrice) { switch(this.discountLevel) { case 1: return basePrice * 0.95; case 2: return basePrice * 0.9; } } } ``` --- ## Replace Query with Parameter (將查詢程式換成參數) **Motivation** > 將所有東西都轉成參數化與共享大量的範圍是對立的 > 前者會造成parameters一長串,後者則是會造成function與環境造成大量的耦合 1. v1 ```javascript class HeatingPlan { constructor() { this._max = xxx; this._min = xxx; } get targetTemperature() { if (thermostat.selectedTemperature > this._max) return this._max; else if (thermostat.selectedTemperature < this._min) return this._min; else return thermostat.selectedTemperature; } } // caller if (thePlan.targetTemperature > thermostat.currentTemperature) setToHeat(); else if (thePlan.targetTemperature < thermostat.currentTemperature) setToCool(); else setOff(); ``` 2. v2 ```javascript class HeatingPlan { constructor() { this._max = xxx; this._min = xxx; } get targetTemperature() { return this.xxxNewTargetTemperature(thermostat.selectedTemperature); } xxxNewTargetTemperature(selectedTemperature) { if (selectedTemperature > this._max) return this._max; else if (selectedTemperature < this._min) return this._min; else return selectedTemperature; } } // caller if (thePlan.targetTemperature > thermostat.currentTemperature) setToHeat(); else if (thePlan.targetTemperature < thermostat.currentTemperature) setToCool(); else setOff(); ``` 3. v3 ```javascript class HeatingPlan { constructor() { this._max = xxx; this._min = xxx; } targetTemperature(selectedTemperature) { if (selectedTemperature > this._max) return this._max; else if (selectedTemperature < this._min) return this._min; else return selectedTemperature; } } // caller if (thePlan.targetTemperature(thermostat.selectedTemperature) > thermostat.currentTemperature) setToHeat(); else if (thePlan.targetTemperature(thermostat.selectedTemperature) < thermostat.currentTemperature) setToCool(); else setOff(); ``` note: 傳送同一組參數進去會reture同樣的value回來,是具有透明性 note: 在function內的elements, 沒有引用透明性會導致function也會失去引用透明性 note: 所以增加parameter移到呼叫方 ---