可以先看【推薦】:http://www.lxweimin.com/p/d6ff54d72afb
原文:http://linbinghe.com/2017/141bc1eb.html
什么是重構
所謂重構是這樣一個過程:在不改變代碼外在行為的前提下,對代碼作出修改,以改進程序的內部結構。本質上說,重構就是在代碼寫好之后改進它的設計。
重構(名詞):對軟件內部結構的一種調整,目的是在不改變軟件可觀察行為的前提下,提高其可理解性,降低其修改成本。
重構(動詞):使用一系列重構手法,在不改變軟件可觀察行為的前提下,調整其結構。
重構的目的是使軟件更容易被理解和修改。重構不會改變軟件可觀察的行為——重構之后軟件功能一如既往。而重構技術就是以微小的步伐修改程序,如果你犯下錯誤,很容易便可以發現它。
為何重構
-
重構改進軟件設計
- 只為了短期目的或者在完全理解整體設計之前編寫出來的代碼,會導致程序逐漸失去自己的結構。這時如果沒有重構,程序的設計會逐漸腐敗變質,程序員愈來愈難通過閱讀源碼而理解原本設計。重構很像是在整理代碼,你所做的就是讓所有東西回到應該的位置上。代碼結構的流失是累積性的。愈難看出代碼所代表的設計意涵,就愈難保護其中設計,于是該設計就腐敗得愈快。經常性的重構可以幫助代碼維持自己該有的形態。
-
重構使軟件更容易理解
- 所謂程序設計,很大程度上就是與計算機交談:你編寫代碼告訴計算機做什么事,它的響應則是精確按照你的指示行動。你得及時填補“想要它做什么”和“告訴它做什么”之間的縫隙。這種編程模式的核心就是“準確說出我所要的”。除了計算機外,你的源碼還有其他讀者:幾個月之后可能會有另一位程序員嘗試讀懂你的代碼并做一些修改。我們很容易忘記這第二位讀者,但他才是最重要的。計算機是否多花了幾個小時來編譯,又有什么關系呢?如果一個程序員花費一周時間來修改某段代碼,那才關系重大—如果他理解你的代碼,這個修改原本只需一小時。
-
重構幫助找到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