# 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移到呼叫方
---