重構:改善既有程式碼的設計

林炳河發表於2017-03-16

 什麼是重構

  所謂重構是這樣一個過程:在不改變程式碼外在行為的前提下,對程式碼作出修改,以改程式序的內部結構。本質上說,重構就是在程式碼寫好之後改進它的設計。

  重構(名詞):對軟體內部結構的一種調整,目的是在不改變軟體可觀察行為的前提下,提高其可理解性,降低其修改成本。

  重構(動詞):使用一系列重構手法,在不改變軟體可觀察行為的前提下,調整其結構。

  重構的目的是使軟體更容易被理解和修改。重構不會改變軟體可觀察的行為——重構之後軟體功能一如既往。而重構技術就是以微小的步伐修改程式,如果你犯下錯誤,很容易便可以發現它。

 為何重構

  • 重構改進軟體設計

    • 只為了短期目的或者在完全理解整體設計之前編寫出來的程式碼,會導致程式逐漸失去自己的結構。這時如果沒有重構,程式的設計會逐漸腐敗變質,程式設計師愈來愈難通過閱讀原始碼而理解原本設計。重構很像是在整理程式碼,你所做的就是讓所有東西回到應該的位置上。程式碼結構的流失是累積性的。愈難看出程式碼所代表的設計意涵,就愈難保護其中設計,於是該設計就腐敗得愈快。經常性的重構可以幫助程式碼維持自己該有的形態。
  • 重構使軟體更容易理解

    • 所謂程式設計,很大程度上就是與計算機交談:你編寫程式碼告訴計算機做什麼事,它的響應則是精確按照你的指示行動。你得及時填補“想要它做什麼”和“告訴它做什麼”之間的縫隙。這種程式設計模式的核心就是“準確說出我所要的”。除了計算機外,你的原始碼還有其他讀者:幾個月之後可能會有另一位程式設計師嘗試讀懂你的程式碼並做一些修改。我們很容易忘記這第二位讀者,但他才是最重要的。計算機是否多花了幾個小時來編譯,又有什麼關係呢?如果一個程式設計師花費一週時間來修改某段程式碼,那才關係重大—如果他理解你的程式碼,這個修改原本只需一小時。
  • 重構幫助找到bug

    • 對程式碼的理解,可以幫助我們找到bug。有些人只要盯著一大段程式碼就可以找出裡面的bug,但大多數人不行。如果對程式碼進行重構,我們就可以深入理解程式碼的作為,並恰到好處地把新的理解反饋回去。搞清楚程式結構的同時,也清楚了自己所做的一些假設,從而更加容易把bug揪出來。
  • 重構提高程式設計速度

    • 良好的設計是快速開發的根本──事實上,擁有良好設計才可能做到快速開發。如果沒有良好設計,或許某一段時間內你的進展迅速,但惡劣的設計很快就讓你的速度慢下來。你會把時間花在除錯上面,難以或者甚至無法新增新功能,修改時間愈來愈長,擴充套件性與可維護性越來越差。因為你必須花愈來愈多的時間去理解系統、尋找重複程式碼。隨著你給最初程式打上一個又一個的補丁,新特性需要更多程式碼才能實現,最終變成一個惡性迴圈。
  • 重構使單元測試成為可能

    • Android作為對單元測試最不友好的系統環境,大多數開發者在剛開始接觸時都會直觀地把和當前介面有關的所有邏輯都放在Activity或者Fragment等檢視裡面,造成View層與邏輯層甚至資料層深度耦合,而單元測試對View的測試非常乏力,也不值得花大量時間在這上面,同時因為邏輯的過度耦合,每一個類裡面有非常多的私有依賴無法進行mock,從而無法達到儘可能全面測試的目的。但是單元測試可以測出整個測試流程中80%的bug,這是對程式碼質量的一個重大保障。
  • 重構應該是開發人員的必備技術手段

    • 一名開發人員的良好發展路線應該是由會寫程式碼轉為寫好程式碼,也是從一名實現者到架構者的轉變,甚至即使是單純的實現者,也應該追求自己能寫出越來越優雅和高效的程式碼。業務需求與技術需求不應該是對立面,技術需求最終也是服務於業務需求,更好的程式碼結構才能幫助專案更快速地開發。

 何時重構

  重構不是一件應該特別撥出時間做的事情,重構應該隨時隨地進行。不應該為重構而重構,之所以重構,是因為我們想做別的什麼事,而重構可以幫助我們把那些事做好。

  • 新增功能時重構。
  • 修補錯誤時重構。
  • 複審程式碼時重構。
  • 三次法則:事不過三,三則重構。

 何時不該重構

  程式碼根本無法工作或者太糟糕,重構還不如重寫來的簡單。

  在專案的最後期限,應該避免重構。

 程式碼的壞味道

  重複程式碼(Duplicated Code)

  • 同一個類的兩個函式還有相同的表示式,這時需要提煉出重複程式碼。
  • 兩個互為兄弟的子類內含有相同的表示式,可以提煉相同程式碼,並放到父類中。如果只是程式碼間相似,並非完全相同,那麼可以將相似部分和差異部分拆開,構成單獨的函式。然後你可以使用模板方法的設計模式。
  • 如果兩個毫不相關的類中出現重複程式碼,則可以將重複程式碼提煉成一個函式放到一個獨立類中或者只放在某一個類中(總之要放在合適的地方),然後其他類都去呼叫這個函式。

  過長函式(Long Method)

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

  過大的類(Large Class)

  類內如果有太多程式碼,也是程式碼重複、混亂並最終走向死亡的源頭。最簡單的解決方案是把多餘的東西消弭於類內部。如果有五個“百行函式”,它們之間很多程式碼都相同,那麼或許你可以把它們程式設計五個“十行函式”和十個提煉出來的“雙行函式”。

  過長引數列(Long Parameter List)

  • 有了物件,就不必把函式需要的所有東西都以引數傳遞給它了,只需傳給它足夠的、讓函式能從中獲得自己需要的東西就行了。函式需要的東西多半可以在函式宿主類中找到。如果將物件傳遞給函式,大多數修改都將沒有必要,因為你很可能只需(在函式內)增加一兩條請求,就能得到更多的資料。
  • 這裡有一個重要的例外:有時候你明顯不希望造成“被呼叫物件”與“較大物件”間的某種依賴關係。這時候將資料從物件中拆解出來單獨作為引數,也很合情合理。

  發散式變化(Divergent Change)

  一個類受多種變化的影響

  如果某個類經常因為不同的原因在不同的方向上發生變化,發散式變化的壞味道就出現了。針對某一外界變化的所有相應修改,都只應該發生在單一類中,而這個新類內的所有內容都應該反應次變化。

  霰彈式修改(Shotgun Surgery)

  一種變化引發多個類相應修改

  如果每遇到某種變化,你都必須在許多不同的類內做出許多小修改,你所面臨的壞味道就是霰彈式修改。這種情況下,可以把所有需要修改的程式碼放進同一個類,如果沒有合適的類可放,就創造一個。

  依戀情結(Feature Envy)

  函式對某個類的興趣高過自己所處類的興趣

  • 物件技術的全部要點在於:這是一種“將資料和對資料的操作行為包裝在一起”的技術。
  • 有一種經典的氣味是:函式對某個類的興趣高過對自己所處類的興趣。這種孺慕之情最通常的焦點便是資料。
  • 處理這種壞味道的原則是:判斷哪個類擁有最多被此函式實用的資料,然後就把這個函式和那些資料擺在一起。
  • 最根本的原則是:將總是一起變化的東西放在一塊。

  資料泥團(Data Clumps)

  相同的若干項資料出現在不同地方,這些綁在一起出現的資料應該有屬於它們自己的物件

  一個好的評判辦法是:刪掉眾多資料中的一項,其他資料有沒有因此而失去意義?如果他們不再有意義,這就是一個明確訊號:你應該為它們產生一個新物件。

  基本型別偏執(Private Obsession)

  很多人不願意在小任務上運用小物件

  • 物件技術的新手通常不願意在小任務上運用小物件。
  • 如果你有一組應該總是被放在一起的欄位(基本型別的資料),那麼可以嘗試將這組資料放到一個單獨類中變成結構型別的資料

  switch驚悚現身(Switch Statements)

  switch語句會在很多地方重複出現,一改則需全改

  物件導向程式的一個最明顯特徵就是:少用switch(或case)語句。從本質上說,switch語句的問題在於重複。物件導向中的多型概念可為此帶來優雅的解決辦法。

  平行繼承體系(Parallel Inheritance Hierarchies)

  當你為某一個類增加子類時,也必須為另一個類相應增加一個類

  • 如果你發現某個繼承體系的類名稱字首和另一個繼承體系的類名稱字首完全相同,便是問到了這種壞味道。
  • 消除這種重複性的一般策略是:讓一個繼承體系的例項引用另一個繼承體系的例項。

  冗贅類(Lazy Class)

  如果一個類不值得存在,那就讓它消失

  誇誇其談的未來星(Speculative Generality)

  預留的無用的抽象類,無用的抽象引數

  當有人說“噢, 我想我們總有一天需要做這件事”,並因而企圖以各式各樣的鉤子和特殊情況來處理一些非必要的事情,這種壞味道就出現了。

  令人迷惑的暫時欄位(Temporary Field)

  類中某個欄位只為某些特殊情況而設定

  過度耦合的訊息鏈(Message Chains)

  使用者向一個物件請求另一個物件,然後再向後者請求另一個物件……

  中間人(Middle Man)

  無用的委託,過多的中間層

  狎暱關係(Inappropriate Intimacy)

  兩個類過於親密,一個類過於關注另一個類的成員

  異曲同工的類(Alternative Classes with Different Interfaces)

  不同名字的類或函式,作者相同的事

  不完美的庫類(Incomplete Library Class)

  類庫設計不可能完美

  純資料類(Data Class):一個類擁有一些欄位以及用於訪問這些欄位的函式,除此之外一無長物

  • 純稚的資料類是指:它們擁有一些欄位,以及用於訪問(讀寫)這些欄位的函式,除此之外一無長物。
  • 這種類如果get/set方法均是public的,則需要引起注意,應該進行適當的封裝,而不是全部公有化。

  被拒絕的遺贈(Refused Bequest):子類不想繼承超類所有的函式和資料,只想挑幾樣來玩

  子類應該繼承超類的函式和資料。但如果他們不想或不需要繼承所有的函式和資料,則應該為這個子類新建一個兄弟類,把所有用不到的函式和資料放到兄弟類中,他們共享的資料和函式則放到共同的超類中。

  過多的註釋(Comments)

  • 當你感覺需要撰寫註釋時,請先嚐試重構,試著讓所有註釋都變得多餘
  • 如果你不知道該做什麼的,這才是註釋的良好運用時機。

 構築測試體系

  • 重構的首要前提是擁有一個可靠的測試環境。
  • 只要寫好一點功能,就立即新增測試,並確保所有測試都完全自動化,讓它們檢查自己的測試結果。一套測試就是一個強大的bug偵測器,能夠大大縮減查詢bug所需要的時間。
  • 撰寫測試程式碼的最有用時機是在開始程式設計之前。當你需要新增特性的時候,先寫相應測試程式碼。編寫測試程式碼其實就是在問自己:新增這個功能需要做些什麼。編寫測試程式碼還能使你把注意力集中於介面而非實現。預先寫好的測試程式碼也為你的工作安上一個明確的結束標誌:一旦測試程式碼正常執行,工作就可以結束了。
  • 多運用單元測試。測試你最擔心出錯的地方,考慮可能出錯的邊界條件。不要因為測試無法捕捉所有bug就不寫測試,因為測試的確可以捕捉到大多數bug。“花合理時間抓出大多數bug”要好過“窮盡一生抓出所有bug”。

 重構手法

  重新組織函式

  提煉函式(Extract Method)

  你有一段程式碼可以被組織在一起並獨立出來。將這段程式碼放進一個獨立函式中,並將函式名稱解釋該函式的用途。

  • 動機:首先,如果每個函式的粒度都很小,那麼函式被服用的機會就更大;其次,這回是高層函式讀起來就像一系列註釋;再次,如果函式都是細粒度,那麼函式的覆寫也會更容易些。函式的長度不是問題,關鍵在於函式名稱和函式本體之間的語義距離。如果提煉可以強化程式碼的清晰度,那麼就去做,就算函式名稱比提煉出來的程式碼還長也無所謂。
  • 做法:創造一個新函式,根據這個函式的意圖來對它命名(以它“做什麼”來命名,而不是以它“怎樣做”命名)。

  行內函數(Inline Method)

  一個函式的本體與名稱同樣清楚易懂。在函式呼叫點插入函式本體,然後移除該函式。

  內聯臨時變數(Inline Temp)

  你有一個臨時變數,只被一個簡單表示式賦值一次,而它妨礙了其他重構手法。將所有對該變數的引用動作,替換為對它賦值的那個表示式自身。

  以查詢取代臨時變數(Replace Temp with Query)

  你的程式以一個臨時變數儲存某一表示式的運算結果。將這個表示式提煉到一個獨立函式中。將這個臨時變數的所有引用點替換為對新函式的呼叫。此後,新函式就可被其他函式使用。

  引入解釋性變數(Introduce Explaining Variable)

  你有一個複雜的表示式。將該複雜表示式(或其中一部分)的結果放進一個臨時變數,以此變數名稱來解釋表示式用途。

  分解臨時變數(Split Temporary Variable)

  你的程式有某個臨時變數被賦值過一次,它既不是迴圈變數,也不被用於收集計算結果。針對每次賦值,創造一個獨立、對應的臨時變數。

  移除對引數的賦值(Remove Assignments Parameters)

  程式碼對一個引數進行賦值。以一個臨時變數取代引數的位置。

  以函式物件取代函式(Replace Method with Method Object)

  你有一個大型函式,其中對區域性變數的使用使你無法採用Extract Method。將這個函式放進一個單獨物件中,如此一來區域性變數就成了物件內的欄位。然後你可以在同一個物件中將這個大型函式分解為多個小型函式。

  替換演算法(Substitute Algorithm)

  你想要把某個演算法替換為另一個更清晰的演算法。將函式本體替換為另一個演算法。

  在物件之間搬移特性

  搬移函式(Move Method)

  你的程式中,有個函式與其所駐之外的另一個類進行更多交流:呼叫後者,或被後者呼叫。在該函式最常引用的類中建立一個有著類似行為的新函式。將舊函式變成一個單純的委託函式,或是將舊函式完全移除。

  搬移欄位(Move Field)

  你的程式中,某個欄位被其所駐類之外的另一個類更多地用到。在目標類新建一個欄位,修改源欄位的所有使用者,令它們改用新欄位。

  提煉類(Extract Class)

  某個類做了應該有兩個類做的事。建立一個新類,將相關的欄位和函式從舊類搬移到新類。

  將類內聯化(Inline Class)

  某個類沒有做太多事情。將這個類的所有特性搬移到另一個類中,然後移除原類。

  隱藏“委託關係”(Hide Delegate)

  客戶通過一個委託來呼叫另一個物件。在服務類上建立客戶所需的所有函式,用以隱藏委託關係。

  移除中間人(Remove Middle Man)

  某個類做了過多的簡單委託動作。讓客戶直接呼叫受託類。

  引入外加函式(Introduce Foreign Method)

  你需要為提供服務的類增加一個函式,但你無法修改這個類。在客戶類中建立一個函式,並以第一引數形式傳入一個服務類例項。

  引入本地擴充套件(Introduce Local Extension)

  你需要為服務類提供一些額外函式,但你無法修改這個類。建立一個新類,使它包含這些額外函式。讓這個擴充套件品成為源類的子類或包裝類。

  重新組織資料

  自封裝欄位(Self Encapsulate Field)

  你直接訪問一個欄位,但與欄位之間的耦合關係逐漸變得笨拙。為這個欄位建立取值/設值函式,並且只以這些函式來訪問欄位。

  以物件取代資料值(Replace Data Value with Object)

  你有一個資料項,需要與其他資料和行為一起使用才有意義。將資料項變成物件。

  將值物件改為引用物件(Change Value to Reference)

  你從一個類衍生出許多彼此相等的例項,希望將它們替換為同一個物件。將這個值物件變成引用物件。

  將引用物件改為值物件(Change Reference to Value)

  你有一個引用物件,很小且不可變,而且不易管理。將它變成一個值物件。

  • 值物件有一個非常重要的特性:它們應該是不可變的。
  • 定義了equals(),就必須同時定義hashCode()。實現hashCode()有個簡單的辦法:讀取equals()使用的所有欄位的hash碼,然後對它們進行按位異或(^)操作。

  以物件取代資料(Replace Array with Object)

  你有一個陣列,其中的元素各自代表不同的東西。以物件替換陣列,對於陣列中的每個元素,以一個欄位來表示。

  用物件替代陣列的好處就是就對陣列的各種操作都可以封裝起來。因此,對於那些需要各種操作的陣列,最好封裝起來。

  複製“被監視資料”(Duplicate Observed Data)

  你有一些領域資料置身GUI控制元件中,而領域函式需要訪問這些資料。將該資料複製到一個領域物件中。建立一個Observe模式,用以同步領域物件和GUI物件內的重複資料。

  將單向關聯改為雙向關聯(Change Unidirectional Association to Bidirectional)

  兩個類都需要使用對方特性,但其間只有一條單向連結。新增一個反向指標,並使修改函式能夠同時更新兩條連結。

  將雙向關聯改為單向關聯(Change Bidirectional Association to Unidirectional)

  兩個類之間有雙向關聯,但其中一個類如今不再需要另一個類的特性。去除不必要的關聯。

  以字面常量取代魔法數(Replace Magic Number with Symbolic Constant)

  你有一個字面數值,帶有特別含義。創造一個常量,根據其意義為它命名,並將上述的字面數值替換為這個常量。

  封裝欄位(Encapsulate Field)

  你的類中存在一個public欄位。將它宣告為private,並提供相應的訪問函式。

  封裝集合(Encapsulate Collection)

  有個函式返回一個集合。讓這個函式返回該集合的一個只讀副本,並在這個類中提供新增/移除集合元素的函式。

  以資料類取代記錄(Replace Record with Data Class)

  你需要面對傳統程式設計環境中的記錄結構。為該記錄建立一個“啞”資料物件。

  以類取代型別碼(Replace Type Code with Class)

  類之中有一個數值類行碼,但它並不影響類的行為。以一個新的類替換該數值型別碼。

  以子類取代型別碼(Replace Type Code with Subclass)

  你又一個不可變的型別碼,它會影響類的行為。以子類取代這個型別碼。

  以State/Strategy取代型別碼(Replace Type Code with State/Strategy)

  你有一個型別碼,它會影響類的行為,但你無法通過繼承手法消除它。以狀態物件取代型別碼。

  以欄位取代子類(Replace Subclass with Fields)

  你的各個子類的唯一差別只在“返回常量資料”的函式身上。修改這些函式,使他麼返回超類中的某個(新增)欄位,然後銷燬子類。

  簡化條件表示式

  分解條件表示式(Decompose Conditional)

  你有一個複雜的條件(if-then-else)語句。從if、then、else三分段落中分別提煉出獨立函式。

  合併條件表示式(Consolidate Conditional Expression)

  你有一系列條件測試,都得到相同結果。將這些測試合併為一個條件表示式,並將這個條件表示式提煉成為一個獨立函式。

  • 如果檢查條件各不相同,最終行為卻一致,就應該使用“邏輯或”和“邏輯與”將他們合併為一個條件表示式。
  • 如果你認為這些檢查的確彼此獨立,的確不應該被視為同一次檢查,那麼就不要使用本項重構。

  合併重複的條件片段(Consolidate Duplicate Conditional Fragments)

  在條件表示式的每個分支上有著相同的一段程式碼。將這段重複的程式碼搬移到條件表示式之外。

  移除控制標記(Remove Control Flag)。在一系列布林表示式中,某個變數帶有“控制標記”的作用。以break語句或return語句取代控制標記。

  以衛語句取代巢狀條件表示式(Replace nested Conditional with Guard Clauses)

  函式中的條件邏輯使人難以看清正常的執行路徑。使用衛語句表現所有的特殊情況。

  • 衛語句,是指在方法最終返回語句前,加入其它返回語句(return語句)
  • 拆分條件,然後加入適當的衛語句,以減少條件的巢狀層數
  • 將條件反轉,然後加入適當的衛語句,以減少條件的巢狀層數

  以多型取代條件表示式(Replace Conditional with Polymorphism)

  你手上有個條件表示式,它根據物件型別的不同選擇不同的行為。將這個條件表示式的每個分支放進一個子類內的覆寫函式中,然後將原始函式宣告為抽象函式。

  引入Null物件(Introduce Null Object)

  你需要再三檢查某物件是否為null。將null值替換為null物件。

  引入斷言(Introduce Assertion)

  某一段程式碼需要對程式狀態做出某種假設。以斷言明確表現這種假設。

  簡化函式呼叫

  函式改名(Rename Method)

  函式的名稱未能揭示函式的用途。修改函式的名稱。

  新增引數(Add Parameter)

  某個函式需要從呼叫端得到更多資訊。為此函式新增一個物件引數,讓該物件帶進函式所需資訊。

  移除引數(Remove Parameter)

  函式本體不再需要某個引數。將該引數去除。

  將查詢函式和修改函式分離(Separate Query from Modifier)

  某個函式既返回物件狀態值,又修改物件狀態。建立兩個不同的函式,其中一個負責查詢,另一個負責修改。

  令函式攜帶引數(Parameterize Method)

  若干函式做了類似的工作,但在函式本體中卻包含了不同的值。建立單一函式,以參數列達那些不同的值。

  以明確函式取代引數(Replace Parameter with Explicit Methods)

  你有一個函式,其中完全取決於引數值而採取不同行為。針對該引數的每一個可能值,建立一個獨立函式。

  保持物件完整(Preserve Whole Object)

  你從某個物件中取出若干值,將它們作為某一次函式呼叫時的引數。改為傳遞整個物件。

  • 有時候,你會將來自同一物件的若干項資料作為引數,傳遞給某個函式。這樣做的問題在於:萬一將來被呼叫的函式需要新的資料項,你就必須查詢並修改對此函式的所有呼叫。如果你把這些資料所屬的整個物件傳給函式,可以避免這種尷尬的處境,因為被呼叫函式可以向那個引數物件請求任何它想要的資訊。
  • 如果被呼叫函式使用了來自另一個物件的很多項資料,這可能意味該函式實際上應該被定義在那些資料所屬的物件中。

  以函式取代引數(Replace Parameter with Methods)

  物件呼叫某個函式,並將所得結果作為引數,傳遞給另一個函式。而接受該引數的函式本身也能夠呼叫前一個函式。讓引數接受者去除該項引數,並直接呼叫前一個函式。

  • 如果函式可以通過其他途徑獲得引數值,那麼它就不應該通過引數取得該值。
  • 過長的引數列會增加程式閱讀者的理解難度,因此我們應該儘可能縮短引數列的長度。

  引入引數物件(Introduce Parameter Object)

  某些引數總是很自然地同時出現。以一個物件取代這些引數。

  移除設值函式(Remove Setting Method)

  • 類中的某個欄位應該在物件建立時被設值,然後就不再改變。這樣的欄位應該去掉其對應的設值函式。
  • 如果你為某個欄位提供了設值函式,這就暗示這個欄位值可以被改變。如果你不希望在物件建立之後此欄位還有機會被改變,那就不要為它提供設值函式(同時該欄位設為final)。這樣你的意圖會更加清晰,並且可以排除其值被修改的可能性——這種可能性往往是非常大的。

  隱藏函式(Hide Method)

  • 有一個函式,從來沒有被其他任何類用到。將這個函式修改為private。
  • 儘可能降低所有函式的可見度。

  以工廠函式取代建構函式(Replace Constructor with Factory Method)

  你希望在建立物件時不僅僅是做簡單的構建動作。將構建函式替換為工廠函式。

  • 在派生子類的過程中以工廠函式取代型別碼。
  • 如果要根據型別來建立不同的物件,這些物件有共同的父類,則可以在父類中新增一個工廠函式來建立不同的物件。

  封裝向下轉型(Encapsulate Downcast)

  某個函式返回的物件,需要由函式呼叫者執行向下轉型。將向下轉型動作移到函式中。

  以異常取代錯誤碼(Replace Error Code with Exception)

  某個函式返回一個特定的程式碼,用以表示某種錯誤情況。改用異常。

  以測試取代異常(Replace Exception with Test)

  面對一個呼叫者可以預先檢查的條件,你丟擲了一個異常。修改呼叫者,使它在呼叫函式之前先做檢查。

  處理概括關係

  欄位上移(Pull Up Field)

  • 兩個子類擁有相同的欄位。將該欄位移至超類。
  • 如果這些欄位是private的,你必須將超類的欄位宣告為protected,這樣子類才能引用它。

  函式上移(Pull Up Method)

  • 有些函式在各個子類中產生完全相同的結果,則將該函式移至超類。
  • 如果你使用的是一種強型別語言,而待提升函式又呼叫了一個只出現於子類而未出現於超類的函式,你可以在超類中為被呼叫函式宣告一個抽象函式。

  建構函式本體上移(Pull Up Constructor Body)

  你在各個子類中擁有一些建構函式,他們的本體幾乎完全一致。在超類中新建一個建構函式,並在子類建構函式中呼叫它。

  函式下移(Push Down Method)

  超類中的某個函式只與部分(而非全部)子類有關。將這個函式移到相關的那些子類去。

  欄位下移(Push Down Field)

  超類中的某個欄位只被部分(而非全部)子類用到。將這個欄位移到需要它的那些子類去。

  提煉子類(Extract Subclass)

  類中的某些特性只被某些(而非全部)例項用到。新建一個子類,將上面所說的那一部分特性移到子類中。

  提煉超類(Extract Superclass)

  兩個類有相似特性。為這兩個類建立一個超類,將相同特性移至超類。

  提煉介面(Extract Interface)

  若干客戶使用類介面中的同一子集,或者兩個類的介面有部分相同。將相同的子集提煉到一個獨立介面中。

  摺疊繼承體系(Collapse Hierarchy)

  超類和子類之間無太大差別。將它們合為一體。

  塑造模板函式(Form TemPlate Method)

  你有一些子類,其中相應的某些函式以相同順序執行類似的操作,但各個操作的細節上所有不同。將這些操作分別放進獨立函式中,並保持它們都有相同的簽名,於是原函式也就變得相同了。然後將原函式上移至超類。

  以委託取代繼承(Replace Inheritance with Delegation)

  某個子類只使用超類介面中的一部分,或是根本不需要繼承而來的資料。在子類中新建一個欄位用以儲存超類;調整子類函式令它改而委託超類;然後去掉兩者之間的繼承關係。

  以繼承取代委託(Replace Delegation with Inheritance)

  你在兩個類之間使用委託關係,並經常為整個介面編寫許多極簡單的委託函式。讓委託類來繼承受託類。

  大型重構

  梳理並分解繼承體系(Tease Apart Inheritance)

  • 如果某個繼承體系同時承擔兩項責任,則可以建立兩個繼承體系,並通過委託關係讓其中一個可以呼叫另一個。
  • 要指出繼承體系是否承擔了兩項不同的責任並不困難:如果繼承體系中的某一特定層級上的所有類,其子類名稱都以相同的形容詞開始,那麼這個體系可能就是承擔著兩項不同的責任。

  將過程化設計轉化為物件設計(Convert Procedural Design to Objects)

  你手上有一些傳統過程化風格的程式碼。將資料記錄變成物件,將大塊的行為分成小塊,並將行為移入相關物件之中。

  將領域和表述/顯示分離(Separate Domain from Presentation)

  某些GUI類之中包含了領域邏輯。將領域邏輯分離出來,為它們建立獨立的領域類。

  提煉繼承體系(Extract Hierarchy)

  你有某各類做了太多工作,其中一部分工作是以大量條件表示式完成的。建立繼承體系,以一個子類表示一種特殊情況。

 為什麼開發者不願意重構他們的程式

  • 不知道如何重構
  • 如果這些利益是長遠的,何必現在付出這些努力呢?長遠看來,說不定當專案收穫這些利益時,你已經不再職位上了
  • 程式碼重構是一項額外工作,老闆付錢給你,主要是讓你編寫新功能
  • 重構可能破壞現有程式

  看完書本這一章節之後,以及自己進行過專案重構的經驗下,其實我們日常開發中很多在做的事情就體現了一些重構手法,比如刪除方法引數,分裝物件等等,只是我們自己不知道而已。

  重構並不難學,真正難的是在於要改變現有程式碼,對自己的程式設計思維做出改變,但是這是一種進步性質的改變,如果對技術有所追求,對自己寫出的程式碼有追求,這就是一條必經的路。

  而且重構可以帶來短期利益,它使軟體更易修改,更易維護,長期下來反倒更能節省你的開發時間,而且通過單元測試或者小步重構等方式可以讓重構非常安全地進行。

 總結

  彙總圖

重構:改善既有程式碼的設計

  要點列表

重構:改善既有程式碼的設計

  程式碼壞味道與重構手法

重構:改善既有程式碼的設計

 感想

  本書和設計模式一樣需要有一定的專案經驗,最好是有在寫程式碼過程中發現了一些問題,或者自己進行過一些重構了的情況下看會比較感同身受一些,否則很容易看了就忘或者甚至覺得看不下去。

  這本書中的例子都是用Java來寫的,例子也大多很簡單,所以有經驗的話看起來也會快很多,至少相比《設計模式:可複用物件導向軟體的基礎》要快得多。

  近來經常看到很多人鼓吹重構無用,甚至說重構就不應該修改程式碼,對於這種觀點不想評價什麼,但是重構作為常年位居程式設計師必看書目的前列應該就可以說明很多問題,很多技術不是沒有用,只是你還沒到覺得它有用的境界,或者你不會用。

  不管怎麼樣,重構和設計模式等諸多思想一樣,是需要反覆學習,反覆實踐,反覆總結的,等你接觸並真正吸收了這些優秀的思想之後,相信一定會看到一個不一樣的程式碼世界。

  最後發一句書作者說的一句話,深以為然:

  Any fool can write code that a computer can understand. Good programmers write code that humans can understand. ——Martin Fowler

相關文章