本文總結(jié)自筆者的開發(fā)經(jīng)驗(yàn)以及 Martin Fowler 的《重構(gòu),改善既有代碼設(shè)計(jì)》讀書體會(huì),希望能幫助更多的開發(fā)者了解重構(gòu),重構(gòu)并不是想象中的重活,它也可以很簡單。Commit a feature,review and refactor。
這里先給重構(gòu)下一個(gè)定義:改善既有代碼的設(shè)計(jì)。
具體來說就是在不改變代碼功能行為的情況下,對(duì)其內(nèi)部結(jié)構(gòu)的一種調(diào)整。需要注意的是,重構(gòu)不是代碼優(yōu)化,重構(gòu)注重的是提高代碼的可理解性與可擴(kuò)展性,對(duì)性能的影響可好可壞。而性能優(yōu)化則讓程序運(yùn)行的更快,當(dāng)然,最終的代碼可能更難理解和維護(hù)。
如果沒有重構(gòu),在軟件不停的版本迭代中,代碼的設(shè)計(jì)只會(huì)越來越腐敗,導(dǎo)致軟件開發(fā)寸步難行。
這里的原因主要有兩點(diǎn):
所以想要體面又快速的開發(fā)功能,重構(gòu)必不可少。
在開發(fā)中,我們需要先理解代碼在做什么,才能著手修改,很多時(shí)候自己寫的代碼都會(huì)忘記其實(shí)現(xiàn),更不論別人的代碼??赡茉谶@段代碼中有一段糟糕的條件判斷邏輯,又或者其變量命名實(shí)在糟糕又確實(shí)注釋,需要花上好一段時(shí)間才能明白其真正用意。
合理的重構(gòu)能讓代碼“自解釋”,以方便理解,無論對(duì)于協(xié)同開發(fā),還是維護(hù)先前自己實(shí)現(xiàn)的功能,對(duì)代碼的開發(fā)有著立竿見影的效果。
提高開發(fā)的速度可能有點(diǎn)“反直覺”,因?yàn)橹貥?gòu)在很多時(shí)候看來是額外的工作量,并沒有新的功能和特性產(chǎn)出,但是減少代碼的書寫量(復(fù)用模塊),方便定位錯(cuò)誤(代碼結(jié)構(gòu)優(yōu)良),這些能讓我們在開發(fā)的時(shí)候節(jié)省大量的時(shí)間,在后續(xù)的開發(fā)中“輕裝上陣”。
Kent Beck 提出了“兩頂帽子”的比喻,在開發(fā)軟件時(shí),把自己的時(shí)間分配給兩種截然不同的行為:添加新功能和重構(gòu),添加新功能的時(shí)候,不應(yīng)該修改既有的代碼,只管添加新功能,并讓程序正確運(yùn)行;在重構(gòu)時(shí)就不能添加新功能,只管調(diào)整代碼結(jié)構(gòu),只有在絕對(duì)必要時(shí)才能修改相關(guān)代碼。
在開發(fā)過程中,我們可能經(jīng)常變換“帽子”,在新增功能的時(shí)候會(huì)意識(shí)到,如果把程序結(jié)構(gòu)改一下,功能的添加會(huì)容易很多,或者實(shí)現(xiàn)會(huì)更加優(yōu)雅,于是一會(huì)換一頂“帽子”,一邊重構(gòu),一邊新增新功能。這很容易讓自己產(chǎn)生混亂,對(duì)自己的代碼難以理解。
任何時(shí)候我們都要清楚自己戴的是哪一頂“帽子”,并專注自己的編程狀態(tài),這讓我們的目標(biāo)清晰且過程可控,能對(duì)自己編碼的進(jìn)度有掌握。
重構(gòu)的過程并非一蹴而就,如果因?yàn)橹貥?gòu)影響了自己對(duì)時(shí)間的掌控,對(duì)函數(shù)功能的掌控,那么你就應(yīng)該及時(shí)停下,思考你的行為是否值得。我們必須保證程序的可用性與時(shí)間的可控性,并且要保證我們的步伐要小,確保每一步都有 git 管理和代碼測試,否則你會(huì)陷入程序不可用的中間態(tài),更可怕的是你忘記了之前代碼的樣子!
在本文后續(xù)章節(jié)何時(shí)開始重構(gòu)中會(huì)有更多這方面的介紹,這里先跳過不談。
重構(gòu)世界的規(guī)則我們已經(jīng)了解,下面有一份重構(gòu)指南北,是時(shí)候去回顧代碼里的片段,識(shí)別它們身上的臭味并將其消滅!
當(dāng)然,如果覺得其中的內(nèi)容過長,可跳過不看,也可匆匆略過,日后回顧也是不錯(cuò)的選擇。
我承認(rèn),在偵探小說透過神秘的文字去猜測故事情節(jié)是一種很棒的體驗(yàn),但在代碼中,這往往讓程序員困擾!需要花費(fèi)大量時(shí)間去探究一個(gè)變量的作用和一個(gè)函數(shù)的功能,甚者需要在該代碼片段中加入大量注釋。
這里并不是批評(píng)注釋這種行為,而是一個(gè)優(yōu)秀的代碼片段和編碼命名,往往能讓代碼自解釋,減少一些不必要的注釋,閱讀代碼如同閱讀文字一樣流暢。
由此可見,變量命名實(shí)在是任何重構(gòu)時(shí)都要第一步更正的地方,但也很遺憾的是,命名是編程中最難的幾件事之一。
cgi
和 cgiList
等變量,你可以直接從中讀出之間的關(guān)聯(lián),若是cgi
和 list
呢,它們之間的聯(lián)系就丟失了,又或者同時(shí)出現(xiàn)了 people
和 human
兩個(gè)變量,這是不是讓你產(chǎn)生了疑惑?變量命名并沒有確切細(xì)致的教程,也很難強(qiáng)制統(tǒng)一,一般符合以下三點(diǎn)即可。
實(shí)踐是檢驗(yàn)質(zhì)量的唯一標(biāo)準(zhǔn),如果你的變量能夠讓其他同學(xué)見名知意,就說明你是正確的!
提煉重復(fù)代碼無疑是重構(gòu)中最經(jīng)典的手法,很多時(shí)候我們會(huì)在不同的地方寫下相似的代碼,又或者拷貝一份副本至當(dāng)前上下文中,它們之間的差異寥寥無幾。
這時(shí)會(huì)出現(xiàn)一個(gè)很棘手的問題,當(dāng)需要去修改其中的功能時(shí),你必須找出所有的副本一一修改,這讓人在閱讀和修改代碼時(shí)都很容易出現(xiàn)紕漏。所以我們要拒絕重復(fù)造輪子,盡量實(shí)現(xiàn)高可復(fù)用性的代碼。
我們可以將其抽離成一個(gè)公共函數(shù),并以其功能作為命名。
函數(shù)越長,就越難以理解,與之帶來的還有高耦合性,不利于拆解重組。
目前普遍認(rèn)為代碼的行數(shù)不要超出一個(gè)屏幕的范圍,因?yàn)檫@樣會(huì)造成上下滾動(dòng),會(huì)增大出錯(cuò)的概率。根據(jù)騰訊代碼規(guī)范,一個(gè)函數(shù)的代碼行數(shù)不要超出 80 行。
直接看下面這兩份代碼,它們實(shí)現(xiàn)的是同樣的功能,不用理解它們的含義(也沒有任何含義),僅僅簡單對(duì)比視覺效果,感覺如何?
// 重構(gòu)前
function changeList(list) {
console.log('some operation of list')
for (let i=0; i<list.length; i ) {
// do sth
}
console.log('conditional judgment')
let result
if (list.length < 4) {
result = list.pop()
} else {
result = list.shift()
}
const today = new Date(Date.now())
const dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() 30);
result.dueDate = dueDate
return result
}
// 重構(gòu)后
function changeList(list) {
console.log('some operation of list')
operationOfList(list)
console.log('conditional judgment')
const result = judgment(list)
result.dueDate = getRecordTime()
return result
}
function operationOfList(list) {
for (let i=0; i<list.length; i ) {
// do sth
}
return list
}
function judgment(list) {
let result
if (list.length < 4) {
result = list.pop()
} else {
result = list.shift()
}
return result
}
function getRecordTime() {
const today = new Date(Date.now())
const dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() 30);
return dueDate
}
事實(shí)證明,拆分函數(shù)有利于更好更快地理解代碼,以及降低耦合度,更方便地重新“組裝”新函數(shù)。當(dāng)然你也可能此時(shí)會(huì)覺得很麻煩,屬于多此一舉,但是重構(gòu)的目標(biāo)就是要保證代碼的可讀性。如果有一天你想要修改或增加該函數(shù)的功能,看到重構(gòu)后的代碼你會(huì)感謝自己的。
數(shù)據(jù)泥團(tuán)(魔法數(shù)字),顧名思義就是一幫數(shù)據(jù)無規(guī)則的結(jié)合在一起,這讓人對(duì)其難以把控。
如果說有多個(gè)參數(shù)互相搭配,又或者說某些數(shù)據(jù)總是成群結(jié)隊(duì)出現(xiàn),那我們就該把這團(tuán)泥塑造成一個(gè)具體的形象,將其封裝成一個(gè)數(shù)據(jù)對(duì)象。
function addUser(name, gender, age) {
// some other codes
...
// officeAreaCode 與 officeNumber 成對(duì)出現(xiàn),如果缺少 officeNumber,那么 officeAreaCode 也沒有意義,這里應(yīng)該組合起來
clumps.officeAreaCode = ' 86'
clumps.officeNumber = 13688888888;
return person
}
// 重構(gòu)后
class TelephoneNumber(officeAreaCode, officeNumber) {
constructor() {
this.officeAreaCode = officeAreaCode
this.officeNumber = officeNumber
}
}
// 參數(shù)融合
function addUser(person) {
// some other codes
...
// 封裝數(shù)據(jù)
person.telephone = new TelephoneNumber(' 86', '13688888888')
}
很多時(shí)候我們都不可避免地使用全局?jǐn)?shù)據(jù),哪怕只有一個(gè)變量,全局?jǐn)?shù)據(jù)對(duì)我們的管理提出了更高的要求。因?yàn)槟呐乱粋€(gè)小小的更改,都可能引起很多地方出現(xiàn)問題,更可怕的是在無意間觸發(fā)了這種更改。
全局?jǐn)?shù)據(jù)也阻礙了程序的可預(yù)測性,由于每個(gè)函數(shù)都能訪問這些變量,因此越來越難弄清那個(gè)函數(shù)實(shí)際讀寫這些變量,要理解一個(gè)程序的工作方式,幾乎必須考慮修改全局狀態(tài)的每個(gè)函數(shù),使得調(diào)試變得困難。
如果不依靠全局變量,則可以根據(jù)不同函數(shù)之間傳遞的狀態(tài),這樣以來,就能更好的了解每個(gè)功能的作用,因?yàn)槟銦o需考慮全局變量。
let globalData = 1
// bad
function foo() {
globalData = 2
}
// bad
function fuu() {
globalData = {
a: 1
}
}
現(xiàn)在,我們要對(duì)全局?jǐn)?shù)據(jù)進(jìn)行一些封裝,控制對(duì)其的訪問。
// 使用常量 good
const constantData = 1
// 封裝變量操作 good
let globalData = 1
function getGlobalData() {
return globalData
}
function setGlobalData(newGlobalData){
if (!isValid(newGlobalData)) {
throw Error('Illegal input?。?!')
return
}
globalData = newGlobalData
}
// 暴露方法
export {
getGlobalData,
setGlobalData
}
現(xiàn)在,全局變量不會(huì)輕易的被“誤觸”,也能很快定義其修改的位置和防止錯(cuò)誤的修改。
當(dāng)某個(gè)函數(shù)會(huì)因?yàn)椴煌蛟诓煌较蛏习l(fā)生變化時(shí),發(fā)散式變化就誕生了。這聽起來有點(diǎn)迷糊,那么就用代碼來解釋吧。
function getPrice(order) {
// 獲取基礎(chǔ)價(jià)格
const basePrice = order.quantity * order.itemPrice
// 獲取折扣
const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05
// 獲取運(yùn)費(fèi)
const shipping = Math.min(basePrice * 0.1, 100)
// 計(jì)算價(jià)格
return basePrice - quantityDiscount shipping
}
const orderPrice = getPrice(order);
這個(gè)函數(shù)用于計(jì)算商品的價(jià)格,它的計(jì)算包含了基礎(chǔ)價(jià)格 數(shù)量折扣 運(yùn)費(fèi),如果基礎(chǔ)價(jià)格的計(jì)算規(guī)則改變,我們需要修改這個(gè)函數(shù);如果折扣規(guī)則發(fā)生改變,我們需要修改這個(gè)函數(shù);如果運(yùn)費(fèi)計(jì)算規(guī)則改變了,我們還是要修改這個(gè)函數(shù)。
這種修改容易造成混亂,我們當(dāng)然也希望程序一旦需要修改,我們就夠跳到系統(tǒng)的某一點(diǎn),所以是時(shí)候抽離它們了。
// 計(jì)算基礎(chǔ)價(jià)格
function calBasePrice(order) {
return order.quantity * order.itemPrice
}
// 計(jì)算折扣
function calDiscount(order) {
return Math.max(0, order.quantity - 500) * order.itemPrice * 0.05
}
// 計(jì)算運(yùn)費(fèi)
function calShipping(basePrice) {
return Math.min(basePrice * 0.1, 100)
}
// 計(jì)算商品價(jià)格
function getPrice(order) {
return calBasePrice(order) - calDiscount(order) calShipping(calBasePrice(order))
}
const orderPrice = getPrice(order)
雖然該函數(shù)行數(shù)不多,當(dāng)其重構(gòu)的過程與先前的過長函數(shù)一致,但是將各個(gè)功能抽離處理,有利于更清晰的定位問題與修改。所以過長函數(shù)擁有多重臭味道!需要及時(shí)消滅。
霰彈式修改與發(fā)散式變化聽起來差異不大,實(shí)則它們是陰陽兩面。霰彈式修改與重復(fù)代碼有點(diǎn)像,當(dāng)我們需要做出一點(diǎn)小修改時(shí),卻要去四處一個(gè)個(gè)的修正,你不僅很難找到它們,也很容易錯(cuò)過某個(gè)重要的修改,直至錯(cuò)誤發(fā)生!
// File Reading.js
const reading = {customer: 'ivan', quantity: 10, month: 5, year: 2017}
function acquireReading() { return reading }
function baseRate(month, year) {
/* */
}
// File 1
const aReading = acquireReading()
const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity
// File 2
const aReading = acquireReading()
const base = (baseRate(aReading.month, aReading.year) * aReading.quantity)
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year))
function taxThreshold(year) { /* */ }
// File 3
const aReading = acquireReading()
const basicChargeAmount = calculateBaseCharge(aReading)
function calculateBaseCharge(aReading) {
return baseRate(aReading.month, aReading.year) * aReading.quantity
}
在上面的代碼中,如果 reading
的邏輯發(fā)生了改變,我們需要跨越好幾個(gè)文件去調(diào)整它,這很容易造成遺漏的發(fā)生。
由于每個(gè)地方都對(duì) reading
進(jìn)行了操作,那么我們可以將其封裝起來,統(tǒng)一在一個(gè)文件中進(jìn)行管理。
// File Reading.js
class Reading {
constructor(data) {
this.customer = data.customer
this.quantity = data.quantity
this.month = data.month
this.year = data.year
}
get baseRate() {
/* ... */
}
get baseCharge() {
return baseRate(this.month, this.year) * this.quantity
}
get taxableCharge() {
return Math.max(0, base - taxThreshold())
}
get taxThreshold() {
/* ... */
}
}
const reading = new Reading({ customer: 'Evan You', quantity: 10, month: 8, year: 2021 })
所有的相關(guān)邏輯在一起,不僅能提供一個(gè)共用的環(huán)境,也可以簡化調(diào)用邏輯,更加清晰。
很驚訝,循環(huán)一直是程序中的核心要素,在這里重構(gòu)的世界里居然變成了臭味道。這里并不是要將循環(huán)取締,但僅僅使用普通的 for 循環(huán)在當(dāng)下有些過時(shí),現(xiàn)在我們有很好的替代品。在 JS 的世界里擁有著管道操作(filter,map 等)它們可以幫助我們更好的處理元素以及幫助我們看清處理的動(dòng)作。
下面我們將會(huì)從人群中挑選出所有的程序員并記錄他們的名字,哪種做法更賞心悅目呢?
// for
const programmerNames = []
for (const item of people) {
if (item.job === 'programmer') {
programmerNames.push(item.name)
}
}
// pipeline
const programmerNames = people
.filter(item => item.job === 'programmer')
.map(item => item.name)
當(dāng)然,這個(gè)時(shí)候你可能會(huì)提出它們之間性能的差別,不要忘了重構(gòu)的意義是為了代碼更清晰,性能在這里并不是優(yōu)先要考慮的事情。
不過這里很也很遺憾的告訴你一個(gè)點(diǎn),僅有少數(shù)的管道操作符支持逆序操作(reduce,reduceRight),更多時(shí)候必須在之前使用 reverse 來反轉(zhuǎn)數(shù)組。所以是否要取締 for 循環(huán),取決于你自己,也取決于實(shí)際場景。
復(fù)雜的條件邏輯是導(dǎo)致復(fù)雜度上升的地點(diǎn)之一,代碼會(huì)告訴我們會(huì)發(fā)生什么事,可我們常常弄不清為什么會(huì)發(fā)生這樣的事,這就證明代碼的可讀性大大降低了。是時(shí)候?qū)⑺鼈兎庋b成一個(gè)帶有說明的函數(shù)了,見文知意,一目了然。
// bad
if (!date.isBefore(plan.summberStart) && !date.isAfter(plan.summberEnd)) {
charge = quantity * plan.summerRate
} else {
charge = quantity * plan.regularRate plan.regularServiceCharge
}
// good
if (isSummer()) {
charge = quantity * plan.summerRate
} else {
charge = quantity * plan.regularRate plan.regularServiceCharge
}
// perfect
isSummer() ? summerCharge() : regularCharge()
如果一串條件檢查,檢查條件各不相同,最終行為卻一致,那么我們就應(yīng)該使用邏輯或和邏輯與將他們合并成為一個(gè)條件表達(dá)式。然后再做上面代碼的邏輯,封裝!
if (man.age < 18) return 0
if (man.hasHeartDisease) return 0
if (!isFull) return 0
// step 1
if (man.age < 18 && man.hasHeartDisease && !isFull) return 0
// step 2
if (isIlegalEntry(man) && !isFull) return 0
如果某個(gè)函數(shù)只是提供一個(gè)值,沒有任何副作用,這是一個(gè)很有價(jià)值的東西,我可以任意調(diào)用這個(gè)函數(shù)沒有后顧之憂,也可以隨意的搬遷該函數(shù)??偠灾枰傩牡氖虑樯俣嗔?。
明確的分離“有副作用”和“無副作用”兩種函數(shù)是一個(gè)很好的想法,查詢函數(shù)和修改函數(shù)搭配在平常的開發(fā)中也經(jīng)常出現(xiàn),是時(shí)候?qū)⑺鼈兎蛛x了!
// 給 2 鵝歲以下的五星員工發(fā)郵件鼓勵(lì)
function getTotalAdnSendEmail() {
const emailList = programmerList
.filter(item => item.occupationalAge <= 2 && item.stars === 5)
.map(item => item.email)
return sendEmail(emailList)
}
// 分離查詢函數(shù),這里可以通過傳遞參數(shù)進(jìn)一步控制查詢的語句
function search() {
return programmerList
.filter(item => item.occupationalAge <= 2 && item.stars === 5)
.map(item => item.email)
}
function send() {
return sendEmail(search())
}
這樣可以更好的控制查詢行為以及復(fù)用函數(shù),我們需要在一個(gè)函數(shù)內(nèi)操心的事情又少了一些。
直接上代碼:
function getPayAmount() {
let result
if (isDead) {
// do sth and assign to result
} else {
if (isSeparated) {
// do sth and assign to result
} else {
if (isRetired) {
// do sth and assign to result
} else {
// do sth and assign to result
}
}
}
return result
}
在閱讀該函數(shù)時(shí),是否慶幸在 if else 之間的并非代碼而是一段注釋,如果是一段代碼,則讓人目眩眼花。那下面的代碼呢?
function getPayAmount() {
if (isDead) return deatAmount()
if (isSeparated) return serparateAmount()
if (isRetired) return retiredAmount()
return normalPayAmount()
}
衛(wèi)語句的精髓就是給予某條分支特別的重視,它告訴閱讀者,這種情況并不是本函數(shù)的所關(guān)心的核心邏輯,如果它真的發(fā)生了,會(huì)做一些必要的工作然后提前退出。
我相信每個(gè)程序員都會(huì)聽過“每個(gè)函數(shù)只能有一個(gè)入口和一個(gè)出口”這個(gè)觀念,但“單一出口”原則在這里似乎不起作用,在重構(gòu)的世界中,保證代碼清晰才是最關(guān)鍵的。如果“單一出口”能讓代碼更易讀,那么就使用它吧,否則就不必這么做。
重構(gòu)的最佳時(shí)機(jī)是在添加新功能之前。
在動(dòng)手添加新功能之前,看看現(xiàn)有的代碼庫,此時(shí)經(jīng)常會(huì)發(fā)現(xiàn),如果對(duì)代碼結(jié)構(gòu)做一點(diǎn)微調(diào),自己的工作會(huì)輕松很多。比如有個(gè)函數(shù)提供了需要的大部分功能,但有幾個(gè)字面量的值與自己的需求不同。如果不做重構(gòu),需要復(fù)制整個(gè)函數(shù)再進(jìn)行微調(diào),這導(dǎo)致重復(fù)代碼的產(chǎn)生,這是代碼臭味道的開始。所以需要戴上重構(gòu)的“帽子”,做完這件事后,再輕松的開發(fā)你的功能。
但這也是在理想情況下的設(shè)想,事實(shí)上任務(wù)的安排總有時(shí)間限制,多出一段的重構(gòu)的耗時(shí)可能會(huì)讓你對(duì)時(shí)間的安排失控,導(dǎo)致延期,所以對(duì)于工作中的場景,并不適用。
結(jié)合任務(wù)的排期和實(shí)際的工作,重構(gòu)的最佳時(shí)機(jī)是在完成一個(gè)功能后和 code review 后。
在完成功能并測試通過后,此時(shí)對(duì)任務(wù)的進(jìn)度是可控的,重構(gòu)不會(huì)影響到代碼既有實(shí)現(xiàn)的功能,在使用 git 等版本控制系統(tǒng)管理的情況下,回退至功能可用時(shí)的代碼片段是非常輕易的,但你無法立即完成你從未實(shí)現(xiàn)好的功能。
在每完成一個(gè)功能后重構(gòu),也類似于垃圾回收中的時(shí)間分片的思想,不必等到代碼中塞滿“垃圾”時(shí)才開始清理,導(dǎo)致“全停頓”的發(fā)生。將重構(gòu)分解為一小步一小步。
讓一個(gè)團(tuán)隊(duì),特別是共同實(shí)現(xiàn)同一項(xiàng)目的團(tuán)隊(duì)來校驗(yàn)自己的代碼,往往能夠發(fā)現(xiàn)自己難以注意的問題。比如自己寫的一個(gè)功能其實(shí)另一個(gè)同學(xué)已經(jīng)實(shí)現(xiàn)過了,完全可以抽離出來復(fù)用;比如有經(jīng)驗(yàn)的同學(xué)提出更加優(yōu)雅的實(shí)現(xiàn)方案。
并且自己編寫的代碼往往帶有自己的風(fēng)格和“壞習(xí)慣”,代碼風(fēng)格并不是一種錯(cuò)誤,但在一個(gè)團(tuán)隊(duì)中,不同代碼風(fēng)格的混雜會(huì)帶來閱讀與合作的困難,而對(duì)于“壞習(xí)慣”而言,比如極其復(fù)雜的條件判斷語句等,自己難以意識(shí)到該做法的不妥,需要群眾的意見加以改正。
實(shí)際上在每完成一個(gè)新功能后重構(gòu)還有一些筆者認(rèn)為很重要的優(yōu)勢,就是你會(huì)對(duì)自己的代碼有更清晰的了解,你會(huì)去做今后不會(huì)再做的事情。
對(duì)代碼更清晰,能讓我們更好的定位問題和提高自己的代碼水平,這很好理解。
那這個(gè)今后不會(huì)再做的事情是什么呢?沒錯(cuò),就是重構(gòu)。當(dāng)你完成新功能后,如果不立刻進(jìn)行 review,那么在上線后很可能就從此被封存在某個(gè)地方,直到它出現(xiàn)了 bug。久而久之,整個(gè)項(xiàng)目變得難以維護(hù),代碼開始發(fā)臭。
而在完成新功能后重構(gòu),工作量一般也不會(huì)很大,是“順手完成的小工作”,屬于一鼓作氣階段,如果打算以后再看,那么往往就沒有這個(gè)以后了。
其實(shí)并不希望這個(gè)狀況發(fā)生,這代表代碼結(jié)構(gòu)已經(jīng)處于混亂中,添加新功能需要翻越好幾個(gè)障礙。此時(shí)重構(gòu)是個(gè)必選項(xiàng),也必然是個(gè)大工程,這會(huì)造成項(xiàng)目的“全停頓”。更糟糕的是此時(shí)重構(gòu)可能不如直接重寫,這是我們需要避免的情況。
這個(gè)無需多言。
如果一個(gè)功能或者 API 一直以來“兢兢業(yè)業(yè)”,從未出現(xiàn)過 bug,即便其底下隱藏著十分丑陋的代碼,那么我們也可以忍受它繼續(xù)保持丑陋。不要忘了重構(gòu)的初衷,其中之一就是為了讓人更好的理解代碼,當(dāng)我們不需要理解其時(shí),就讓它安安靜靜地躺在哪兒吧,不要讓不可控制的行為發(fā)生是重構(gòu)的原則之一。
如果一個(gè)功能被多個(gè)模塊引用,而這些模塊并非你負(fù)責(zé)時(shí),你必須提前通知負(fù)責(zé)人,聲明將要對(duì)這部分功能進(jìn)行修改,哪怕重構(gòu)不會(huì)帶來任何使用上的變化,因?yàn)檫@也意味著重構(gòu)行為將會(huì)帶來“不可控”。
關(guān)于重構(gòu)對(duì)性能的影響,是被提及最多的問題。畢竟重構(gòu)代碼很多時(shí)候都帶來了運(yùn)行代碼行數(shù)的增加(并不一定是代碼總行數(shù)增加,因?yàn)橹貥?gòu)有提煉函數(shù)的部分,優(yōu)秀的重構(gòu)總會(huì)帶來代碼總行數(shù)的下降)。又或者說將一些性能好的代碼變?yōu)榭勺x性更高的代碼,犧牲掉性能優(yōu)勢。
首先需要回顧一下,代碼重構(gòu)和性能優(yōu)化是兩個(gè)不同的概念,重構(gòu)僅僅只考慮代碼的可理解性和可拓展性,對(duì)于代碼的執(zhí)行效率是不在乎的,在重構(gòu)時(shí)切記不要同時(shí)戴著“兩頂帽子”。
而重構(gòu)對(duì)于性能的影響,也很可能沒有你想象中的那么高,在面對(duì)大部分的業(yè)務(wù)情況時(shí),重構(gòu)前和重構(gòu)后代碼的性能差別幾乎難以體現(xiàn)。
大部分情況下,我們不需要極致的“壓榨”計(jì)算機(jī),來減少使用的微乎其微的計(jì)算機(jī)時(shí)鐘周期時(shí)間,更重要的是,減少自己在開發(fā)中使用的時(shí)間。
如果對(duì)于重構(gòu)后的的性能不滿意,可以在完成重構(gòu)后有的放矢的對(duì)部分高耗時(shí)功能進(jìn)行代碼優(yōu)化。一件很有趣的事情是:大多數(shù)程序運(yùn)行的大半時(shí)間都在一小部分代碼身上,只要優(yōu)化這部分代碼,就能帶來顯著的性能提高。如果你一視同仁的優(yōu)化所有代碼,就會(huì)發(fā)現(xiàn)這是在白費(fèi)勁,因?yàn)楸粌?yōu)化的代碼不會(huì)被經(jīng)常執(zhí)行。
所以我認(rèn)為重構(gòu)時(shí)大可不必為性能過多擔(dān)憂,可以放手去重構(gòu),如有必要再針對(duì)個(gè)別代碼片段優(yōu)化。短期來看,重構(gòu)的確可能使軟件變慢,但重構(gòu)也使性能調(diào)優(yōu)更容易,最終還是會(huì)得到很好的效果。
筆者并非“重構(gòu)大師”,本文也只展現(xiàn)了一些十分常見的重構(gòu)手法以及對(duì)重構(gòu)淺略的思考,還有很多經(jīng)典的手法與案例,本文未于展示,讀者如果對(duì)重構(gòu)感興趣,想深入了解的話,可以閱讀 Martin Fowler 的經(jīng)典書籍《重構(gòu),改善既有代碼的設(shè)計(jì) 第二版》,其中的示例語言選用了 JavaScript,這簡直是前端工程師的福音。
對(duì)于 VSCode 用戶而言,有很多優(yōu)秀的插件幫助你重構(gòu),比如 JavaScript Booster 或 Stepsize,這些插件能提示你如何重構(gòu)且為代碼添加書簽和報(bào)告。
都讀到這了,接下來知道該怎么做了吧。Commit a feature,review and refactor。
[0] 《重構(gòu),改善既有代碼的設(shè)計(jì) 第二版》Martin Fowler
[1] 代碼中常見的 24 種壞味道及重構(gòu)手法
[2] vscode中6個(gè)好用的前端重構(gòu)插件
聯(lián)系客服