重構:改善既有程式碼的設計(第二版讀書筆記) - 重構、壞程式碼、寫好程式碼

金振宗發表於2020-12-19

偶然發現重構這本書推出了js版,果斷入手,名書之一,尤其還是js版本,相較於java版來說,肯定更適合前端閱讀,購買來自當當。

本書作者 馬丁·福勒,主要著作有:分析模式---可重用物件模型、Kent Beck. 規劃極限程式設計、 UML精粹---標準物件建模語言簡明指南(第三版)、 企業應用架構模式以及本書。本書內容以各種程式碼的“壞味道”,來推進合適的重構手法,和第一版內容相比,有一些部分是更新了(那些被淘汰的程式碼、不合適的例子)。但主體思想還是沒有變,總而言之是一本值得讀的好書

重構,改善既有程式碼設計(第二版)

下面來分享本文章核心內容,讀書筆記。

讀書中感悟最深的名言

  1. 原文:重構技術就是以微小的步伐修改程式。如果你犯下錯誤,很容易便可發現它。

    感悟:細碎的步子前進,可以使得我們避免bug,在實際中,我們應當是以稍大的步驟來改進,當遇到問題時,撤銷我們的更改 轉而走向細碎的步子推進

  2. 原文:重構前,先檢查自己是否擁有一套可靠的測試集,這些測試必須有自我檢視能力。

    感悟:和鮑勃大叔在程式碼整潔之道(clean code)中的觀點一致,先編寫測試,才能再開發。重構亦如此

  3. 原文:一些重構手法也會顯著地影響效能。但即便如此,我通常也不去管它,繼續重構,因為有了一份結構良好的程式碼,回頭調優其效能也容易得多

    感悟:不要因為效能問題而不敢重構,一份好的程式碼再去調優是很容易的,更何況在現在各種快取、壓縮、瀏覽器的優化等加持下,真正影響效能的往往只是我們專案中的某一小塊程式碼

  4. 原文:每當感覺需要以註釋來說明點什麼的時候,我們就把需要說明的東西寫進一個獨立函式中,並以其用途(而非實現手法)命名。我們可以對一組甚至短短一行程式碼做這件事。哪怕替換後的函式呼叫動作比函式自身還長,只要函式名稱能夠解釋其用途,我們也該毫不猶豫地那麼做。關鍵不在於函式的長度,而在於函式“做什麼”和“如何做”之間的語義距離。

    感悟:一個最好的程式就是不需要任何註釋,自己本身就已經說明了程式的運轉流程,這和在初入行時,老師讓我們多寫註釋,最好每一行都寫的不太一樣,但也不算老師或者馬丁大叔說錯了,初入行時,我們什麼都不懂,經常編寫匪夷所思的程式碼,所以註釋很必要,但是更好的註釋是我們自身的程式碼。這個在很多程式碼規範中或者一些優秀的程式碼也都是這麼做的。

tips:好的程式碼中,註釋應該是短小精悍的,應該只是我們需要描述一些非常規做法的說明,或者隱患的說明以及我們某些時候冒出的一些對程式有用但沒有做的想法

  1. 原文:事實上,撰寫測試程式碼的最好時機是在開始動手編碼之前。當我需要新增特性時,我會先編寫相應的測試程式碼。聽起來離經叛道,其實不然。編寫測試程式碼其實就是在問自己:為了新增這個功能,我需要實現些什麼?編寫測試程式碼還能幫我把注意力集中於介面而非實現(這永遠是一件好事)。預先寫好的測試程式碼也為我的工作安上一個明確的結束標誌:一旦測試程式碼正常執行,工作就可以結束了

    感悟:編寫測試程式碼的最佳時機是在開始編寫之前,將業務最終效果編為測試程式碼,有利於我們明白我們為什麼要做這個功能,需要實現什麼樣的東西。在作者看來,不論是新增功能、修改功能、bug fix都應該測試先行。這是一種正確的思路,也是我們現在很多程式設計師中所缺少的。大部分人做的都是先寫功能,在寫測試。甚至一些公司連測試程式碼都沒有

注: 本書經典語句比較多,只挑選了其中我覺得感悟最深刻的五點寫出

兩頂帽子的概念

Kent Beck提出了“兩頂帽子”的比喻。使用重構技術開發軟體時,我把自己的時間分配給兩種截然不同的行為:新增新功能和重構。新增新功能時,我不應該修改既有程式碼,只管新增新功能。通過新增測試並讓測試正常執行,我可以衡量自己的工作進度。重構時我就不能再新增功能,只管調整程式碼的結構。此時我不應該新增任何測試(除非發現有先前遺漏的東西),只在絕對必要(用以處理介面變化)時才修改測試軟體開發過程中,我可能會發現自己經常變換帽子。首先我會嘗試新增新功能,然後會意識到:如果把程式結構改一下,功能的新增會容易得多,於是我換一頂帽子,做一會兒重構工作。程式結構調整好後,我又換上原先的帽子,繼續新增新功能。新功能正常工作後,我又發現自己的編碼造成程式難以理解,於是又換上重構帽子……整個過程或許只花10分鐘,但無論何時我都清楚自己戴的是哪一頂帽子,並且明白不同的帽子對程式設計狀態提出的不同要求。

  • [x] 時刻牢記自己正在做什麼,不要混在一起。這也是我自己所欠缺的東西

讀書內容詳解

重構的定義

  • [x] 所謂重構(refactoring)是這樣一個過程:在不改變程式碼外在行為的前提下,對程式碼做出修改,以改程式序的內部結構。重構是一種經千錘百煉形成的有條不紊的程式整理方法,可以最大限度地減小整理過程中引入錯誤的概率。本質上說,重構就是在程式碼寫好之後改進它的設計。

應該重構的原因

  1. 需求變化

    需求的變化使重構變得必要。如果一段程式碼能正常工作,並且不會再被修改,那麼完全可以不去重構它。能改進之當然很好,但若沒人需要去理解它,它就不會真正妨礙什麼。

  2. 新需求(預備性重構)

    重構的最佳時機在新增新功能之前,在動手新增新功能之前,我會看看現有的程式碼庫,此時經常會發現:如果對程式碼結構做一點微調,工作會變的容易很多(舊程式碼重構來擴充套件新功能)

  3. 幫助理解的重構

    我需要先理解程式碼在做什麼,然後才能著手修改。這段程式碼可能是我寫的,也可能是別人寫的。一旦我需要思考“這段程式碼到底在做什麼”,我就會自問:能不能重構這段程式碼,令其一目瞭然?我可能看見了一段結構糟糕的條件邏輯,也可能希望複用一個函式,但花費了幾分鐘才弄懂它到底在做什麼,因為它的函式命名實在是太糟糕了。這些都是重構的機會。

  4. 撿垃圾式重構

    當我在重構過程中或者開發過程中,發現某一塊不好,如果很容易修改可以順手修改,但如果很麻煩,我又有緊急事情時候,可以選擇記錄下來(但不代表我就一點都做不到把他變好)。就像野營者的老話:至少讓營地比你到達時更乾淨,久而久之,營地就非常乾淨(來自營地法則)

  5. 見機行事的重構

    重構經常發生在我們日常開發中,隨手可改的地方。當我們發現不好的味道,就要將他重構

  6. 長期的重構

    可以在一個團隊內,達成共識。當大家遇到時候,就改正它例如,如果想替換一個正在使用的庫,可以先引入一層新的抽象,使其相容新舊兩個庫的介面,然後一旦呼叫方完全改為了使用這層抽象,替換下面的庫就會如容易的多

  7. 複審程式碼(code review)時的重構

    開發者與審查者保持持續溝通,使得審查者能夠深入瞭解邏輯,使得開發者能充分認同複審者的修改意見(結對程式設計)

不知道何時該重構,那就遵循三次法則(來自書中)

Don Roberts給了我一條準則:第一次做某件事時只管去做;第二次做類似的事會產生反感,但無論如何還是可以去做;第三次再做類似的事,你就應該重構正如老話說的:事不過三,三則重構

重構的意義

  1. 改進軟體的設計(也可以說是增加程式的健壯、耐久)

    通過投入精力改善內部設計,我們增加了軟體的耐久性,從而可以更長時間地保持開發的快速

  2. 使得程式碼更容易理解

  3. 找到潛在bug

  4. 提高程式設計速度

重構的挑戰

  1. 延緩新功能開發

    實際上,這只是一部分不理解重構真正原因的人的想法,重構是為了從長效上見到收益,一段優秀的程式碼能讓我們開發起來更順手,要權衡好重構與新功能的時機,比如一段很少使用的程式碼。就沒必要對他重構

  2. 程式碼所有權

    有時候我們經常會遇到,介面釋出者與呼叫者不是同一個人,並且甚至可能是使用者與我們團隊的區別,在這種情況下,需要使用函式改名手法,重構新函式,並且保留舊的對外介面來呼叫新函式,並且標記為不推薦使用。

  3. 分支的差異

    經常會有長期不合並的分支,一旦存在時間過長,合併的可能性就越低,尤其是在重構時候,我們經常要對一些東西進行改名和變化,所以最好還是儘可能短的進行合併,這就要求我們儘可能的將功能顆粒化,如果遇到還沒開發完成且又無法細化的功能,我們可以使用特性開關對其隱藏

  4. 缺乏一組自測試的程式碼

    一組好的測試程式碼對重構很有意義,它能讓我們快速發現錯誤,雖然實現比較複雜,但他很有意義

  5. 遺留程式碼

    不可避免,一組別人的程式碼使得我們很煩惱,如果是一套沒有合理測試的程式碼則使得我們更加苦惱。這種情況下,我們需要增加測試,可以運用重構手法找到程式的接縫,再接縫處增加測試,雖然這可能有風險,但這是前進所必須要冒的風險,同時不建議一鼓作氣的把整個都改完,更傾向於能夠逐步地推進

何時不應該重構

  1. 不需要修改的程式碼
  2. 隱藏在一個API之下,只有當我需要理解其工作原理時,對其進行重構才有價值
  3. 重寫比重構還容易

重構與其他的關係

  1. 開發:短期會耽誤一定的開發事件,但從長期來看,重構使得新功能會更容易開發
  2. 效能:會影響部分效能,但在大多數的加持下,顯得微不足道,並且重構有利於效能優化的點集中於某一處或者幾處
  3. 架構:相輔相成
  4. 需求:需求推動重構前進

程式碼的壞味道

本書之中的核心之一:簡單來說就是碰到什麼樣子的程式碼,你就需要警惕起來,需要進行重構了!

本文章中主要分成三部分進行描述,第一部分為名字就是它的術語,第二部分為詳解:它的描述及一些實際場景,第三部分重構:就是他的參考重構手法,但這些手法僅作為參考,有時我們可能會需要更多的手法

  1. 神祕命名

    詳解:也包含那些隨意的abc或者漢語拼音,總之一切我們看不懂的、爛的都算,好的命名能節省我們很大的時間

    重構:改變函式宣告、變數改名、欄位改名

  2. 重複程式碼

    詳解:自然這個就好理解了,只要是我們看到兩段相似的語法都可以確定為這段程式碼可以提煉,通常提煉出來會更好,當然這個要看具體情況,個人感覺真的遇到那種只有兩處,且程式碼使用地方八杆子打不著,在程式碼穩定期間也不用浪費這個時間(這個時間不止體現在改動過程,也包括你可能因為改動導致的隱藏bug,尤其是系統核心模組,一旦出現問題只能馬上回滾,不會給你時間去找問題

    重構:提煉函式、移動語句、函式上移等手法

  3. 過長的函式

    描述:短小才是精悍!比如一些條件分支、一個函式做了很多事情、迴圈內的處理等等的都是應該重構的

    重構:提煉函式(常用)、以查詢取代臨時變數、引入引數物件、保持物件完整性、以命令取代引數(消除一些引數)、分解條件表示式、以多型取代條件表示式(應對分支語句)、拆分迴圈(應對一個迴圈做了很多事情)

  4. 過長的引數列表

    描述:正常來說,函式中所需的東西應該以引數形式傳入,避免全域性變數的使用,但過長的引數列表其實也很噁心。

    重構:查詢取代引數、保持物件完整、引入引數物件、移除標記引數、函式組合成類

  5. 全域性資料

    描述:最常見的就是全域性變數,但類變數與單例模式也有這樣的問題,我們通常無法保證專案啟動後不被修改,這就很容易造成詭異的bug,並且很難追查到

    重構:封裝變數

  6. 可變資料

    描述:資料的可變性和全域性變數一樣,如果我其他使用者修改了這個值,而引發不可理喻的bug。 這是很難排查的。

    重構:封裝變數,拆分變數,移動語句、提煉函式,查詢函式和修改函式分離,移除設值函式,以查詢取代變數函式組合成類

  7. 發散式變化

    描述:發散式變化是指某個模組經常因為不同的原因在不同的方向上變化了(可以理解為某一處修改了,造成其他模組方向錯亂)

    重構:拆分階段、搬移函式、提煉函式、提煉類

  8. 霰彈式修改

    描述:和發散式變化接近,卻又相反。我們每次修改一個功能或者新增一個功能都需要對多處進行修改;並且隨著功能增多我們可能還需要修改更多。 這樣程式時是很不健康的,其實我個人理解為:霰彈用來描述發散式變化更好,想想霰彈是一個點發射出去變成很多。而本條應該用另一個詞來描述更好,但我還想不到叫什麼詞。或許叫多路並進?僅限個人觀點,每個人理解可能不一樣,建議以作者為準

    重構:搬移函式、搬移欄位、函式組合成類、函式組合成變換、拆分階段、行內函數、內聯欄位

  9. 依戀情結

    描述:一個模組內的一部分頻繁的和外面的模組進行互動溝通,甚至超過了它與內部的溝通。也就是違反了高內聚低耦合,遇到這種的“叛亂者”,不如就讓他去他想去的地方吧

    重構:搬移函式、提煉函式

  10. 資料泥團

    描述:雜合纏繞在一起的。程式碼中也如是,我們可能經常看到三四個相同的資料,兩個類中相同欄位等等。總之像泥一樣,這裡也是這樣那裡也是這樣,就是他了

    重構:提煉類、引入引數物件、保持物件完整性

  11. 基本型別偏執

    描述:一些基本型別無法表示一個資料的真實意義,例如電話號碼、溫度等,

    重構:以物件取代基本型別、以子類取代型別碼、以多型取代條件表示式

  12. 重複的switch

    描述:不只是switch,大片相聯的if也應該包含在內,甚至在古老的前端時代,曾經一度無條件反對這樣的寫法。

    重構:多型取代條件表示式

  13. 迴圈語句

    描述:在js中體現為傳統的for類迴圈

    重構:用管道來取代迴圈(管道:map、forEach、reduce、filter等一系列)

  14. 冗贅的元素

    描述:元素指類和函式,但是這些元素可能因為種種原因,導致函式過於小,導致沒有什麼作用,以及那些重複的,都可以算作冗贅

    重構:行內函數、內聯類、摺疊繼承類

  15. 誇誇其談通用性

    描述:為了將來某種需求而實現的某些特殊的處理,但其實可能導致程式難以維護難以理解,直白來說就是沒個錘子用的玩意,你留下他幹個屁

    重構:摺疊繼承體系、行內函數、內聯類、改變函式宣告、移除死程式碼

  16. 臨時欄位

    描述:那些本身就足以說明自己是誰的,不需要名字來描述的

    重構:提煉類、提煉函式、引入特例

  17. 過長的訊息鏈

    描述:一個物件請求另一個物件,然後再向後者請求另一個物件,然後再請求另一個物件……這就是訊息鏈,舉個例子來說

    new Car().properties.bodyWork.material.composition().start()
    

    這意味著在查詢過程中,很多的類耦合在一起。個人認為,不僅是結構的耦合,也很難理解。這也包含某類人jq的那一大串的連續呼叫。都是很難讓人理解的。

    重構: 隱藏委託關係、提煉函式、搬移函式

  18. 中間人

    描述:如果一個類有大部分的介面(函式)委託給了同一個呼叫類。當過度運用這種封裝就是一種程式碼的壞味道

    重構:移除中間人、行內函數

  19. 內幕交易

    描述:兩個模組的資料頻繁的私下交換資料(可以理解為在程式的不為人知的角落),這樣會導致兩個模組耦合嚴重,並且資料交換隱藏在內部,不易被察覺

    重構:搬移函式、隱藏委託關係、委託取代子類、委託取代超類

  20. 過大的類

    描述:單個類做了過多的事情,其內部往往會出現太多欄位,一旦如此,重複程式碼也就接踵而至。這也意味著這個類絕不只是在為一個行為負責

    重構:提煉超類、以子類取代型別碼

  21. 異曲同工的類

    描述:兩個可以相互替換的類,只有當介面一致才可能被替換

    重構:改變函式宣告、搬移函式、提煉超類

  22. 純資料類

    描述:擁有一些欄位以及用於讀寫的函式,除此之外一無是處的類,一般這樣的類往往半一定被其他類頻繁的呼叫(如果是不可修改欄位的類,不在此列,不可修改的欄位無需封裝,直接通過欄位取值即可),這樣的類往往是我們沒有把呼叫的行為封裝進來,將行為封裝進來這種情況就能得到很大改善。

    重構:封裝記錄、移除取值函式、搬移函式、提煉函式、拆分階段

  23. 被拒絕的遺贈

    描述:這種味道比較奇怪,說的是繼承中,子類不想或不需要繼承某一些介面,我們可以用函式下移或者欄位下移來解決,但不值得每次都這麼做,只有當子類複用了超類的行為卻又不願意支援超類的介面時候我們才應該做出重構

    重構:委託取代子類、委託取代超類

  24. 註釋

    描述:這裡提到註釋並非是說註釋是一種壞味道,只是有一些人經常將註釋當作“除臭劑”來使用(一段很長的程式碼+一個很長的註釋,來幫助解釋)。往往遇到這種情況,就意味著:我們需要重構了

    重構:提煉函式、改變函式宣告、引入斷言

重構手法介紹

如果說上面的味道是核心的話,那手法應該就是本書的重中之重。通常我們發現哪裡味道不對之後,就要選擇使用不同的手法進行重構。將他們變得味道好起來。

本文中每個手法通常包含三個模組:時機(遇到什麼情況下使用)、做法(詳細步驟的概括)、關鍵字(做法的縮影)

提煉函式

  • 時機:
  1. 當我們覺得一段大函式內某一部分程式碼在做的事情是同一件事,並且自成體系,不與其他摻雜時
  2. 當程式碼展示的意圖和真正想做的事情不是同一件時候,如作者提到的例子。想要高亮,程式碼意思為反色,這樣就不容易讓人誤解,印證了作者前面說的:當你需要寫一行註釋時候,就適合重構了
  • 做法:
  1. 一個以他要做什麼事情來命名的函式
  2. 待提煉程式碼複製到這個函式
  3. 檢查這個函式內的程式碼的作用域、變數
  4. 編譯檢視函式內有沒有報錯(js可以通過eslint協助)
  5. 替換源函式的被提煉程式碼替換為函式呼叫
  6. 測試
  7. 替換其他程式碼中是否有與被提煉的程式碼相同或相似之處
  • 關鍵字:

    新函式、拷貝、檢查、作用域/上下文、編譯、替換、修改細節

行內函數

  • 時機:

    1. 函式內程式碼直觀表達的意思與函式名字相同
    2. 有一堆雜亂無章的程式碼需要重構,可以先行內函數,再通過提煉函式合理重構
    3. 非多型性函式(函式屬於一個類,而這個類被繼承)
  • 做法:

    1. 檢查多型性(如果該函式屬於某個超類,並且它具有多型性,那麼就無法內聯)
    2. 找到所有呼叫點
    3. 將函式所有呼叫點替換為函式本體(非一次性替換,可以分批次替換、適應新家、測試)
    4. 刪掉該函式的定義(也可能會不刪除,比如我們放棄了有一些函式呼叫,因為重構為漸進式,非一次性)
  • 關鍵字:

    檢查多型、找呼叫並替換、刪除定義

提煉變數

  • 時機:
  1. 一段又臭又長的表示式
  2. 在多處地方使用這個值(可能是當前函式、當前類乃至於更大的如全域性作用域)
  • 做法:
  1. 確保要提煉的表示式,對其他地方沒有影響
  2. 宣告一個不可修改的變數,並用表示式作為該變數的值
  3. 用新變數取代原來的表示式
  4. 測試
  5. 交替使用3、4
  • 關鍵字:

副作用、不可修改的變數、賦值、替換

內聯變數

  • 時機:
  1. 變數沒有比當前表示式有什麼更好的釋義
  2. 變數妨礙了重構附近程式碼
  • 做法:
  1. 檢查確認變數賦值的右側表示式不對其他地方造成影響
  2. 確認是否為只讀,如果沒有宣告只讀,則要先讓他只讀,並測試
  3. 找到使用變數的地方,直接改為右側表示式
  4. 測試
  5. 交替使用3、4
  • 關鍵字

副作用、只讀、替換變數

改變函式宣告

最好能把大的修改拆成小的步驟,所以如果你既想修改函式名,又想新增引數最好分成兩步來做。
不論何時,如果遇到了麻煩,請撤銷修改,並改用遷移式做法)
  • 時機:
  1. 函式名字不夠貼切函式所做的事情
  2. 函式引數增加
  3. 函式引數減少
  4. 函式引數概念發生變化
  5. 函式因為某個引數導致的函式應用範圍小(全域性有很多類似的函式,在做著類似的事情)
  • 做法(適用於確定了函式或者引數只在有限的小範圍內使用,並且僅僅改名)
  1. 先確定函式體內有沒有使用這個引數(針對於引數)
  2. 確定函式呼叫者(針對於函式)
  3. 修改函式/引數的宣告,使其達到我們想要的效果
  4. 找到所有的函式/引數宣告的地方將其改名
  5. 找到所有函式/引數呼叫的地方將其替換
  • 關鍵字

使用變數者、函式呼叫者、修改函式、宣告改名、呼叫替換

  • 做法(標準化做法)
  1. 對函式內部進行重構(如果有必要的話)
  2. 使用提煉函式手法,將函式體提煉成一個新函式,同名的話,可以改為一個暫時的易於搜尋的隨意名字(如:aaa_getData,只要好搜尋且唯一即可。),非同名的話,使用我們想要的名字作為新函式名字
  3. 在新函式內做我們的變更(新增引數、刪除引數、改變引數釋義等)
  4. 改變函式呼叫的地方(如果是新增、修改、刪除引數)
  5. 測試
  6. 對舊函式使用行內函數來呼叫或返回新函式
  7. 如果使用了臨時名字,使用改變函式宣告將其改回原來的名字(這時候就要刪除舊函式了)
  8. 測試
  • 關鍵字:

內部重構、提煉新函式、好搜尋的臨時名字、變更、改變呼叫、舊函式使用新函式、改變呼叫名字

封裝變數

  • 時機:
  1. 當我們在修改或者增加使用可變資料的時候
  2. 資料被大範圍使用(設定值)
  3. 物件、陣列無外部變動需要內部一起改變的需求時候,最好返回一份副本
  • 做法:
  1. 建立封裝函式(包含訪問和更新函式)
  2. 修改獲取這個變數和更新這個變數的地方
  3. 測試
  4. 控制變數外部不可見(可以藉助es6類中的get來實現不可變數以及限制可見)
  5. 測試
  • 關鍵字:

新函式、替換呼叫、不可見

變數改名

  • 時機:
  1. 變數/常量的名字不足以說明欄位的意義
  2. 垃圾命名
  • 做法:
  1. 針對廣泛使用的

    1.1 先用封裝變數手法封裝

    1.2 找到所有使用該變數的程式碼,修改測試(如果是對外已釋出的變數,可以標記為不建議使用(作者沒提到,但是個人感覺是可以這樣的)

    1.3 測試

  2. 只作用於某個函式的直接替換即可

  3. 替換過程中可以以新名字作為過渡。待全部替換完畢再刪除舊的名字

  • 關鍵字:

封裝變數手法、替換名字、中間過渡

引入引數物件

  • 時機:
  1. 一組引數總在一起出現
  2. 函式引數過多
  • 做法:
  1. 建立一個合適的資料結構(如果已經有了,可以略過)

    資料結構選擇:一種是以物件的形式,一種是以類的形式,作者推薦以類的形式,但是在我看來,要根據場景,如果這組資料以及其相關行為可以變為一組方法,如陣列類裡面的比較兩個陣列是否完全一致,這就可以以類來宣告(js中也可以以export來匯出而使用)

  2. 使用改變函式宣告手法給原函式增加一個引數為我們新的結構

  3. 測試

  4. 舊資料中的引數傳到新資料結構(變更呼叫方)

  5. 刪除一項舊引數,並將之使用替換為新引數結構

  6. 測試

  7. 重複5、6

  • 關鍵字:

新結構、增加引數、入參新結構、刪除舊引數、使用新結構

函式組合成類

  • 時機:
  1. 一組函式(行為)總是圍繞一組資料做事情
  2. 客戶端有許多基於基礎資料計算派生資料的需求
  3. 一組函式可以自成一個派系,而放在其他地方總是顯得不夠完美
  • 做法:
  1. 如果這一組資料還未做封裝,則使用引入引數物件手法對其封裝
  2. 運用封裝記錄手法將資料記錄封裝成資料類
  3. 使用搬移函式手法將已有的函式加入類(如果遇到引數為新類的成員,則一併替換為使用新類的成員)
  4. 替換客戶端的呼叫
  5. 將處理資料記錄的邏輯運用提煉函式手法提煉出來,並轉為不可變的計算資料
  • 關鍵字:

提煉變數、封裝成類、移入已有函式、替換呼叫、移入計算資料

函式組合成變換

  • 時機:
  1. 函式組合成變換手法時機等同於組合成類的手法,區別在於其他地方是否需要對源資料做更新操作。 如果需要更新則使用類,不需要則使用變換,js中推薦類的方式
  • 做法:
  1. 宣告一個變換函式(工廠函式)
  2. 引數為需要做變換的資料(需要deepclone)
  3. 計算邏輯移入變換函式內(比較複雜的可以使用提煉函式手法做個過渡)
  4. 測試
  5. 重複3、4
  • 關鍵字:

變換函式、變換入參、搬移計算邏輯

封裝記錄

  • 時機:
  1. 可變的記錄型結構
  2. 一條記錄上有多少欄位不夠直觀
  3. 有需要對記錄進行控制的需求(個人理解為需要控制許可權、需要控制是否只讀等情況)
  4. 需要對結構內欄位進行隱藏
  • 做法:
  1. 首先用封裝變數手法將記錄轉化為函式(舊的值的函式)
  2. 宣告一個新的類以及獲取他的函式
  3. 找到記錄的使用點,在類內宣告設定方法
  4. 替換設定值的方法(es6 set)
  5. 宣告一個取值方法,並替換所有取值的地方
  6. 測試
  7. 刪除舊的函式
  8. 當我們需要改名時,可以保留老的,標記為不建議使用,並宣告新的名字進行返回
  • 關鍵字:

轉化函式、取值函式、設值函式、替換呼叫者、替換設定者

以物件取代基本型別

  • 時機:
  1. 隨著開發迭代,我們一個簡單的值已經不僅僅只是簡單的值那麼簡單了,他可能還要肩負一些其他的職責,如比較、值行為等
  2. 一些關鍵的、非僅僅只有列印的功能的值
  • 做法:
  1. 如果沒被封裝,先使用封裝變數手法
  2. 為要修改的資料值建立一個物件,併為他提供取值、設值函式(看需求)
  3. 使用者(可能是另外一個大類)修改其取值設值函式
  4. 測試
  5. 修改大類中的取值設值函式的名稱,使其更好的語義化
  6. 為這個新類增加其行為(可能是轉換函式、比較函式、特殊處理函式、操作函式)等
  7. 根據實際需求對新類進行行為擴充套件(如果有必要的話)
  8. 修改外部客戶端的使用
  • 關鍵字:

新類、取設值函式、行為入類、擴充套件類

以查詢取代臨時變數

  • 時機:
  1. 修改物件最好是一個類(這也是為什麼提倡class,因為類可以開闢一個名稱空間,不至於有太多全域性變數)
  2. 有很多函式都在將同一個值作為引數傳遞
  3. 分解過長的冗餘函式
  4. 多個函式中重複編寫計算邏輯,比如講一個值進行轉換(好幾個函式內都需要這個轉換函式)
  5. 如果這個值被多次修改,應該將這些計算程式碼一併提煉到取值函式
  • 做法:
  1. 檢查是否每次計算過程和結果都一致(不一致則放棄)
  2. 如果能改為只讀,就改成只讀
  3. 將變數賦值取值提煉成函式
  4. 測試
  5. 去掉臨時變數
  • 關鍵字:

只讀、提煉函式、刪變數

提煉類

  • 時機:
  1. 一個大的類在處理多個不同的事情(這個類不純潔了)
  • 做法:
  1. 確定分出去的部分要做什麼事情
  2. 建立一個新的類,表示從舊地方分離出來的責任
  3. 舊類建立時,為新類初始化
  4. 使用搬移函式手法將需要的方法搬移到新的類(搬移函式時候就將呼叫地方改名)
  5. 刪除多餘的介面函式,併為新類的介面取一個適合自己的名字
  6. 考慮是否將新的類開放為公共類
  • 關鍵字:

職責邊界確認、建立新域、新舊同步初始化、行為搬家、介面刪除

內聯類

  • 時機:
  1. 一個曾經有很多功能的類,在重構過程中,已經變成一個毫無單獨職責的類
  2. 需要對兩個類重新進行職責劃分
  • 做法:
  1. 將需要內聯的類中的所有對外可呼叫函式(也可能是欄位)在目標類中新建一個對應的中間代理函式
  2. 修改呼叫者,呼叫代理方法並測試
  3. 將原函式中的相關方法(欄位)搬移到新地方並測試
  4. 原類變為空殼後就可以刪除了
  • 關鍵字:

代理、修改呼叫者、方法搬家、拋棄舊類

隱藏委託關係

  • 時機:
  1. 一個類需要隱藏其背後的類的方法或事件
  2. 一個客戶端呼叫類的方法時候,必須知道隱藏在後面的委託關係才能呼叫
  • 做法:
  1. 在服務類(對外的類)中新建一個委託函式,讓其呼叫受託類(背後的類)的相關方法
  2. 修改所有客戶端呼叫為這個委託函式
  3. 重複12直到受託類全部被搬移完畢,移除服務類中返回受託類的函式
  • 關鍵字:

委託函式、替換呼叫者、刪除委託整個類

移除中間人

  • 時機:
  1. 因為隱藏委託關係(當初可能是比較適合隱藏的)手法造成的現在轉發函式越來越多
  2. 過度的迪米特法則造成的轉發函式越來越多
  • 做法:
  1. 在服務類(對外)內為受託物件(背後的類)建立一個返回整個委託物件的函式
  2. 客戶端的呼叫轉為連續的訪問函式進行呼叫
  3. 刪除原本的中間代理函式
  • 關鍵字:

委託整個類、修改呼叫、刪除代理

替換演算法

  • 時機:
  1. 舊演算法已經不滿足當前功能
  2. 有更好的方式可以完成與舊演算法相同的事情(通常是因為優化)
  • 做法:
  1. 保證待替換的演算法為單獨的封裝,否則先將其封裝
  2. 準備好更好的演算法,
  3. 替換演算法過去
  4. 執行並測試新演算法與舊演算法對比(一定要對比,也許你選的還不如以前呢)
  • 關鍵字:

演算法封裝、編寫新演算法、替換演算法、比較演算法

搬移函式

  • 時機:
  1. 隨著對專案(模組)的認知過程中,也可能是改造過程中,一些函式已經脫離了當前模組的範圍
  2. 一個模組內的一些函式頻繁的與其他模組互動,卻很少和自身內部進行互動(出現了叛變者)
  3. 一個函式在發展過程中,現在他已經有了更通用的場景
  • 做法:
  1. 查詢要搬移的函式在當前上下文中引用的所有元素(先將依賴最少的元素進行搬離)
  2. 考慮待搬移函式是否具有多型性(複寫了超類的函式或者被子類重寫)
  3. 複製函式到目標上下文,調整函式,適應新的上下文
  4. 函式內使用的變數考慮是一起搬移還是以引數傳遞
  5. 改寫原函式為代理函式(也可以內聯)
  6. 檢查新函式是否可以繼續進行搬離
  • 關鍵字:

確定關係、確定繼承、優先基礎、函式搬家、相關部分位置確定、原址代理、優化新函式

搬移欄位

  • 時機:
  1. 隨著業務推進過程中,原有的資料結構已經不能很好的表示程式的邏輯
  2. 每當呼叫一個函式時,需要傳入的記錄引數,總是需要傳入另一條記錄或者他的某些欄位一起
  3. 修改(行為)一條記錄時,總是需要同時改動其他記錄
  4. 更新(資料)一條欄位時,總是需要同時在多個結構中作出修改
  • 做法:
  1. 源欄位已經被封裝(如果未封裝,則應該先使用封裝變數手法對其封裝)
  2. 目標物件上建立一個欄位,及其訪問函式
  3. 源物件對目標物件的欄位做對應的代理
  4. 調整源物件的訪問函式,令其使用目標物件的欄位
  5. 測試
  6. 移除源物件的欄位
  7. 視情況而定決定是否需要內聯變數訪問函式
  • 關鍵字:

封裝、新欄位、源址代理、代理新址、舊欄位移除、確定是否內聯

搬移語句到函式

  • 時機:
  1. 重複程式碼
  2. 每次呼叫a方法時,b操作也總是每次都執行
  3. 某些語句放在特定函式內更像一個整體
  • 做法:
  1. 將重複程式碼使用搬移函式手法到緊鄰目標函式的位置
  2. 如果目標函式緊被唯一一個原函式呼叫,則只需要將原函式的重複片段貼上到目標函式即可
  3. 選擇一個呼叫點進行提煉函式,將目標語句函式與語句提煉成一個新的函式
  4. 修改函式其他呼叫點,令他們呼叫新提煉的函式
  5. 調整函式的引用點
  6. 行內函數手法將目標函式內聯到新函式裡
  7. 移除原目標函式
  8. 對新函式應用函式改名手法(改變函式宣告的簡單做法)
  • 關鍵字:

程式碼靠近、單點提煉、中間函式、修改引用、函式內聯、原函式刪除、函式改名

函式搬移到呼叫者

  • 時機:
  1. 隨著系統前進過程中,函式某一塊的作用發生改變,不再適合原函式位置
  2. 之前在多個地方表現一致的行為,如今在不同呼叫點面前表現了不同的行為

tips: 本手法只適合邊界有些許偏移的場景,不適合相差較大的場景

  • 做法:
  1. 簡單情況下,直接剪下
  2. 將不想搬移的部分提煉成與當前函式同級函式(如果是超類方法,子類也要一起提煉)
  3. 原函式呼叫新的同級函式
  4. 替換呼叫點為新的同級函式和要內聯的語句
  5. 刪除原函式
  6. 使用函式改名手法(改變函式宣告的簡單做法)改回名字
  • 關鍵字:

提煉不變的為臨時方法、搬移語句、刪除原,改名字

以函式呼叫替換內聯程式碼

  • 時機:
  1. 函式內做的某些事情與已有函式重複
  2. 已有函式與函式之間希望同步變更
  • 做法:
  1. 內聯程式碼替換為函式(可能有引數,就要對應傳遞)
  • 關鍵字:

內聯替換

移動語句

  • 時機:
  1. 移動語句一般用於整合相關邏輯程式碼到一處,這是其他部分手法的基礎
  2. 程式碼相關邏輯整合一處方便我們對這部分程式碼優化和重構
  • 做法:
  1. 確定要移動的語句要移動到哪(調整的目標是什麼、該目標能否達到)
  2. 確定要移動的語句是否搬移後會使得程式碼不能正常工作,如果是,則放棄
  • 關鍵字:

確定副作用、確定目標

拆分迴圈

  • 時機:
  1. 一個迴圈做了多件不相干事
  • 做法:
  1. 複製迴圈
  2. 如果有副作用則刪除單個迴圈內的重複片段
  3. 提煉函式
  4. 優化內部
  • 關鍵字:

複製迴圈、行為拆分、函式提煉

以管道替代迴圈

  • 時機:
  1. 一組雖然在做相同事情的迴圈,但是內部過多的處理邏輯,使其晦澀難懂
  2. 不合適的管道(如過濾使用some)
  • 做法:
  1. 建立一個新變數,用來存放每次行為處理後,參與迴圈的剩餘集合
  2. 選用合適的管道,將每一次迴圈的行為進行搬移
  3. 搬移完所有的迴圈行為,刪除整個迴圈
  • 關鍵字:

新變數、合適的管道、刪除整個迴圈

移除死程式碼

  • 時機:
  1. 程式碼隨著迭代已經變得沒用了。
  2. 即使這段程式碼將來很有可能還會使用,那也應該移除,畢竟現在版本控制很實用。
  • 做法:
  1. 如果不可以外部引用,則放心刪除(如果可能將來極有可能會啟用,在這裡留下一行註釋,標示曾經有過這段程式碼,以及它被刪除的那個提交的版本號)
    2、如果外部引用了,則需要仔細確認還有沒有其他呼叫點(有eslint規則限制的話。其實可以先刪了,看有沒有報錯)
  • 關鍵字:

檢查引用

拆分變數

  • 時機:
  1. 一個變數被應用到兩種/多種的作用下
  2. 修改輸入引數的值
  • 做法:
  1. 在變數第一次賦值的地方,為函式取一個更加有意義的變數名(儘量宣告為const)
  2. 在第二次賦值地方宣告該變數
  3. 以該變數第二次賦值動作為界,修改此前對該變數的所有引用。讓他們引用新的變數
  4. 測試
  5. 重複上述,直到變數拆分完畢
  • 關鍵字:

新變數、賦值時宣告、替換呼叫

欄位改名

  • 時機:
  1. 記錄結構中的欄位需要改個名字
  • 做法:
  1. 如果結構簡單,可以一次性替換
  2. 如果記錄沒有封裝,最好是先封裝記錄
  3. 修改構造時候做相容判斷(老的值與新的值相容判斷:this.a = data.a || data.b)
  4. 修改內部設取值函式
  5. 修改記錄資料類中的內部呼叫
  6. 測試
  7. 修改外部呼叫初始化時候的資料
  8. 刪除初始化相容判斷
  9. 使用函式改名手法(改變函式宣告的簡單做法),修改呼叫處的呼叫方式及內部取設值函式為新欄位名
  • 關鍵字:

封裝、相容初始化、內部取設只返回新欄位,修改內部呼叫,測試、刪除相容、內部取設改名、替換外部呼叫

以查詢取代派生變數

  • 時機:
  1. 兩個變數相互耦合
  2. 設定一個變數的同時,將另一個變數與該變數結合,通過計算後給另一個變數設定值

tips:計算的參考變數,是不可變的,計算結果也是不可變的。可以不重構(還是那句話,不可變的資料,我們就沒必要理他)

  • 做法:
  1. 確定可以引起變數發生變化的所有點(如果有來自其他模組變數,需要先用拆分變數手法
  2. 新建一個計算函式,計算變數值
  3. 引入斷言(assert),確保計算函式的值與該變數結果相同
  4. 測試
  5. 修改讀取變數的程式碼,用行內函數手法將計算函式內聯進來)
  6. 移除死程式碼手法將舊的更新點的地方清理掉
  • 關鍵字:

來源確定、結果相同、計算函式、清理更新點

將引用物件改為值物件

  • 時機:
  1. 幾個物件中共享了一個物件,並且要聯動變更的情況下
  2. 值物件就是每次設定都直接設定這個值,比如:
    值物件:a.b=new b(1)
    引用物件:a.b.c=1
  • 做法:
  1. 檢查重構的目標是否為不可變物件,如果不是的話,則看看是否可以將其改為不可變物件
  2. 移除設值函式手法去掉第一個設引用值函式(每次都用設定值的方式複寫整個物件)
  3. 測試
  4. 重複2、3
  5. 判斷兩次相同輸入時候,值是否相等
  • 關鍵字:

不可變、替換設定引用值為設定值

將值物件改為引用物件

  • 時機:
  1. 資料副本在多處使用,並且需要一處變化其他地方同步更新
  • 做法:
  1. 建立一個倉庫(如果沒有的話),倉庫要支援:每次訪問相同資料都是一個相同的引用物件、支援註冊新資料和獲取同一個引用資料(js可以在簡單場景下簡單的使用{})
  2. 確保倉庫的建構函式有辦法找到關聯物件的正確例項
  3. 修改呼叫點,令其從倉庫獲取關聯物件。
  4. 測試
  • 關鍵字:

共享倉庫、單例的引用物件、替換呼叫點

分解條件表示式

  • 時機:
  1. 條件邏輯內,過長的函式,導致反而難以理解條件邏輯的場景
  2. 單個條件邏輯處理的函式過大
  • 做法:
  1. 對條件判斷的每個分支分別運用提煉函式手法
  2. 如果條件表示式過長,對條件表示式運用提煉函式手法
  3. 優化當前條件邏輯(如使用三元表示式)
  • 關鍵字:

提煉分支、提煉條件、優化判斷

合併條件表示式

  • 時機:
  1. 無其他副作用的巢狀if
  2. 無其他副作用的,且返回一致的並列if
  3. 這些if都是關聯的(可以用是否能提煉出一個合適的函式名來作為依據,但也不是絕對,我們可以選擇不提煉函式,但是還是建議是相關的if作為一組)
  • 做法:
  1. 確定條件表示式有副作用,先用將查詢函式和修改函式分離的手法對其處理
    2、如果是巢狀函式一般是用邏輯與合併,如果是並列的if一般是用邏輯或合併,如果兩種均有,就要組合使用了(但是我更建議他們應該分離成多個判斷)
    3、測試
  2. 重複2、3
  3. 對合並後的條件表示式進行提煉函式手法(有必要的話)
  • 關鍵字:

分離副作用、合適的邏輯符、提煉條件函式

以衛語句取代巢狀表示式

  • 時機:
  1. 無其他副作用的巢狀if
  2. 無其他副作用的,且返回一致的並列if
  3. 這些if都是關聯的(可以用是否能提煉出一個合適的函式名來作為依據,但也不是絕對,我們可以選擇不提煉函式,但是還是建議是相關的if作為一組)
  • 做法:
  1. 選取最外層需要被替換的條件邏輯,將其替換為衛語句(單獨檢查條件、並在條件為真時立刻返回的語句,叫做衛語句)
  2. 測試
  3. 重複1、2
  • 關鍵字:

從外而內

以多型取代條件表示式

  • 時機:
  1. 多種並列或者巢狀的條件邏輯,讓人難以理解
  2. switch
  3. 同行為不同型別的判斷
  • 做法:
  1. 確定現有的條件類是否具有多型性,如果沒有,可以通過將行為封裝成類(藉助其他手法如函式組合成類等)

  2. 在呼叫方使用工廠函式獲得行為物件的例項

  3. 針對不同型別建立子類(相當於在超類在分化)

  4. 呼叫方此時應當通過一個工廠返回合適的子類

  5. 將超類中針對子類型別所做的判斷,逐一移入對應子類進行復寫(相關子類複寫超類的分支函式),超類只留下預設值

注意:這種手法其實是在物件導向開發中很常用的一種方式,但是如果不是

  1. 在寫一個物件導向很明確的專案
  2. 這個判斷過於大
  3. 可以明確這些子類抽取出來是有意義的(從後期維護角度來說,需要對其增加一些行為)
  4. 這個子類可以自成體系

不如將其通過一個json或者map來進行指責劃分。在js中我覺得更常用的是以策略來代替if

  • 關鍵字:

多型、繼承、封裝、行為拆分

引入特例

  • 時機:
  1. 資料結構的呼叫者都在檢查某個特殊值,並且這個值每次所做的處理也都相同

  2. 多處以同樣方式應對同一個特殊值

三種情況
第一種原始為類,特例元素沒有設定值的操作
第二種原始為類,特例元素有設定值的操作
第三種 原始就是普通的json

  • 做法:

針對於有自己對應行為的類

  1. 在原類中為特例元素增加一個函式,用以標記這個特例的情況,預設返回一個寫死的就行)

  2. 為特例建立一個class,用以處理特例的正常邏輯和行為,需要把特例物件及其所有行為放到這個類

  3. 將本次特例的條件使用提煉函式手法抽成一個在類中的欄位函式返回true

  4. 修改所有呼叫者為第3步的函式

  5. 修改第一步建立的類。讓它返回我們的特例物件

  6. 特例中的其他欄位

針對於只讀的類

  1. 將上面做法的建立一個b類改為在類內建立一個函式,返回物件即可。把特例所需資訊全部返回在js

針對於原始不是類的

  1. 為特例物件建立一個函式,返回特例物件的深拷貝狀態

  2. 將本次特例的條件使用提煉函式手法抽成一個統一的函式

  3. 對第一步建立的函式返回值做特殊增強。 將需要的特例的值,逐一放進來。

  4. 替換呼叫者使用函式的返回值

  • 關鍵字:

特例邏輯搬到class、過渡函式、替換呼叫者、修改新class

將查詢函式和修改函式分離

  • 時機:
  1. 一個函式既有返回值又有設定值
  • 做法:
  1. 複製一份目標函式並改名為查詢函式的名字
  2. 將被複制的函式刪除設定值的程式碼
  3. 將呼叫者替換為新函式,並在下面呼叫原函式
  4. 刪除原函式返回值
  5. 將原函式和新函式中的相同程式碼進行優化
  • 關鍵字:

新函式為查詢、刪除設定值、替換呼叫者、刪除返回值、優化

函式引數化

  • 時機:
  1. 有多餘一個函式的邏輯非常相似,只是有一些字面量不同(有時候可能會碰到a、b很相似,a、c也很相似,但是b、c差距比較大時候,這種情況個人觀點為:將ab、ac中邏輯緊密的抽成一個,不要形式化的就要吧abc抽到一起。反而適得其反)
  • 做法:
  1. 從這一組相似函式中,找到一組,通常來說盡可能選擇呼叫比較少的地方
  2. 運用改變函式宣告手法(改變引數)使其在呼叫時候,將變化的部分以引數形式傳入)
  3. 修改當前這個函式的所有呼叫點,為呼叫新函式,並傳遞引數
  4. 修改新函式,讓它使用新傳進來的引數
  5. 將其他相似的函式,逐一替換為這個新函式,每次替換都要測試一下
  • 關鍵字:

呼叫較少、變化點入參、修改呼叫、替換使用

移除標記引數

  • 時機:
  1. 一個用來控制函式流程的引數
  • 做法:
  1. 針對引數的每一種可能值,新建一個明確函式(如果引數控制整個流程,則可以用分解條件表示式手法建立明確函式,如果只控制一部分函式則建立轉發函式,將這些函式,統一通過這些明確函式進行轉發)

  2. 替換呼叫者

tips:如果是這個標記即作為標記,又作為引數值。則對其進行拆分。

  • 關鍵字:

流程、行為拆分

保證物件完整的手法

  • 時機:
  1. 從一個程式碼中匯出幾個值
  2. 呼叫者將自身的部分引數傳遞
  3. 一般發生在引入引數物件手法之後
  • 做法:
  1. 新建一個空函式(可能是新建,也可能是用提煉函式),接受完整物件
  2. 新函式體內呼叫舊函式,並且使用合適的引數列表
  3. 修改舊函式的呼叫者,令他使用新函式,修改舊函式內部
  4. 使用行內函數手法將舊函式內程式碼搬移到新建的函式
    5、修改新函式的名字為舊函式
  • 關鍵字:

接受完整物件、新呼叫老、修改呼叫、內聯、改名

以查詢取代引數

  • 時機:
  1. 一個函式傳入了多個相同的值(如總是能根據b引數不需要很複雜就可以查到a引數)
  2. 呼叫函式傳入了一個函式本身就可以很容易獲得的引數(指的是內部或者計算獲得,而非從其他模組拿)
  3. 如果目標函式本身就具有引用透明性(函式的返回值只依賴於其輸入值),用查詢後,他去訪問了一個全域性變數,則不適合用本重構

一言以概之:這個函式自身或者通過引數都能得到另一個值就可以使用這個手法

  • 做法:
  1. 如果有必要,可以將引數計算的過程提煉為一個只讀變數或者一個函式
    2、將函式體內引用該引數的地方,都改為運用計算函式
    3、去掉該引數(呼叫者也要去掉)
  • 關鍵字:

提煉變數、引數消除

以引數取代查詢

  • 時機:
  1. 一個函式內部因為引用了全域性變數而導致了不透明
  2. 一個函式內部引用了一個即將被刪除的元素
  3. 一個函式內部,過多的依賴了另一個模組(這種有兩種做法:一種是本手法,另一種是搬移函式手法,要根據函式實際作用操作
  • 做法:
  1. 使用提煉變數手法將目標(希望作為引數傳入的查詢)提煉出來
  2. 把整個函式體提煉,並且單獨放到一個函式內(需要保留計算邏輯,計算邏輯作為代理函式每次的值以引數傳入函式)
  3. 消除剛才提煉出來的變數(舊函式應該只剩下一個簡單的呼叫)
  4. 修改呼叫方,改為呼叫新函式,並傳入呼叫時候計算的計算值
  5. 刪除原函式內的計算代理
  6. 新函式改回舊函式的名字(如果意義發生變化,需要重新起名字)
  • 關鍵字:

變數提煉、函式體換新、舊函式傳參、舊函式調新函式,刪除代理函式、函式改名

移除設值函式

  • 時機:
  1. 類內某個欄位有一些設值函式
  2. 類無任何副作用(如:操作渲染html的append、往localstorage寫東西、init呼叫介面、多處共享引用等)
  3. 很龐大的類(需要先作拆分優化)
  • 做法:
  1. 如果無法拿到設定變化的值,就通過建構函式的引數傳入
  2. 在建構函式內部呼叫設值函式進行更新
  3. 移除所有的設定值的函式呼叫,改為new一個類
  4. 使用行內函數手法消除設值函式。

tips:可以批量操作多個設值函式。

  • 關鍵字:

設值替換為new

以工廠函式取代建構函式

  • 時機:
  1. 建構函式每次都需要new關鍵字,又臭又長(個人觀點是這條沒必要,除非完全忍受不了)
  2. 建構函式如果不是default匯出的話,這個名字那就是固定的。有時候語義化不明顯
  3. 有時雖然都是呼叫同一個類。但所處環境不同,我呼叫意義就不同
  • 做法:
  1. 新建一個工廠
  2. 工廠呼叫並返回現有的建構函式
  3. 替換呼叫者
  4. 儘可能縮小建構函式可見範圍(js中很難實現,可能只能藏的深一些)
  • 關鍵字:

工廠函式、呼叫類、替換呼叫

以命令取代函式手法

  • 時機:
  1. 在js中,體現為又臭又長的還沒法進行指責劃分的函式(可能是它們都屬於同一部分邏輯,也可能是因為內部寫法導致不好劃分)
  • 做法:
  1. 新建一個空的類
  2. 搬移函式手法將函式搬移到這個新的類
  3. 給類改個有意義的名字,如果沒什麼好名字就給命令物件的實際具體執行的函式起一個通用的名字,如:execute或者call
  4. 將原函式作為轉發函式,去構造類
  5. 將函式內的引數,改為構造時候傳入
  6. 如果可以將其他欄位修改為只讀
  • 關鍵字:

新的類、函式搬家、原類轉發函式、構造入參、只讀

函式上移手法

  • 時機:
  1. 子類中有絕大部分都在複製某個函式
  2. 這些函式函式體都相同或者近似
  • 做法:
  1. 確保待提升函式的行為完全一致,否則需要先將他們一致化
  2. 檢查函式體內的所有呼叫和欄位都能從超類中呼叫(如果有不一致則考慮先把它們提升)
  3. 檢查函式名字全部一致,不一致的話先將他們名字統一
  4. 將函式複製到超類中
  5. 逐一移除子類中的函式。每一次都要測試
  • 關鍵字:

函式體一致化、名字一致化、引用呼叫先行、提升函式、刪除重寫

欄位上移手法

  • 時機:
  1. 子類中有絕大部分都在複製某個欄位
  • 做法:
  1. 檢查該欄位的所有使用點,確保是在同樣的方式被使用
  2. 如果名字不同,先把名字統一化
  3. 移動到父類,並確保子類都能訪問父類的這個欄位
  4. 逐一移除子類的該欄位
  • 關鍵字:

同樣方式使用、統一名字、欄位上移、刪除子類欄位

建構函式本體上移

  • 時機:
  1. 子類中有絕大部分都在複製某個建構函式函式
  2. 這些建構函式函式體都相同或者近似
  • 做法:
  1. 如果超類沒有建構函式,就先定義一個,所有子類增加super關鍵字
  2. 使用移動語句將子類的公共語句移動到super緊挨著之後
    3、提升到超類建構函式中
    4、逐一移除子類的公共程式碼,如果這個值來自於呼叫者,則從super上傳給父類
    5、如果要上移的語句有基於子類的欄位而設定初始化的值的,檢視是否可以將這個欄位上移,如果不能,則使用提煉函式語句,將這句提煉為一個函式,在建構函式內呼叫他
    6、函式上移
  • 關鍵字:

建構函式內的語句上移

函式下移、欄位下移

  • 時機:
  1. 超類中的函式(欄位)只與一部分子類有關(這個範圍需要掌控好,我通常選擇如果使用超過三分之二的,並且在剩餘的三分之一里面,這個函式/欄位沒有副作用,就選擇上移,否則下移)
  • 做法:
  1. 將超類中的函式(欄位)本體逐一複製到每一個需要此函式(欄位)的子類中
  2. 刪除超類中的函式(欄位)
  • 關鍵字:

按需放置

以子類取代狀態碼

  • 時機:
  1. 一個類中有一些有必要的多型性被隱藏
  2. 根據某個狀態碼來返回不同的行為
  • 做法:

直接繼承超類的

  1. 將型別碼欄位進行封裝,改為一個get type()的形式
  2. 選擇其中一個型別碼,為其建立一個自己型別的子類
  3. 建立一個選擇器邏輯(根據型別,選擇正確的子類)把型別碼複製到新的子類
  4. 測試
  5. 逐一建立、新增選擇邏輯的程式碼
  6. 移除建構函式的這個引數
  7. 將與型別相關的程式碼重構優化

間接繼承(通過型別的超類而非現有超類進行繼承)

  1. 用型別類包裝型別碼(以物件取代基本型別手法)
  2. 走直接繼承超類的邏輯,唯一不同的是,這次要繼承型別超類,而非當前超類
  • 關鍵字:

封裝型別碼、多型化、選擇子類的函式、移除型別引數

移除子類

  • 時機:
  1. 隨著程式發展子類原有行為被搬離殆盡
  2. 原本是為了適應未來,而增加子類,但是現在放棄了這部分程式碼。
  3. 子類的用處太少,不值得保留
  • 做法:
  1. 檢查子類的使用者,是否根據不同子類進行處理
  2. 如果處理了則將處理函式封裝為一個函式,並將他們搬移到父級
  3. 新建一個欄位在超類,用以代表子類的型別
  4. 將選擇哪個類來例項化的建構函式搬移到超類
  5. 逐步搬移所有的型別
  6. 將原本的型別處理改為使用新建的欄位進行判斷處理
  7. 刪除子類
  • 關鍵字:

工廠函式取代子、型別提煉、檢查型別判斷

提煉超類

  • 時機:
  1. 兩個類在做類似的事情
  2. 兩個類隨著程式發展,有一些共同部分需要合併到一起
  • 做法:
  1. 新建超類(可能已經存在)
  2. 調整建構函式(從資料開始)
  3. 調整子類需要的欄位
  4. 將多個子類內共同的行為複製到超類
  5. 檢查客戶端程式碼。考慮是否調整為超類
  • 關鍵字:

相同事情搬移到超類

以委託取代子類

  • 時機:
  1. 類只能繼承一個,無法多繼承
  2. 繼承給類引入了緊密的關係(超類、子類耦合嚴重)
  • 做法:
  1. 使用以工廠函式取代建構函式將子類封裝
  2. 建立一個委託類、接受所有子類的資料,如果用到了超類,則以一個引數指代超類
  3. 超類中增加一個安放委託類的欄位
  4. 增加一個建立子類的工廠,讓他初始化超類中的委託欄位
  5. 將子類中的函式搬移至委託類,不要刪除委託程式碼(如果用到了其他元素也要一併搬離)
  6. 如果這個函式被子類之外使用了,把留在子類的委託移動到超類中,並加上衛語句,檢查委託物件初始化
  7. 如果沒有其他呼叫者,使用移除死程式碼手法去掉沒人使用的委託程式碼
  8. 測試
  9. 重複567。直到所有函式都搬到了委託類
  10. 找到呼叫子類的地方,將其改為使用超類的建構函式
  11. 去掉子類
  • 關鍵字:

工廠函式初始化類、委託類、所有子類資料搬移至委託類、超類增加委託類的欄位、子類函式搬移到委託類、刪除子類

以委託取代超類手法

  • 時機:
  1. 錯誤的繼承(如父子不是同一個意義的東西,但是子還想要用超類的一些欄位)
  2. 超類不是所有方法都適用於子類
  • 做法:
  1. 在子類中建立一個屬性,指向新建的超類例項
  2. 子類中用到超類的函式,為他們建立轉發函式(用上面的屬性)
  3. 去除子類與超類的繼承關係
  • 關鍵字:

子類屬性指向超類、轉發函式、去除繼承

讀後感:

收穫最大的莫過於感嘆作者過於謹慎,震驚於作者重構能力之強,對程式碼重構理解程度之深,雖然作者有一些“墨跡”,但不可否認,這是極佳的一種方式,雖然工作中我們不得已沒有那麼多時間去這麼小的步子,我們可以步子稍微大一些,當遇到問題時,在回滾並放慢步子。

第二點收穫就為作者對於程式碼好壞的定義,好的程式碼就是讓人能夠理解,能夠讓人很快的找到自己要修改的地方,並可以高效的規避報錯風險。雖然前期投入時間可能會多一些,但後期的效果卻是讓人能夠驚訝的,正如作者所說的:清晰的程式碼更容易理解,使你能夠發現更深層次的設計問題,從而形成積極正向的迴圈。

作者一直在強調,重構是一步一步改進的,不是說一下子就要如何如何,不只是說單次改進過程要小幅度多測試,也是在說我們不一定要將程式碼中所有都實現到近乎完美的地步,而是應該抉擇一個程式碼重構與真實情況的平衡點,這和大刀闊斧直談架構重構的也不一樣,程式碼是在不斷構築-設計中保持自己的新鮮性,直談大型架構重構,只能是笑談,畢竟架構為設計、編寫。而直接重構整個架構,除非你想被老闆炒魷魚了。

最後本章作者使用了很多的手法,雖然都只是一些常用的什麼提煉函式啊、內聯變數啊之類的,但處處又透露著我們需要學習的!

重構是為了程式碼能被人讀懂(所謂什麼更好擴充套件啊、更好的設計模式啊、結構化啊等等等等都是為了這點。所以我統歸為為了能讀懂)可以選擇犧牲一些(不是說可以完全忽略了效能),畢竟在現代瀏覽器、打包工具的加持、快取的加持下,肉眼看到的問題以及我們思考的問題也許已經被各種加持下悄悄消失了

從長久來看,重構對於日後的維護日後的開發,隨著時間的流逝肯定是一個正收益,但是短期來說可能要影響我們一些,我們要權衡好這些點之間的平衡,畢竟工作是為了賺錢,公司也是為了盈利,不可能給我們無限時間去搞這些,作者同時也提出了重構並不是工作日誌中的某一個任務的時機,主要體現在:新功能開發時、為了程式碼可讀性、程式碼整合、有計劃的重構程式碼以及堅持長期的重構以及review的重構。可以說是隨時隨地都可以重構,但也不是任何地方任何時機都可以重構,我們要利用好測試的套件,保證原效果的前提下,結合實際情況,多維度思考,即使閱讀過後,也應該時常翻開這本書,進行反覆閱讀,以提醒自己。

在本書中我學到了如何甄別壞的程式碼,以及怎麼處理他們,學到了開發中應該測試先行以及一些重構的基本知識。

不過作為jser,我個人覺得雖然作為一本通用型書籍,確實應該不摻雜很多的語法,不過既然選定了js,自動化重構這塊其實怎麼說呢,寫的都是IDE。但是js中並沒有這類工具。然後作者也沒有說在js下應該可以藉助某些功能來幫助重構。所以這塊還是一片空白,雖然這種事理應自己去研究。但是還是想免費更好嘛

相關文章