<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          重構(gòu)指北——《重構(gòu),改善既有代碼設(shè)計(jì)》精讀

          共 17296字,需瀏覽 35分鐘

           ·

          2021-08-22 11:13

          本文總結(jié)自筆者的開發(fā)經(jīng)驗(yàn)以及 Martin Fowler 的《重構(gòu),改善既有代碼設(shè)計(jì)》讀書體會(huì),希望能幫助更多的開發(fā)者了解重構(gòu),重構(gòu)并不是想象中的重活,它也可以很簡單。Commit a feature,review and refactor。

          1. 什么是重構(gòu)

          這里先給重構(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ù)。

          2. 為什么要重構(gòu)

          2.1. 改善程序的內(nèi)部設(shè)計(jì)

          如果沒有重構(gòu),在軟件不停的版本迭代中,代碼的設(shè)計(jì)只會(huì)越來越腐敗,導(dǎo)致軟件開發(fā)寸步難行。

          這里的原因主要有兩點(diǎn):

          • 人們只為了短期目的而修改代碼時(shí),往往沒有完全理解整體的架構(gòu)設(shè)計(jì)(在大項(xiàng)目中常有這種情況,比如在不同的地方,使用完全相同的語句做著同樣的事情),代碼就會(huì)失去自己的結(jié)構(gòu),代碼結(jié)構(gòu)的流失具有累積效應(yīng),越難看出代碼所代表的設(shè)計(jì)意圖,就越難保護(hù)其設(shè)計(jì)。
          • 我們幾乎不可能預(yù)先做出完美的設(shè)計(jì),以面對(duì)后續(xù)未知的功能開發(fā),只有在實(shí)踐中才能找到真理。

          所以想要體面又快速的開發(fā)功能,重構(gòu)必不可少。

          2.2. 使得代碼更容易理解

          在開發(fā)中,我們需要先理解代碼在做什么,才能著手修改,很多時(shí)候自己寫的代碼都會(huì)忘記其實(shí)現(xiàn),更不論別人的代碼??赡茉谶@段代碼中有一段糟糕的條件判斷邏輯,又或者其變量命名實(shí)在糟糕又確實(shí)注釋,需要花上好一段時(shí)間才能明白其真正用意。

          合理的重構(gòu)能讓代碼“自解釋”,以方便理解,無論對(duì)于協(xié)同開發(fā),還是維護(hù)先前自己實(shí)現(xiàn)的功能,對(duì)代碼的開發(fā)有著立竿見影的效果。

          2.3. 提高開發(fā)的速度 && 方便定位錯(cuò)誤

          提高開發(fā)的速度可能有點(diǎn)“反直覺”,因?yàn)橹貥?gòu)在很多時(shí)候看來是額外的工作量,并沒有新的功能和特性產(chǎn)出,但是減少代碼的書寫量(復(fù)用模塊),方便定位錯(cuò)誤(代碼結(jié)構(gòu)優(yōu)良),這些能讓我們在開發(fā)的時(shí)候節(jié)省大量的時(shí)間,在后續(xù)的開發(fā)中“輕裝上陣”。

          3. 重構(gòu)的原則

          3.1. 保持當(dāng)下的編程狀態(tài)

          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)度有掌握。

          3.2. 可控制的重構(gòu)

          重構(gòu)的過程并非一蹴而就,如果因?yàn)橹貥?gòu)影響了自己對(duì)時(shí)間的掌控,對(duì)函數(shù)功能的掌控,那么你就應(yīng)該及時(shí)停下,思考你的行為是否值得。我們必須保證程序的可用性與時(shí)間的可控性,并且要保證我們的步伐要小,確保每一步都有 git 管理和代碼測試,否則你會(huì)陷入程序不可用的中間態(tài),更可怕的是你忘記了之前代碼的樣子!

          在本文后續(xù)章節(jié)何時(shí)開始重構(gòu)中會(huì)有更多這方面的介紹,這里先跳過不談。

          4. 識(shí)別代碼的臭味道

          重構(gòu)世界的規(guī)則我們已經(jīng)了解,下面有一份重構(gòu)指北,是時(shí)候去回顧代碼里的片段,識(shí)別它們身上的臭味并將其消滅!

          當(dāng)然,如果覺得其中的內(nèi)容過長,可跳過不看,也可匆匆略過,日后回顧也是不錯(cuò)的選擇。

          4.1. 神秘命名

          我承認(rèn),在偵探小說透過神秘的文字去猜測故事情節(jié)是一種很棒的體驗(yàn),但在代碼中,這往往讓程序員困擾!需要花費(fèi)大量時(shí)間去探究一個(gè)變量的作用和一個(gè)函數(shù)的功能,甚者需要在該代碼片段中加入大量注釋。

          這里并不是批評(píng)注釋這種行為,而是一個(gè)優(yōu)秀的代碼片段和編碼命名,往往能讓代碼自解釋,減少一些不必要的注釋,閱讀代碼如同閱讀文字一樣流暢。

          由此可見,變量命名實(shí)在是任何重構(gòu)時(shí)都要第一步更正的地方,但也很遺憾的是,命名是編程中最難的幾件事之一。

          • 需要在簡潔性和命名長度中平衡。
          • 需要統(tǒng)一變量命名的風(fēng)格,特別是一個(gè)整個(gè)團(tuán)隊(duì)!因?yàn)樽兞棵辉诖a風(fēng)格檢測之內(nèi)!
          • 需要變量的名字既能做到彼此關(guān)聯(lián),又對(duì)其信息的識(shí)別互不干擾,想象一下,在一個(gè)代碼片段中在存在著 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)即可。

          • 有意義的
          • 相關(guān)聯(lián)的
          • 不復(fù)用的

          實(shí)踐是檢驗(yàn)質(zhì)量的唯一標(biāo)準(zhǔn),如果你的變量能夠讓其他同學(xué)見名知意,就說明你是正確的!

          4.2. 重復(fù)代碼

          提煉重復(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ù),并以其功能作為命名。

          4.3. 過長函數(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ì)感謝自己的。

          4.4. 數(shù)據(jù)泥團(tuán) && 過長參數(shù)

          數(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')
          }

          4.5. 全局?jǐn)?shù)據(jù)

          很多時(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 = {
              a1
            }
          }

          現(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ò)誤的修改。

          4.6. 發(fā)散式變化

          當(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.1100)
            // 計(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.1100)
          }
          // 計(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í)消滅。

          4.7. 霰彈式修改

          霰彈式修改與發(fā)散式變化聽起來差異不大,實(shí)則它們是陰陽兩面。霰彈式修改與重復(fù)代碼有點(diǎn)像,當(dāng)我們需要做出一點(diǎn)小修改時(shí),卻要去四處一個(gè)個(gè)的修正,你不僅很難找到它們,也很容易錯(cuò)過某個(gè)重要的修改,直至錯(cuò)誤發(fā)生!

          // File Reading.js
          const reading = {customer"ivan"quantity10month5year2017}
          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'quantity10month8year2021 })

          所有的相關(guān)邏輯在一起,不僅能提供一個(gè)共用的環(huán)境,也可以簡化調(diào)用邏輯,更加清晰。

          4.8. for 循環(huán)語句

          很驚訝,循環(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í)際場景。

          4.9. 復(fù)雜的條件邏輯 && 合并條件表達(dá)式

          復(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 < 18return 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

          4.10. 查詢函數(shù)與修改函數(shù)耦合

          如果某個(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)操心的事情又少了一些。

          4.11. 以衛(wèi)語句(Guard Clauses)取代嵌套條件表達(dá)式

          直接上代碼:

          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)鍵的。如果“單一出口”能讓代碼更易讀,那么就使用它吧,否則就不必這么做。

          5. 何時(shí)開始重構(gòu)

          5.1. 添加新功能之前

          重構(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ì)于工作中的場景,并不適用。

          5.2. 完成新功能后或 code review 后

          結(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è)以后了。

          5.3. 難以添加新功能的時(shí)候

          其實(shí)并不希望這個(gè)狀況發(fā)生,這代表代碼結(jié)構(gòu)已經(jīng)處于混亂中,添加新功能需要翻越好幾個(gè)障礙。此時(shí)重構(gòu)是個(gè)必選項(xiàng),也必然是個(gè)大工程,這會(huì)造成項(xiàng)目的“全停頓”。更糟糕的是此時(shí)重構(gòu)可能不如直接重寫,這是我們需要避免的情況。

          6. 什么時(shí)候不該重構(gòu)

          6.1. 重寫比重構(gòu)容易

          這個(gè)無需多言。

          6.2. 不需要理解該代碼片段時(shí)

          如果一個(gè)功能或者 API 一直以來“兢兢業(yè)業(yè)”,從未出現(xiàn)過 bug,即便其底下隱藏著十分丑陋的代碼,那么我們也可以忍受它繼續(xù)保持丑陋。不要忘了重構(gòu)的初衷,其中之一就是為了讓人更好的理解代碼,當(dāng)我們不需要理解其時(shí),就讓它安安靜靜地躺在哪兒吧,不要讓不可控制的行為發(fā)生是重構(gòu)的原則之一。

          6.3. 未與合作者商量時(shí)

          如果一個(gè)功能被多個(gè)模塊引用,而這些模塊并非你負(fù)責(zé)時(shí),你必須提前通知負(fù)責(zé)人,聲明將要對(duì)這部分功能進(jìn)行修改,哪怕重構(gòu)不會(huì)帶來任何使用上的變化,因?yàn)檫@也意味著重構(gòu)行為將會(huì)帶來“不可控”。

          7. 重構(gòu)與性能

          關(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ì)得到很好的效果。

          8. 完結(jié)撒花

          筆者并非“重構(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。

          9. 引用

          [0] 《重構(gòu),改善既有代碼的設(shè)計(jì) 第二版》Martin Fowler

          [1]   代碼中常見的 24 種壞味道及重構(gòu)手法

          [2]   vscode中6個(gè)好用的前端重構(gòu)插件


          往期精彩

          瀏覽 43
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产拍拍拍| 国产精品无码午夜福利 | 凹凸视频在线 | 久久黄色视频网址 | 色婷婷在线精品 |