# Refactoring, 2nd Edition - CH6 ###### tags: `front-end` `refactor` ## Outline ~~1. Encapsulate Variable (封裝變數)~~ ~~2. Rename Variable(更改變數名稱~~ 3. [Introduce Parameter Object (使用參數物件)](#3-Introduce-Parameter-Object-使用參數物件) 4. [Combine Functions into Class (將函式移入類別)](#4-Combine-Functions-into-Class-將函式移入類別) 5. [Combine Functions into Transform (將函式組成轉換函式)](#5-Combine-Functions-into-Transform-(將函式組成轉換函式)) 6. [Split Phase (拆成不同階段)](#6-Split-Phase-(拆成不同階段)) - Discucsion - Reference ~~## 1. Encapsulate Variable (封裝變數)~~ ![](https://i.imgur.com/jU7aWcJ.png) ~~## 2. Rename Variable(更改變數名稱)~~ ![](https://i.imgur.com/O1qWlWh.png) ## 3. Introduce Parameter Object (使用參數物件) ![](https://i.imgur.com/bB7mKmK.png) Before ```javascript! function amountInvoiced(startDate, endDate) {...} function amountReceived(startDate, endDate) {...} function amountOverdue(startDate, endDate) {...} ``` After ```javascript! function amountInvoiced(aDateRange) {...} function amountReceived(aDateRange) {...} function amountOverdue(aDateRange) {...} ``` ### Why: Motivation 1. 將資料泥團改成一個資料結構,可清楚展示資料間的關係 2. 能讓使用新結構的 function params 變少,也可維持取資料的一致性 3. 可以將使用資料相關的行為寫成 function 放入新的資料結構 ### How: Mechanics & Example 0. [Original Code](https://github.com/CodewJoy/refactoring_practice/commit/127f6e5a7a6cc036c66cfe44d8f5ed1ffce98f78) ```javascript! const station = { name: "ZB1", readings: [ {temp: 47, time: "2016-11-10 09:10"}, {temp: 53, time: "2016-11-10 09:20"}, {temp: 58, time: "2016-11-10 09:30"}, {temp: 53, time: "2016-11-10 09:40"}, {temp: 51, time: "2016-11-10 09:50"}, ] }; const operatingPlan = { temperatureFloor: 52, temperatureCeiling: 57 }; // add by JOy function readingsOutsideRange(station, min, max) { return station.readings .filter(r => (r.temp < min || r.temp > max)); } const outsideRange = readingsOutsideRange(station, operatingPlan.temperatureFloor, operatingPlan.temperatureCeiling); console.log('outsideRange', outsideRange); ``` Console result ![](https://i.imgur.com/VpaqWp6.png) #### Class 改法: 1. 宣告 NumberRange 的 Class,將參數放入 Class constructor 中,使用 Change Function Declaration 在 Class 中建立拿取參數的 functio 2. new 一個新的 Class instance,修改呼叫對應參數的 function,確認傳入正確的 instance 3. 將使用原始參數的地方改成使用 new instance 的元素,並移除其他參數 [將參數泥團轉成物件](https://github.com/CodewJoy/refactoring_practice/commit/a55e85f6c1b067d412a08367248cc56b4b973417?diff=split) 4. [將行為移到新建立的 Class 並做對應修改](https://github.com/CodewJoy/refactoring_practice/commit/c2dd1bb7f8ee2c9348b222d80b4e7b557adafd6f) 5. Refacterd code ```javascript! const station = { name: "ZB1", readings: [ {temp: 47, time: "2016-11-10 09:10"}, {temp: 53, time: "2016-11-10 09:20"}, {temp: 58, time: "2016-11-10 09:30"}, {temp: 53, time: "2016-11-10 09:40"}, {temp: 51, time: "2016-11-10 09:50"}, ] }; const operatingPlan = { temperatureFloor: 52, temperatureCeiling: 57 }; class NumberRange { constructor(min, max) { this._data = { min, max } } get min() { return this._data.min; } get max() { return this._data.max; } contains(arg) { return (arg >= this.min && arg <= this.max); } } function readingsOutsideRange(station, range) { return station.readings .filter(r => !range.contains(r.temp)); } const range = new NumberRange(operatingPlan.temperatureFloor, operatingPlan.temperatureCeiling); const outsideRange = readingsOutsideRange(station, range); ``` #### [Functional Programming 改法 (add by me)](https://github.com/CodewJoy/refactoring_practice/commit/fb94508e910578a4d39092f873879bcdf1fde411) * Use higher-order function (NumberRange) to create an object that represents a range of numbers, and a pure function (readingsOutsideRange) to filter the readings that are outside the range. ```javascript! const station = { name: "ZB1", readings: [ {temp: 47, time: "2016-11-10 09:10"}, {temp: 53, time: "2016-11-10 09:20"}, {temp: 58, time: "2016-11-10 09:30"}, {temp: 53, time: "2016-11-10 09:40"}, {temp: 51, time: "2016-11-10 09:50"}, ] }; const operatingPlan = { temperatureFloor: 52, temperatureCeiling: 57 }; const NumberRange = (min, max) => ({ min, max, contains: arg => (arg >= min && arg <= max), }); const readingsOutsideRange = (station, range) => station.readings.filter(r => !range.contains(r.temp)); const range = NumberRange(operatingPlan.temperatureFloor, operatingPlan.temperatureCeiling); const outsideRange = readingsOutsideRange(station, range); console.log('outsideRange', outsideRange); ``` ### 複習重要觀念 * Class: Class 本身是一個物件,包含了共用資料和方法。當 new 一個 Class 時,會得到一個 Instance Object。每個 Instance 都有自己的實體變數,這些變數儲存在記憶體中,可以記錄自己的參數狀態。不同 Instance 之間是獨立的,修改其中一個物件的實體變數不會影響其他物件。 * 在這裡的 [Functional Programming 改法](#Functional-Programming-改法-add-by-me),每次調用 NumberRange 函數都會回傳一個新的物件。因為 NumberRange 是一個純函數,它不會有任何紀錄狀態的能力,也不會有任何共享資料。如果在應用程序中需要多個 NumberRange 物件並且需要有共用的狀態,那麼使用 Class 定義 NumberRange 會比較適合。 ### Discussion * 在這個重構方法中,Martin Fowler prefer 使用 Class(OOP),因為完成後,作者喜歡將行為移到新建立的物件。 * Would you prefer OOP or FP when use this refactor method in React? . . . . . . * use OOP here props: - 未來可以將更複雜的邏輯和方法放進 Class Instance 中 - 可以創建具有不同狀態的多個 Instance - 可輕鬆管理 Application 狀態 * use FP here props: - 簡單與輕量化,在 NumberRange object 功能相對簡單,也沒有太多複雜的邏輯時適合使用 - 可讀性高 - 可以輕鬆測試和 debug # Refactor functions which deal with same data source ### Why: Motivation - 我們經常需要在取得原始資料後,透過一些前處理來取得 derived data,而許多的 derived data 都是根據相同的邏輯而產生。 - 當遇到這種情形,作者喜歡把所有這類的計算邏輯放在一起避免邏輯重複,也可以在固定的地方尋找與修改他們。 ### How to refactor 這裡有 2 種做法 - [4. Combine Functions into Class (將函式移入類別)](#4-Combine-Functions-into-Class-將函式移入類別) 將原始資料做成 derived data,把邏輯放在 Class 中的方法 - [5. Combine Functions into Transform](#5-Combine-Functions-into-Transform-(將函式組成轉換函式)) 使用 function 接收原始資料,deep clone 後計算 derived data 並回傳 ## 4. Combine Functions into Class (將函式移入類別) ![](https://i.imgur.com/nlIMNqW.png) ### Props 可以清楚展現資料與處理資料的函式所共用的環境 ### How: Mechanics & Example #### 0. Origin Code [Code Demo](https://github.com/CodewJoy/refactoring_practice/commit/a6f0758a0680d49c09d32fc51a591a1aafbbf39a) ```javascript! /** Origin data * 假設供應茶飲的國營企業,每個月都會查看 customer 的使用紀錄 * This funct is assumed by Joy cause in the book has no example of this func * it acquires a reading of the tea meter, * and return a hardcoded reading in this example * but in a real world scenario * it might be getting the reading from a database, a web service, a file or other data source. */ function acquireReading() { const data = { customer: "ivan", quantity: 10, month: 5, year: 2017, } return data; } /** client 1, client 2, client 3 指的是在 code base 中不同地方有使用到 baseRate 的代碼 */ // client 1: 計算該使用者在某年某月使用的茶飲數量所必須繳的稅額(茶在英國視為生活必需品,必須繳稅) const aReading1 = acquireReading(); const baseCharge = baseRate(aReading1.month, aReading1.year) * aReading1.quantity; console.log('baseCharge', baseCharge); // client 2: taxableCharge: 計算法規允許的基本免稅額 const aReading2 = acquireReading(); const base = (baseRate(aReading2.month, aReading2.year) * aReading2.quantity); const taxableCharge = Math.max(0, base - taxThreshold(aReading2.year)); console.log('taxableCharge', taxableCharge); // client 3: 在 code base 中其他地方,寫了跟 client 1 一樣的邏輯,並且使用了 Extract Function const aReading3 = acquireReading(); const basicChargeAmount = calculateBaseCharge(aReading3); function calculateBaseCharge(aReading) { return baseRate(aReading.month, aReading.year) * aReading.quantity; } function calculateBaseCharge(aReading) { return baseRate(aReading.month, aReading.year) * aReading.quantity; } console.log('basicChargeAmount', basicChargeAmount); /** Following funct is assumed by Joy cause in the book has no related example */ function baseRate(month, year) { // hypothetical calculations to determine base rate based on month and year const baseRate = 2; if(year >= 2020 && month >= 6) baseRate +=1; return baseRate; } function taxThreshold(year) { // hypothetical calculations to determine tax threshold based on year const taxThreshold = 100; if(year >= 2021) taxThreshold +=50; return taxThreshold; } ``` 1. 對共用的資料記錄使用 Encapsulate Record,將資料紀錄轉成 Class [Code Demo](https://github.com/CodewJoy/refactoring_practice/commit/de00807116e57adfe761da3564db5d8c44e86e66) 2. 使用 Move Function 將每個使用共用資料紀錄的函式移入 new 出來的 Class [Move Function and handle customer 3](https://github.com/CodewJoy/refactoring_practice/commit/6cbbb5f8e5462e8a9ebe5ffa1a5b2c2337d1fba5) [Rename Function and handle customer 1](https://github.com/CodewJoy/refactoring_practice/commit/bcf97d14a8a4142b6e7103771a22d004c3160ea5) 3. 使用 Extract Function 提取所有資料處理邏輯,移入 Class 中 Move Function and handle customer 3 [Handle customer 2 and Extract Function](https://github.com/CodewJoy/refactoring_practice/commit/2c2e1d1cd73f92fdb1102bbfa9bc6407728b41ec) [Handle customer 2 and Move Function again](https://github.com/CodewJoy/refactoring_practice/commit/6ec7a3520e778900c2a0f8bac179cf9025c56770) 4. Final Code ```javascript! /** Origin data * 假設供應茶飲的國營企業,每個月都會查看 customer 的使用紀錄 * This funct is assumed by Joy cause in the book has no example of this func * it acquires a reading of the tea meter, * and return a hardcoded reading in this example * but in a real world scenario * it might be getting the reading from a database, a web service, a file or other data source. */ function acquireReading() { const data = { customer: "ivan", quantity: 10, month: 5, year: 2017, } return data; } class Reading { constructor(data) { this._customer = data.customer; this._quantity = data.quantity; this._month = data.month; this._year = data.year; } get customer() {return this._customer;} get quantity() {return this._quantity;} get month() {return this._month;} get year() {return this._year;} get baseCharge() { // Rename Function return baseRate(this._month, this._year) * this._quantity; // move Funtion into Class } get taxableCharge() { return Math.max(0, this.baseCharge - taxThreshold(this.year)); } } /** client 1, client 2, client 3 指的是在 code base 中不同地方有使用到 baseRate 的代碼 */ // client 1: 計算該使用者在某年某月使用的茶飲數量所必須繳的稅額(茶在英國視為生活必需品,必須繳稅) const rawReading1 = acquireReading(); const aReading1 = new Reading(rawReading1); const baseCharge = aReading1.baseCharge; console.log('baseCharge', baseCharge); // test result: 20 // client 2: taxableCharge: 計算法規允許的基本免稅額 const rawReading2 = acquireReading(); const aReading2 = new Reading(rawReading2); const taxableCharge = aReading2.taxableCharge; console.log('taxableCharge', taxableCharge); // test result: 0 // client 3: 在 code base 中其他地方,寫了跟 client 1 一樣的邏輯,並且使用了 Extract Function const rawReading3 = acquireReading(); const aReading3 = new Reading(rawReading3); const basicChargeAmount = aReading3.baseCharge; console.log('basicChargeAmount', basicChargeAmount); // test result: 20 /** Following funct is assumed by Joy cause in the book has no related example */ function baseRate(month, year) { // hypothetical calculations to determine base rate based on month and year const baseRate = 2; if(year >= 2020 && month >= 6) baseRate +=1; return baseRate; } function taxThreshold(year) { // hypothetical calculations to determine tax threshold based on year const taxThreshold = 100; if(year >= 2021) taxThreshold +=50; return taxThreshold; } ``` ### Uniform Access Principle (統一存取原則) * 物件的屬性(ex: aReading1.quantity)和計算函式(ex: aReading1.baseCharge)應該以相同的方式存取。這代表著使用方不應該可以分辨出一個值是被存成物件的屬性欄位還是計算函式。 * 符合這個原則是好的,因為它允許使用方以相同的方式跟物件互動,程式碼會更好維護。 ## 5. Combine Functions into Transform (將函式組成轉換函式) ![](https://i.imgur.com/YT74s9u.png) ### How: Mechanics & Example Concept: 使用「資料轉換函式」,用它接受原始資料,並計算所有的 derived data。 0. [Original Code: Same as original code in Combine Functions into Class (將函式移入類別)](https://github.com/CodewJoy/refactoring_practice/commit/a6f0758a0680d49c09d32fc51a591a1aafbbf39a) 1. 建立轉換函式,用它接收原始資料,建立一個 deep copy obj 並回傳 命名邏輯 - 如果轉換邏輯產生的是相同東西,但是有額外的資訊 => 作者習慣用 enrich,ex: enrichReading - 如果會產生不一樣的東西,則習慣用 transform,ex: transformReading - 針對還沒轉換前的資料,作者習慣加上 raw,ex: rawReading 2. 選出一些邏輯,將其移入轉換函式,在 copied obj 建立新欄位(key),修改原本使用該邏輯的地方,讓他使用新欄位 [Move Funtion and handle client 3](https://github.com/CodewJoy/refactoring_practice/commit/1d4dc21f85ad31993bbc5b562ff3c33c24fb9b81) [Handle client 2](https://github.com/CodewJoy/refactoring_practice/commit/2761c166d606ec0c9e3446b69bf815f9c039e113) 3. 重複處理其他函式 [Move Funtion of Client 2's taxableCharge](https://github.com/CodewJoy/refactoring_practice/commit/7e15d48d3c204e1434618ab474908870b8a6442e) 4. Final Code ```javascript! /** Origin data * 假設供應茶飲的國營企業,每個月都會查看 customer 的使用紀錄 * This funct is assumed by Joy cause in the book has no example of this func * it acquires a reading of the tea meter, * and return a hardcoded reading in this example * but in a real world scenario * it might be getting the reading from a database, a web service, a file or other data source. */ function acquireReading() { const data = { customer: "ivan", quantity: 10, month: 5, year: 2017, } return data; } /** client 1, client 2, client 3 指的是在 code base 中不同地方有使用到 baseRate 的代碼 */ // client 1: 計算該使用者在某年某月使用的茶飲數量所必須繳的稅額(茶在英國視為生活必需品,必須繳稅) const rawReading1 = acquireReading(); const aReading1 = enrichReading(rawReading1); const baseCharge = aReading1.baseCharge; console.log('baseCharge', baseCharge); // test result: 20 // client 2: taxableCharge: 計算法規允許的基本免稅額 const rawReading2 = acquireReading(); const aReading2 = enrichReading(rawReading2); const taxableCharge = aReading2.taxableCharge; console.log('taxableCharge', taxableCharge); // test result: 0 // client 3: 在 code base 中其他地方,寫了跟 client 1 一樣的邏輯,並且使用了 Extract Function const rawReading3 = acquireReading(); const aReading3 = enrichReading(rawReading3); const basicChargeAmount = aReading3.baseCharge; console.log('basicChargeAmount', basicChargeAmount); // test result: 20 function enrichReading(original) { const result = deepClone(original); result.baseCharge = calculateBaseCharge(result); result.taxableCharge = Math.max(0, result.baseCharge - taxThreshold(result.year)); return result; } function calculateBaseCharge(aReading) { return baseRate(aReading.month, aReading.year) * aReading.quantity; } /** Following funct is assumed by Joy cause in the book has no related example */ function baseRate(month, year) { // hypothetical calculations to determine base rate based on month and year const baseRate = 2; if(year >= 2020 && month >= 6) baseRate +=1; return baseRate; } function taxThreshold(year) { // hypothetical calculations to determine tax threshold based on year const taxThreshold = 100; if(year >= 2021) taxThreshold +=50; return taxThreshold; } function deepClone(obj) { if (obj === null || typeof obj !== 'object') { return obj; } let clone = Array.isArray(obj) ? [] : {}; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { clone[key] = deepClone(obj[key]); } } return clone; } ``` ### How to choose **Combine Functions into Class v.s. Combine Functions into Transform** - **如果資料會被程式碼更新**:使用 Combine Functions into Class 較適合,因為 Class 的 derived data 可以即時反應原始資料的更新(new 一個 Class 時,instance 的資料會存在同一個 object 中且可以記錄自己的參數狀態),這樣才能確保每次拿到的都是最新且正確的資料。也可以用 setter 去 update 資料。 - **如果資料不會被程式碼更新**:可以使用 Combine Functions into Transform,該方法會 deep clone 原始資料後再 parse 成 derived data。由於 derived data 是一個新的 object,所以當原始資料被改變時,derived data 並不會隨之改變。因此,當來源資料會被程式碼更新時,採取該方法可能會造成資料上的不一致。 - 作者也會根據原本專案的風格來選擇要用哪一種方法 - 心得 ![](https://i.imgur.com/vtZiPRT.png) ![](https://i.imgur.com/C7ZTeNu.png) ## 6. Split Phase (拆成不同階段) ![](https://i.imgur.com/3sVriPN.png) ### Why: Motivation 1. 當遇到一段處理不同事情的程式碼,應該設法把它拆成不同的模組 => 好處是當以後需要調整時,就可以分別調整其中一個模組,不需要同時調整兩段程式碼 2. 進行拆解的作法其中之一:把想要的執行的大邏輯拆解成連續的步驟去抽 function,在 function 中做資料的 interface,減少 function 的參數傳遞 3. 當拆解成不同模組與資料 interface,就可以任意的根據使用情境組合模組與資料 ### How: Mechanics & Example 0. Example 商業邏輯:假設今天在實作商城的購物計算... - 產品資訊:產品的底價為 5 美元,折扣門檻為 10 件,折扣率為 10% => 如果訂購數量為 10 件或更多,就會有折扣。 - 運費資訊:如果訂購數量為 15 件產品,運費為每件 2 美元,折扣閾值為 $50。如果訂單總價格大於 $50 美元,可使用折扣 => 折扣後的費用為每件 1 美元。 1. [origin Code](https://github.com/CodewJoy/refactoring_practice/commit/7c50c91aebc3371593fc26afc6ec1ae4aa916da0) ```javascript! const product = { // add by Joy basePrice: 5, discountThreshold: 10, discountRate: 0.1 }; const quantity = 15; // add by Joy const shippingMethod = { // add by Joy feePerCase: 2, discountThreshold: 50, discountedFee: 1 }; function priceOrder(product, quantity, shippingMethod) { // 使用產品資訊計算產品價格 const basePrice = product.basePrice * quantity; const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate; // 使用貨運資訊計算運費 const shippingPerCase = (basePrice > shippingMethod.discountThreshold) ? shippingMethod.discountedFee : shippingMethod.feePerCase; const shippingCost = quantity * shippingPerCase; // 最終價格 const price = basePrice - discount + shippingCost; return price; } const price = priceOrder(product, quantity, shippingMethod); console.log('price', price); // 87.5 ``` 2. 針對第二階段的程式碼抽 function [Extract function of applyShipping](https://github.com/CodewJoy/refactoring_practice/commit/474b87b5fedb0fd1a6441bb570018e3252cc0a9f) 3. 加入兩個階段溝通的資料結構 interface [Create Data Interface](https://github.com/CodewJoy/refactoring_practice/commit/ce6ea605c027fa04c336a95e0a4596956e900d92) 4. 針對第一階段的程式碼抽 function [Extract function of calculatePricingData](https://github.com/CodewJoy/refactoring_practice/commit/7c6a18c239e95f53283b599ad6be25db92b259cd) 5. Refactered Code ```javascript! const product = { // add by Joy... basePrice: 5, discountThreshold: 10, discountRate: 0.1 }; const quantity = 15; // add by Joy... const shippingMethod = { // add by Joy... feePerCase: 2, discountThreshold: 50, discountedFee: 1 }; function priceOrder(product, quantity, shippingMethod) { const priceData = calculatePricingData(product, quantity); return applyShipping(priceData, shippingMethod); } // 使用產品資訊計算產品價格 function calculatePricingData(product, quantity) { const basePrice = product.basePrice * quantity; const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate; return { basePrice, quantity, discount }; } function applyShipping(priceData, shippingMethod) { // 使用貨運資訊計算運費 const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold) ? shippingMethod.discountedFee : shippingMethod.feePerCase; const shippingCost = priceData.quantity * shippingPerCase; // 最終價格 const price = priceData.basePrice - priceData.discount + shippingCost; return price; } const price = priceOrder(product, quantity, shippingMethod); console.log('price', price); // test result: 87.5 ``` ### Discussion [My way of Extract Function](https://github.com/CodewJoy/refactoring_practice/commit/f24916f7e5ca05253e3c920696ade1daa01fbf4d) => 我認為這樣修改可讀性較高,符合單一職責原則 Single Responsibility Principle [if consider 商城全館免運活動](https://github.com/CodewJoy/refactoring_practice/commit/8e3db526112eaded8babcaa0589d0585d1ea1385) => 也具有可讀性 ## Reference * Refactoring, 2nd Edition * [[Book] 重構:改善既有程式的設計](https://pjchender.dev/software-development/book-refactor/)