Contents hide 1. 什么是重構這里先給重構下一個定義:改善既有代碼的設計。 具體來說就是在不改變代碼功能行為的情況下,對其內部結構的一種調整。需要注意的是,重構不是代碼優(yōu)化,重構注重的是提高代碼的可理解性與可擴展性,對性能的影響可好可壞。而性能優(yōu)化則讓程序運行的更快,當然,最終的代碼可能更難理解和維護。 2. 為什么要重構2.1. 改善程序的內部設計如果沒有重構,在軟件不停的版本迭代中,代碼的設計只會越來越腐敗,導致軟件開發(fā)寸步難行。 這里的原因主要有兩點:
所以想要體面又快速的開發(fā)功能,重構必不可少。 2.2. 使得代碼更容易理解在開發(fā)中,我們需要先理解代碼在做什么,才能著手修改,很多時候自己寫的代碼都會忘記其實現(xiàn),更不論別人的代碼??赡茉谶@段代碼中有一段糟糕的條件判斷邏輯,又或者其變量命名實在糟糕又確實注釋,需要花上好一段時間才能明白其真正用意。 合理的重構能讓代碼“自解釋”,以方便理解,無論對于協(xié)同開發(fā),還是維護先前自己實現(xiàn)的功能,對代碼的開發(fā)有著立竿見影的效果。 2.3. 提高開發(fā)的速度 && 方便定位錯誤提高開發(fā)的速度可能有點“反直覺”,因為重構在很多時候看來是額外的工作量,并沒有新的功能和特性產出,但是減少代碼的書寫量(復用模塊),方便定位錯誤(代碼結構優(yōu)良),這些能讓我們在開發(fā)的時候節(jié)省大量的時間,在后續(xù)的開發(fā)中“輕裝上陣”。 3. 重構的原則3.1. 保持當下的編程狀態(tài)Kent Beck 提出了“兩頂帽子”的比喻,在開發(fā)軟件時,把自己的時間分配給兩種截然不同的行為:添加新功能和重構,添加新功能的時候,不應該修改既有的代碼,只管添加新功能,并讓程序正確運行;在重構時就不能添加新功能,只管調整代碼結構,只有在絕對必要時才能修改相關代碼。 在開發(fā)過程中,我們可能經常變換“帽子”,在新增功能的時候會意識到,如果把程序結構改一下,功能的添加會容易很多,或者實現(xiàn)會更加優(yōu)雅,于是一會換一頂“帽子”,一邊重構,一邊新增新功能。這很容易讓自己產生混亂,對自己的代碼難以理解。 任何時候我們都要清楚自己戴的是哪一頂“帽子”,并專注自己的編程狀態(tài),這讓我們的目標清晰且過程可控,能對自己編碼的進度有掌握。 3.2. 可控制的重構重構的過程并非一蹴而就,如果因為重構影響了自己對時間的掌控,對函數(shù)功能的掌控,那么你就應該及時停下,思考你的行為是否值得。我們必須保證程序的可用性與時間的可控性,并且要保證我們的步伐要小,確保每一步都有 git 管理和代碼測試,否則你會陷入程序不可用的中間態(tài),更可怕的是你忘記了之前代碼的樣子! 在本文后續(xù)章節(jié)何時開始重構中會有更多這方面的介紹,這里先跳過不談。 4. 識別代碼的臭味道重構世界的規(guī)則我們已經了解,下面有一份重構指南北,是時候去回顧代碼里的片段,識別它們身上的臭味并將其消滅! 當然,如果覺得其中的內容過長,可跳過不看,也可匆匆略過,日后回顧也是不錯的選擇。 4.1. 神秘命名我承認,在偵探小說透過神秘的文字去猜測故事情節(jié)是一種很棒的體驗,但在代碼中,這往往讓程序員困擾!需要花費大量時間去探究一個變量的作用和一個函數(shù)的功能,甚者需要在該代碼片段中加入大量注釋。 這里并不是批評注釋這種行為,而是一個優(yōu)秀的代碼片段和編碼命名,往往能讓代碼自解釋,減少一些不必要的注釋,閱讀代碼如同閱讀文字一樣流暢。 由此可見,變量命名實在是任何重構時都要第一步更正的地方,但也很遺憾的是,命名是編程中最難的幾件事之一。
變量命名并沒有確切細致的教程,也很難強制統(tǒng)一,一般符合以下三點即可。
實踐是檢驗質量的唯一標準,如果你的變量能夠讓其他同學見名知意,就說明你是正確的! 4.2. 重復代碼提煉重復代碼無疑是重構中最經典的手法,很多時候我們會在不同的地方寫下相似的代碼,又或者拷貝一份副本至當前上下文中,它們之間的差異寥寥無幾。 這時會出現(xiàn)一個很棘手的問題,當需要去修改其中的功能時,你必須找出所有的副本一一修改,這讓人在閱讀和修改代碼時都很容易出現(xiàn)紕漏。所以我們要拒絕重復造輪子,盡量實現(xiàn)高可復用性的代碼。 我們可以將其抽離成一個公共函數(shù),并以其功能作為命名。 4.3. 過長函數(shù)函數(shù)越長,就越難以理解,與之帶來的還有高耦合性,不利于拆解重組。 目前普遍認為代碼的行數(shù)不要超出一個屏幕的范圍,因為這樣會造成上下滾動,會增大出錯的概率。根據(jù)騰訊代碼規(guī)范,一個函數(shù)的代碼行數(shù)不要超出 80 行。 直接看下面這兩份代碼,它們實現(xiàn)的是同樣的功能,不用理解它們的含義(也沒有任何含義),僅僅簡單對比視覺效果,感覺如何? // 重構前事實證明,拆分函數(shù)有利于更好更快地理解代碼,以及降低耦合度,更方便地重新“組裝”新函數(shù)。當然你也可能此時會覺得很麻煩,屬于多此一舉,但是重構的目標就是要保證代碼的可讀性。如果有一天你想要修改或增加該函數(shù)的功能,看到重構后的代碼你會感謝自己的。 4.4. 數(shù)據(jù)泥團 && 過長參數(shù)數(shù)據(jù)泥團(魔法數(shù)字),顧名思義就是一幫數(shù)據(jù)無規(guī)則的結合在一起,這讓人對其難以把控。 如果說有多個參數(shù)互相搭配,又或者說某些數(shù)據(jù)總是成群結隊出現(xiàn),那我們就該把這團泥塑造成一個具體的形象,將其封裝成一個數(shù)據(jù)對象。 4.5. 全局數(shù)據(jù)很多時候我們都不可避免地使用全局數(shù)據(jù),哪怕只有一個變量,全局數(shù)據(jù)對我們的管理提出了更高的要求。因為哪怕一個小小的更改,都可能引起很多地方出現(xiàn)問題,更可怕的是在無意間觸發(fā)了這種更改。 全局數(shù)據(jù)也阻礙了程序的可預測性,由于每個函數(shù)都能訪問這些變量,因此越來越難弄清那個函數(shù)實際讀寫這些變量,要理解一個程序的工作方式,幾乎必須考慮修改全局狀態(tài)的每個函數(shù),使得調試變得困難。 如果不依靠全局變量,則可以根據(jù)不同函數(shù)之間傳遞的狀態(tài),這樣以來,就能更好的了解每個功能的作用,因為你無需考慮全局變量。 let globalData = 1現(xiàn)在,我們要對全局數(shù)據(jù)進行一些封裝,控制對其的訪問。 現(xiàn)在,全局變量不會輕易的被“誤觸”,也能很快定義其修改的位置和防止錯誤的修改。 4.6. 發(fā)散式變化當某個函數(shù)會因為不同原因在不同方向上發(fā)生變化時,發(fā)散式變化就誕生了。這聽起來有點迷糊,那么就用代碼來解釋吧。 function getPrice(order) {這個函數(shù)用于計算商品的價格,它的計算包含了基礎價格 數(shù)量折扣 運費,如果基礎價格的計算規(guī)則改變,我們需要修改這個函數(shù);如果折扣規(guī)則發(fā)生改變,我們需要修改這個函數(shù);如果運費計算規(guī)則改變了,我們還是要修改這個函數(shù)。 這種修改容易造成混亂,我們當然也希望程序一旦需要修改,我們就夠跳到系統(tǒng)的某一點,所以是時候抽離它們了。 雖然該函數(shù)行數(shù)不多,當其重構的過程與先前的過長函數(shù)一致,但是將各個功能抽離處理,有利于更清晰的定位問題與修改。所以過長函數(shù)擁有多重臭味道!需要及時消滅。 4.7. 霰彈式修改霰彈式修改與發(fā)散式變化聽起來差異不大,實則它們是陰陽兩面。霰彈式修改與重復代碼有點像,當我們需要做出一點小修改時,卻要去四處一個個的修正,你不僅很難找到它們,也很容易錯過某個重要的修改,直至錯誤發(fā)生! // File Reading.js在上面的代碼中,如果 由于每個地方都對 所有的相關邏輯在一起,不僅能提供一個共用的環(huán)境,也可以簡化調用邏輯,更加清晰。 4.8. for 循環(huán)語句很驚訝,循環(huán)一直是程序中的核心要素,在這里重構的世界里居然變成了臭味道。這里并不是要將循環(huán)取締,但僅僅使用普通的 for 循環(huán)在當下有些過時,現(xiàn)在我們有很好的替代品。在 JS 的世界里擁有著管道操作(filter,map 等)它們可以幫助我們更好的處理元素以及幫助我們看清處理的動作。 下面我們將會從人群中挑選出所有的程序員并記錄他們的名字,哪種做法更賞心悅目呢? // for當然,這個時候你可能會提出它們之間性能的差別,不要忘了重構的意義是為了代碼更清晰,性能在這里并不是優(yōu)先要考慮的事情。 不過這里很也很遺憾的告訴你一個點,僅有少數(shù)的管道操作符支持逆序操作(reduce,reduceRight),更多時候必須在之前使用 reverse 來反轉數(shù)組。所以是否要取締 for 循環(huán),取決于你自己,也取決于實際場景。 4.9. 復雜的條件邏輯 && 合并條件表達式復雜的條件邏輯是導致復雜度上升的地點之一,代碼會告訴我們會發(fā)生什么事,可我們常常弄不清為什么會發(fā)生這樣的事,這就證明代碼的可讀性大大降低了。是時候將它們封裝成一個帶有說明的函數(shù)了,見文知意,一目了然。 如果一串條件檢查,檢查條件各不相同,最終行為卻一致,那么我們就應該使用邏輯或和邏輯與將他們合并成為一個條件表達式。然后再做上面代碼的邏輯,封裝! if (man.age < 18) return 04.10. 查詢函數(shù)與修改函數(shù)耦合如果某個函數(shù)只是提供一個值,沒有任何副作用,這是一個很有價值的東西,我可以任意調用這個函數(shù)沒有后顧之憂,也可以隨意的搬遷該函數(shù)??偠灾枰傩牡氖虑樯俣嗔?。 明確的分離“有副作用”和“無副作用”兩種函數(shù)是一個很好的想法,查詢函數(shù)和修改函數(shù)搭配在平常的開發(fā)中也經常出現(xiàn),是時候將它們分離了! 這樣可以更好的控制查詢行為以及復用函數(shù),我們需要在一個函數(shù)內操心的事情又少了一些。 4.11. 以衛(wèi)語句(Guard Clauses)取代嵌套條件表達式直接上代碼: function getPayAmount() {在閱讀該函數(shù)時,是否慶幸在 if else 之間的并非代碼而是一段注釋,如果是一段代碼,則讓人目眩眼花。那下面的代碼呢? 衛(wèi)語句的精髓就是給予某條分支特別的重視,它告訴閱讀者,這種情況并不是本函數(shù)的所關心的核心邏輯,如果它真的發(fā)生了,會做一些必要的工作然后提前退出。 我相信每個程序員都會聽過“每個函數(shù)只能有一個入口和一個出口”這個觀念,但“單一出口”原則在這里似乎不起作用,在重構的世界中,保證代碼清晰才是最關鍵的。如果“單一出口”能讓代碼更易讀,那么就使用它吧,否則就不必這么做。 5. 何時開始重構5.1. 添加新功能之前重構的最佳時機是在添加新功能之前。 在動手添加新功能之前,看看現(xiàn)有的代碼庫,此時經常會發(fā)現(xiàn),如果對代碼結構做一點微調,自己的工作會輕松很多。比如有個函數(shù)提供了需要的大部分功能,但有幾個字面量的值與自己的需求不同。如果不做重構,需要復制整個函數(shù)再進行微調,這導致重復代碼的產生,這是代碼臭味道的開始。所以需要戴上重構的“帽子”,做完這件事后,再輕松的開發(fā)你的功能。 但這也是在理想情況下的設想,事實上任務的安排總有時間限制,多出一段的重構的耗時可能會讓你對時間的安排失控,導致延期,所以對于工作中的場景,并不適用。 5.2. 完成新功能后或 code review 后結合任務的排期和實際的工作,重構的最佳時機是在完成一個功能后和 code review 后。 在完成功能并測試通過后,此時對任務的進度是可控的,重構不會影響到代碼既有實現(xiàn)的功能,在使用 git 等版本控制系統(tǒng)管理的情況下,回退至功能可用時的代碼片段是非常輕易的,但你無法立即完成你從未實現(xiàn)好的功能。 在每完成一個功能后重構,也類似于垃圾回收中的時間分片的思想,不必等到代碼中塞滿“垃圾”時才開始清理,導致“全停頓”的發(fā)生。將重構分解為一小步一小步。 讓一個團隊,特別是共同實現(xiàn)同一項目的團隊來校驗自己的代碼,往往能夠發(fā)現(xiàn)自己難以注意的問題。比如自己寫的一個功能其實另一個同學已經實現(xiàn)過了,完全可以抽離出來復用;比如有經驗的同學提出更加優(yōu)雅的實現(xiàn)方案。 并且自己編寫的代碼往往帶有自己的風格和“壞習慣”,代碼風格并不是一種錯誤,但在一個團隊中,不同代碼風格的混雜會帶來閱讀與合作的困難,而對于“壞習慣”而言,比如極其復雜的條件判斷語句等,自己難以意識到該做法的不妥,需要群眾的意見加以改正。 實際上在每完成一個新功能后重構還有一些筆者認為很重要的優(yōu)勢,就是你會對自己的代碼有更清晰的了解,你會去做今后不會再做的事情。 對代碼更清晰,能讓我們更好的定位問題和提高自己的代碼水平,這很好理解。 那這個今后不會再做的事情是什么呢?沒錯,就是重構。當你完成新功能后,如果不立刻進行 review,那么在上線后很可能就從此被封存在某個地方,直到它出現(xiàn)了 bug。久而久之,整個項目變得難以維護,代碼開始發(fā)臭。 而在完成新功能后重構,工作量一般也不會很大,是“順手完成的小工作”,屬于一鼓作氣階段,如果打算以后再看,那么往往就沒有這個以后了。 5.3. 難以添加新功能的時候其實并不希望這個狀況發(fā)生,這代表代碼結構已經處于混亂中,添加新功能需要翻越好幾個障礙。此時重構是個必選項,也必然是個大工程,這會造成項目的“全停頓”。更糟糕的是此時重構可能不如直接重寫,這是我們需要避免的情況。 6. 什么時候不該重構6.1. 重寫比重構容易這個無需多言。 6.2. 不需要理解該代碼片段時如果一個功能或者 API 一直以來“兢兢業(yè)業(yè)”,從未出現(xiàn)過 bug,即便其底下隱藏著十分丑陋的代碼,那么我們也可以忍受它繼續(xù)保持丑陋。不要忘了重構的初衷,其中之一就是為了讓人更好的理解代碼,當我們不需要理解其時,就讓它安安靜靜地躺在哪兒吧,不要讓不可控制的行為發(fā)生是重構的原則之一。 6.3. 未與合作者商量時如果一個功能被多個模塊引用,而這些模塊并非你負責時,你必須提前通知負責人,聲明將要對這部分功能進行修改,哪怕重構不會帶來任何使用上的變化,因為這也意味著重構行為將會帶來“不可控”。 7. 重構與性能關于重構對性能的影響,是被提及最多的問題。畢竟重構代碼很多時候都帶來了運行代碼行數(shù)的增加(并不一定是代碼總行數(shù)增加,因為重構有提煉函數(shù)的部分,優(yōu)秀的重構總會帶來代碼總行數(shù)的下降)。又或者說將一些性能好的代碼變?yōu)榭勺x性更高的代碼,犧牲掉性能優(yōu)勢。 首先需要回顧一下,代碼重構和性能優(yōu)化是兩個不同的概念,重構僅僅只考慮代碼的可理解性和可拓展性,對于代碼的執(zhí)行效率是不在乎的,在重構時切記不要同時戴著“兩頂帽子”。 而重構對于性能的影響,也很可能沒有你想象中的那么高,在面對大部分的業(yè)務情況時,重構前和重構后代碼的性能差別幾乎難以體現(xiàn)。 大部分情況下,我們不需要極致的“壓榨”計算機,來減少使用的微乎其微的計算機時鐘周期時間,更重要的是,減少自己在開發(fā)中使用的時間。 如果對于重構后的的性能不滿意,可以在完成重構后有的放矢的對部分高耗時功能進行代碼優(yōu)化。一件很有趣的事情是:大多數(shù)程序運行的大半時間都在一小部分代碼身上,只要優(yōu)化這部分代碼,就能帶來顯著的性能提高。如果你一視同仁的優(yōu)化所有代碼,就會發(fā)現(xiàn)這是在白費勁,因為被優(yōu)化的代碼不會被經常執(zhí)行。 所以我認為重構時大可不必為性能過多擔憂,可以放手去重構,如有必要再針對個別代碼片段優(yōu)化。短期來看,重構的確可能使軟件變慢,但重構也使性能調優(yōu)更容易,最終還是會得到很好的效果。 8. 完結撒花筆者并非“重構大師”,本文也只展現(xiàn)了一些十分常見的重構手法以及對重構淺略的思考,還有很多經典的手法與案例,本文未于展示,讀者如果對重構感興趣,想深入了解的話,可以閱讀 Martin Fowler 的經典書籍《重構,改善既有代碼的設計 第二版》,其中的示例語言選用了 JavaScript,這簡直是前端工程師的福音。 對于 VSCode 用戶而言,有很多優(yōu)秀的插件幫助你重構,比如 JavaScript Booster 或 Stepsize,這些插件能提示你如何重構且為代碼添加書簽和報告。 都讀到這了,接下來知道該怎么做了吧。Commit a feature,review and refactor。 9. 引用[0] 《重構,改善既有代碼的設計 第二版》Martin Fowler |
|
|