帶你入門前端工程(十):重構
《重構2》一書中對重構進行了定義:
所謂重構(refactoring)是這樣一個過程:在不改變代碼外在行為的前提下,對代碼做出修改,以改進程序的內部結構。重構是一種經千錘百煉形成的有條不紊的程序整理方法,可以最大限度地減小整理過程中引入錯誤的概率。本質上說,重構就是在代碼寫好之后改進它的設計。
重構和性能優化有相同點,也有不同點。
相同的地方是它們都在不改變程序功能的情況下修改代碼;不同的地方是重構為了讓代碼變得更加容易理解、易于修改,性能優化則是為了讓程序運行得更快。這里還得重點提一句,由于側重點不同,重構可能使程序運行得更快,也可能使程序運行得更慢。
重構可以一邊寫代碼一邊重構,也可以在程序寫完后,拿出一段時間專門去做重構。沒有說哪個方式更好,視個人情況而定。如果你專門拿一段時間來做重構,則建議在重構一段代碼后,立即進行測試。這樣可以避免修改代碼太多,在出錯時找不到錯誤點。
重構的原則
- 事不過三,三則重構。即不能重復寫同樣的代碼,在這種情況下要去重構。
- 如果一段代碼讓人很難看懂,那就該考慮重構了。
- 如果已經理解了代碼,但是非常繁瑣或者不夠好,也可以重構。
- 過長的函數,需要重構。
- 一個函數最好對應一個功能,如果一個函數被塞入多個功能,那就要對它進行重構了。(4 和 5 不沖突)
- 重構的關鍵在于運用大量微小且保持軟件行為的步驟,一步步達成大規模的修改。每個單獨的重構要么很小,要么由若干小步驟組合而成。
重構的手法
在《重構2》這本書中,介紹了多達上百種重構手法。但我覺得以下八種是比較常用的:
- 提取重復代碼,封裝成函數
- 拆分功能太多的函數
- 變量/函數改名
- 替換算法
- 以函數調用取代內聯代碼
- 移動語句
- 折分嵌套條件表達式
- 將查詢函數和修改函數分離
提取重復代碼,封裝成函數
假設有一個查詢數據的接口 /getUserData?age=17&city=beijing
?,F在需要做的是把用戶數據:{ age: 17, city: 'beijing' }
轉成 URL 參數的形式:
let result = ''
const keys = Object.keys(data) // { age: 17, city: 'beijing' }
keys.forEach(key => {
result += '&' + key + '=' + data[key]
})
result.substr(1) // age=17&city=beijing
如果只有這一個接口需要轉換,不封裝成函數是沒問題的。但如果有多個接口都有這種需求,那就得把它封裝成函數了:
function JSON2Params(data) {
let result = ''
const keys = Object.keys(data)
keys.forEach(key => {
result += '&' + key + '=' + data[key]
})
return result.substr(1)
}
拆分功能太多的函數
下面是一個打印賬單的程序:
function printBill(data = []) {
// 匯總數據
const total = {}
data.forEach(item => {
if (total[item.department] === undefined) {
total[item.department] = 0
}
total[item.department] += item.value
})
// 打印匯總后的數據
const keys = Object.keys(total)
keys.forEach(key => {
console.log(`${key} 部門:${total[key]}`)
})
}
printBill([
{
department: '銷售部',
value: 89,
},
{
department: '后勤部',
value: 132,
},
{
department: '財務部',
value: 78,
},
{
department: '總經辦',
value: 90,
},
{
department: '后勤部',
value: 56,
},
{
department: '總經辦',
value: 120,
},
])
可以看到這個 printBill()
函數實際上包含有兩個功能:匯總和打印。我們可以把匯總數據的代碼提取出來,封裝成一個函數。這樣 printBill()
函數就只需要關注打印功能了。
function printBill(data = []) {
const total = calculateBillData(data)
const keys = Object.keys(total)
keys.forEach(key => {
console.log(`${key} 部門:${total[key]}`)
})
}
function calculateBillData(data) {
const total = {}
data.forEach(item => {
if (total[item.department] === undefined) {
total[item.department] = 0
}
total[item.department] += item.value
})
return total
}
變量/函數改名
無論是變量命名,還是函數命名,都要盡量讓別人明白你這個變量/函數是干什么的。變量命名的規則著重于描述“是什么”,函數命名的規則著重于描述“做什么”。
變量
const a = width * height
上面這個變量就不太好,a
很難讓人看出來它是什么。
const area = width * height
改成這樣就很好理解了,原來這個變量是表示面積。
函數
function cache(data) {
const result = []
data.forEach(item => {
if (item.isCache) {
result.push(item)
}
})
return result
}
這個函數名稱會讓人很疑惑,cache
代表什么?是設置緩存還是刪除緩存?再一細看代碼,噢,原來是獲取緩存數據。所以這個函數名稱改成 getCache()
更加合適。
替換算法
function foundPersonData(person) {
if (person == 'Tom') {
return {
name: 'Tom',
age: 18,
id: 21,
}
}
if (person == 'Jim') {
return {
name: 'Jim',
age: 20,
id: 111,
}
}
if (person == 'Lin') {
return {
name: 'Lin',
age: 19,
id: 10,
}
}
return null
}
上面這個函數的功能是根據用戶姓名查找用戶的詳細信息,可以看到這個函數做了三次 if
判斷,如果沒找到數據就返回 null
。這個函數不利于擴展,每多一個用戶就得多寫一個 if
語句,我們可以用更方便的“查找表”來替換它。
function foundPersonData(person) {
const data = {
'Tom': {
name: 'Tom',
age: 18,
id: 21,
},
'Jim': {
name: 'Jim',
age: 20,
id: 111,
},
'Lin': {
name: 'Lin',
age: 19,
id: 10,
},
}
return data[person] || null
}
修改后代碼結構看起來更加清晰,也方便未來做擴展。
以函數調用取代內聯代碼
如果一些代碼所做的事情和已有函數的功能重復,那就最好用函數調用來取代這些代碼。
let hasApple = false
for (const fruit of fruits) {
if (fruit == 'apple') {
hasApple = true
break
}
}
例如上面的代碼,可以用數組的 includes()
方法代替:
const hasApple = fruits.includes('apple')
修改后代碼更加簡潔。
移動語句
讓存在關聯的東西一起出現,可以使代碼更容易理解。如果有一些代碼都是作用在一個地方,那么最好是把它們放在一起,而不是夾雜在其他的代碼中間。最簡單的情況下,只需使用移動語句就可以讓它們聚集起來。就像下面的示例一樣:
const name = getName()
const age = getAge()
let revenue
const address = getAddress()
// ...
const name = getName()
const age = getAge()
const address = getAddress()
let revenue
// ...
由于兩塊數據區域的功能是不同的,所以除了移動語句外,我還在它們之間空了一行,這樣讓人更容易區分它們之間的不同。
折分嵌套條件表達式
當很多的條件表達式嵌套在一起時,會讓代碼變得很難閱讀:
function getPayAmount() {
if (isDead) {
return deadAmount()
} else {
if (isSeparated) {
return separatedAmount()
} else if (isRetired) {
return retireAmount()
} else {
return normalAmount()
}
}
}
function getPayAmount() {
if (isDead) return deadAmount()
if (isSeparated) return separatedAmount()
if (isRetired) return retireAmount()
return normalAmount()
}
將條件表達式拆分后,代碼的可閱讀性大大增強了。
將查詢函數和修改函數分離
一般的查詢函數都是用于取值的,例如 getUserData()
、getAget()
、getName()
等等。有時候,我們可能為了方便,在查詢函數上附加其他功能。例如下面的函數:
function getValue() {
let result = 0
this.data.forEach(val => result += val)
// 這里插入了一個奇怪的操作
sendBill()
return result
}
千萬不要這樣做,函數很重要的功能是職責分離。所以我們要將它們分開:
function getValue() {
let result = 0
this.data.forEach(val => result += val)
return result
}
function sendBill() {
// ...
}
這樣函數的功能就很清晰了。
小結
古人云:盡信書,不如無書?!吨貥?》也不例外,在看這本書的時候一定要帶著批判性的目光去閱讀它。
里面介紹的重構手法有很多,多達上百種,但這些手法不一定適用所有人。所以一定要有取舍,將里面有用的手法摘抄下來,時不時的看幾遍。這樣在寫代碼時,重構才能像呼吸一樣自然,即使用了你也不知道。
參考資料
帶你入門前端工程 全文目錄:
- 技術選型:如何進行技術選型?
- 統一規范:如何制訂規范并利用工具保證規范被嚴格執行?
- 前端組件化:什么是模塊化、組件化?
- 測試:如何寫單元測試和 E2E(端到端) 測試?
- 構建工具:構建工具有哪些?都有哪些功能和優勢?
- 自動化部署:如何利用 Jenkins、Github Actions 自動化部署項目?
- 前端監控:講解前端監控原理及如何利用 sentry 對項目實行監控。
- 性能優化(一):如何檢測網站性能?有哪些實用的性能優化規則?
- 性能優化(二):如何檢測網站性能?有哪些實用的性能優化規則?
- 重構:為什么做重構?重構有哪些手法?
- 微服務:微服務是什么?如何搭建微服務項目?
- Severless:Severless 是什么?如何使用 Severless?
0 條評論