重構,改善既有代碼的設計
個人總結:以前對面向對象總覺得很一般的感覺,無法體會出面向對象真正帶來的便利。而對於繼承和多態,也只是瞭解知道怎麼用,但是看了這本書才真正對於繼承和多態有了進一步的理解,“原來繼承和多態是這麼用的”這種感覺,對於好的代碼應該是怎樣子的,也有了更具體一點的認知。對於以前自己寫的代碼,回想起來,根本就不是面向對象的,不堪回首……當然以後我還會不斷髮現自己以前寫的代碼很爛,這樣的話,說明我在不斷進步。
爲什麼重構:改善既有代碼的設計;持續糾偏和改進軟件設計;幫助發現隱藏的代碼缺陷;從長遠來看,有助於提高編程效率。(使代碼更易於理解、擴展和修改)
第一章 重構,第一個案例
任何一個傻瓜都能寫出計算機可以理解的代碼。唯有寫出人類容易理解的代碼,纔是優秀的程序員。
Ø 代碼被閱讀和修改的次數遠遠多於它被編寫的次數。
Ø 外國人代碼:各種類、接口、繼承、方法調用。每個方法做的事情少,通過類圖就可以看出邏輯。一個方法或類做的事情太多了,修改起來就很可能帶來很多Bug,鬆耦合!
Ø 重構第一步:建議一組可靠的測試環境。好的測試是重構的根本。
Ø 代碼塊越小,代碼的功能就越容易管理,代碼的處理和移動也就越輕鬆。
Ø 任何不會被修改的變量都可以被我當成參數傳入新的函數,至於會被修改的變量就要格外小心,如果只有一個變量會被修改,我可以把它當作返回值。
Ø 重構步驟的本質:由於每次修改的幅度都很小,所以任何錯誤都很容易發現。(基於測試)
Ø 如果爲了提高代碼的清晰度,需要修改某些東西的名字,那麼就大膽去做吧。
Ø 我喜歡儘量除去這一類臨時變量。臨時變量往往引發問題,它們會導致大量參數被傳來傳去,而其實完全沒有這種必要。你很容易跟丟它們,尤其在長長的函數中根式如此。(當然這麼做會也需付出性能上的代價,但是可以被優化。)
Ø 最好不要在另一個對象的屬性基礎上運用switch語句。如果不得不使用,也應該在對象自己的數據上使用,而不是別人的數據上使用。
Ø 重構手法:ExtractMethod、Move Method、Replace Conditional with Polymorphism、Self EncapsulateField、Replace Type Code with State/Strategy。
第二章 重構原則
Ø 何時重構:
重構應該隨時隨地進行。
不是爲了重構而重構,重構是爲了幫助做好想做的事情。
事不過三,三則重構。(三次法則)
添加功能時重構。
修補錯誤時重構。
複審代碼時重構。
Ø 你可以這麼想:如果收到一份錯誤報告,這就是需要重構的信號,因爲顯然代碼還不夠清晰——沒有清晰到讓你能一眼看出bug。
Ø 程序有兩面價值: “今天它可以爲你做什麼”和“明天它可以爲你做什麼”。不要爲了完成今天的任務而不擇手段,導致不能在明天完成明天的任務。
Ø 難以閱讀的程序,難以修改;
邏輯重複的程序,難以修改;
添加新行爲時需要修改已有代碼的程序,難以修改;
帶有複雜條件邏輯的程序,難以修改。
Ø 作者:我發現重構是理解軟件的最快方式。
Ø Dennis DeBruler:“計算機科學是這樣一門科學:它相信所有問題都可以通過增加一個間接層來解決。”
Ø “寄生式間接層”:你希望在不同的地點共享它,或者讓它表現出多態性,最終卻只在一處用到。那麼請拿掉它。
Ø “已發佈接口”:需要修改的接口被那些“找不到,即使找到也不能修改”的代碼使用。重構手法:儘量讓舊接口調用新接口。或者同時維護新舊兩個接口。
Ø 不要過早發佈接口,請修改你的代碼所有權政策,使重構更順暢。
Ø 何時不該重構:
現有代碼根本不能正常運作。
項目接近最後期限,也應該避免重構。如果最後你沒有足夠時間,通常就表示你其實早該進行重構。
方法:重寫(慎重)。折中辦法:將“大塊頭軟件”重構爲封裝良好的小型組件。
第三章 代碼的壞味道
Ø 重複代碼。DuplicatedCode。如果你在一個以上(兩個及兩個以上)的地點看到相同的程序結構,那麼可以肯定:設法將它們合而爲一,程序會變得更好。
Ø 過長函數。Long Method。早期的編程語言中,子程序調用需要額外開銷,這使得人們不太樂意使用小函數。現代OO語言幾乎已經完全免除了進程內的函數調用開銷。不過閱讀時需要京城轉換上下文去看看子程序做了什麼,故而一個好的函數名字顯得更加重要。最終效果:你應該更積極地分解函數。原則:每當感覺需要以註釋來說明點什麼的時候,我們就把需要說明的東西寫進一個獨立函數中,並以其用途(而非實現手法)命名。哪怕函數名比函數自身還長。
Ø Replace Temp with Query,Introduce ParameterObject,Preserve Whole Object,Replace Method with Method Object,DecomposeConditional,
Ø 就算只有一行代碼,如果它需要以註釋來說明,那也值得將它提煉到獨立函數去。
Ø 過大的類。Large Class。
Ø 過長參數列。Long Parameter List。
Ø 發散式變化。Divergent Change。
Ø 霰彈式修改。Shotgun Surgery。
Ø 依戀情結。Feature Envy。
Ø 數據泥團。Data Clumps。
Ø 基本類型偏執。Primitive Obsession。對象技術的新手通常不願意在小任務上運用小對象。
Ø switch驚悚現身。Switch Statements。面向對象程序的一個最明顯的特徵就是:少用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
Ø 被拒絕的遺贈。Refused Bequest
Ø 過多的註釋。Comments。當你感覺需要撰寫註釋時,請先嚐試重構,試着讓所有註釋都變得多餘。
第四章 構築測試體系
Ø 類應該包含它們自己的測試。(並且經常運行測試,可以提高生產)
Ø Java則使用JUnit測試框架。
Ø 別人的代碼,重構前構建測試體系。
第五章 重構列表
第六章 重新組織函數
Ø 提煉函數(Extract Method)。函數命名要儘量簡短良好,細粒度:複用機會大,高層函數讀起來就像註釋,函數複寫更容易。命名以“做什麼”來命名。
Ø 儘量讓一個函數只返回一個值,像C#那種支持“出參數”的語言可以,但還是不推崇。
Ø 內聯函數(Inline Method)。
Ø 內聯臨時變量(Inline Temp)。一個臨時變量只被簡單表達式賦值了一次,而妨礙了其他重構。
Ø 以查詢取代臨時變量(Replace Temp with Query)。
Ø 引解釋性變量(Introduce Explaining Variable)。複雜表達式中的結果放進一個臨時變量,以此解釋表達式用途。
Ø 分解臨時變量(Split Temporary Variable)。某個臨時變量被賦值超過一個,既不是循環變量也不用於收集計算結果,則針對每一次賦值,創造一個獨立的對應的臨時變量。
Ø 每個變量應該只承擔一個責任,因此多了就應該分解。
Ø 移除對參數的賦值(Remove Assignments to Parameters)。代碼對一個參數進行賦值,以一個臨時變量取代該參數的位置。
Ø 我並不經常用final 來修飾參數,我會在較長的函數中使用它,讓它幫我檢查參數是否被做了修改。
Ø 以函數對象取代函數(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)。你需要爲服務類提供一些額外函數,但你無法修改這個類。
第八章 重新組織數據
Ø 魔法數指代碼中直接出現的數值,不要使用魔法數值,代之以有名字的Static final或者enum。
Ø 自封裝字段(Self Encapsulate Field)。你直接訪問一個字段,但與字段之間的耦合關係逐漸變得笨拙。爲這個字段建立取值設值函數,並且只以這些函數來訪問字段。
Ø 以對象取代數據值(Replace Data Value with Object)。你有一個數據項,需要與其他數據和行爲一起使用纔有意義。
Ø 將值對象改爲引用對象(Change Value to Reference)。你從一個類衍生出許多彼此相等的實例,希望將它們替換爲同一個對象。
Ø 將引用對象改爲值對象(Change Reference to Value)。你有一個引用對象,很小且不可改變,而且不易管理。
Ø 如果引用對象開始變得難以使用,也許就應該將它改爲值對象。引用是對象必須被某種方式控制,你總是必須向其控制者請求適當地引用對象。它們可能造成內存區域之間錯綜複雜的關聯。在分佈系統和兵法系統中,不可變的值對象特別有用,因爲你無需考慮它們的同步問題。
Ø 以對象取代數組(Replace Array with Object)。數組中的原色各自代表不同的東西。以對象取代數組,對於數組中的每個元素,以一個字段來表示。
Ø 複製“被監視數據”(Duplicate Observed Data)。你有一些領域數據置身於GUI控件中,而領域函數需要訪問這些數據。
Ø 將單向關聯改爲雙向關聯(Change Unidirectional Association to Bidirectional)。兩個類都需要使用對方特性,但其間只有一條單向連接。添加一個党項指針,並使修改函數能夠同時更新兩條連接。如果兩者都是引用對象,關聯是“一對多”關係,那麼就由“擁有單一引用”的哪一方承擔“控制者”角色;如果某個對象是組成另一個對象的部件,那麼由後者負責控制關聯關係;如果兩者都是引用對象,關聯是“多對多”關係,那麼隨便其中哪個對象來控制關聯關係,都無所謂。
Ø 將雙向關聯該爲單向關聯(Change Bidirectional Associational to Unidirectional)。兩個類之間有雙向關聯,但其中一個類如今不再需要另一個類的特性。雙向關聯很有用,但是需付出代價,那就是維護雙向連接、確保對象被正確創建和刪除而增加的複雜度。而且很多程序員並不習慣使用雙向關聯,它往往成爲錯誤之源。大量的雙向連接也很容易造成“殭屍對象”。
Ø 以字面常量來取代魔法數(Replace Magic Number with Symbolic Constant)。
Ø 封裝字段(Encapsulate Field)。你的類存在一個public字段。
Ø 封裝集合(Encapsulate Collection)。有個函數返回一個集合。
Ø 以數據類取代記錄(Replace Record with Data Class)。你需要面對傳統編程環境中的記錄結構。
Ø 以類取代類型碼(Replace Type Code with Class)。類之中有一個數值類型碼,但它不影響類的行爲。
Ø 以子類取代類型碼(Replace Type Code with Subclasses)。你有一個不可變的類型碼,它會影響類的行爲。
Ø 以State/Strategy取代類型碼(ReplaceType with State/Strategy)。你有一個類型碼,它會影響類的行爲,但你無法通過繼承手法消除它。
Ø 以字段取代子類(Replace Subclass with Fields)。你的各個子類的唯一差別只在“返回常量數據”的函數身上,修改這些函數,使他們返回超類中的某個(新增)字段,然後銷燬子類。
第九章 簡化條件表達式
Ø 分解條件表達式(Decompose Conditional)。你有一個負責的條件語句(if-else-then)。
Ø 合併條件表達式(Consolidate Conditional Expression)。你有一系列條件測試,都得到相同結果。
Ø 合併重複的條件片段(Consolidate Duplicate Conditional fragments)。在條件表達式我的每個分支上有着相同的一段代碼。將這段重複代碼搬移到條件表達式之外。
Ø 移除控制標記(Remove Control Flag)。在一系列布爾表達式中,某個變量帶有“控制標記”的作用,以break語句或return語句取代控制標記。
Ø 以衛語句取代嵌套條件表達式(Replace Nested Conditional with Guard Clauses)。函數中的條件邏輯使人難以看清正常的執行路徑。使用衛語句表現所有特殊情況。
Ø 以多態取代條件表達式(Replace Conditional with Polymorphism)。你手上有個條件表達式,它根據對象類型的不同而選擇不同的行爲。將這個條件表達式的每個分支放進一個子類內的覆寫函數中,然後將原始函數聲明爲抽象函數。
Ø 引入Null對象(IntroduceNull 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)。類中的某個字段應該在對象創建時被設值,然後就不再改變,則去掉該字段的所有設值函數。
Ø 隱藏函數(Hide Method)。有一個函數,從來沒有被其他任何類用到,則將這個函數修改爲private。
Ø 以工廠函數取代構造函數(Replace Constructor with Factory Method)。你希望在創建對象時不僅僅是做簡單的構建動作,將構造函數替換爲工廠函數。
Ø 封裝向下轉型(Encapsulate Downcast)。某個函數返回的對象,需要由函數調用者執行向下轉型,則將向下轉型動作移到函數中。
Ø 以異常取代錯誤碼(Replace Error Code with Exception)。throw
Ø 以測試取代異常(Replace Exception with Test)。
第十一章 處理概括關係(繼承關係)
Ø 字段上移(Pull Up Field)。兩個子類擁有相同的字段,則將字段移至超類。
Ø 函數上移(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)。你手上有一些傳統過程化風格的代碼,將數據記錄編程對象,將大塊的行爲分成小塊,並將行爲移入相關對象之中。
Ø 將領域和表述/顯示分離(SeparateDomain from Presentation)。某些GUI類之中包含了領域邏輯,將領域邏輯分離出來,爲它們建立獨立的領域類。
Ø 提煉繼承體系(Extract Hierarchy)。你有某個類做了太多工作,其中一部分工作是以大量條件表達式完成的,則應該建立繼承體系,以一個子類表示一種特殊情況。
第十三章 重構,複用與現實
Ø 對設計模式的很多研究,都集中於良好編程風格以及程序各部分之間有用的交互模式,而這些都可以映射爲結構特徵和重構手法。
Ø 經驗是無可替代的。
Ø 研究人員的典型尷尬處境——技術的發展超前於實踐。(如果可以好好利用這一點現實或許可以開闢一番新天地)
Ø 自動化重構工具:http://st-www.cs.uiuc.edu
第十四章 重構工具
Ø Refactoring Browser
第十五章 總結
Ø 前面列出的技術僅僅是一個七點,是你登堂入室之前的大門。如果沒有這些技術,你根本無法對運行中的程序進行任何涉及上的改動。有了這些技術,你仍然做不到,但起碼可以開始嘗試了。
Ø 使重構能夠成功的,不是前面各自獨立的技術,而是這種節奏:知道何時應該使用它們、何時不應該使用,何時開始、何時停止,何時前進、何時等待。
Ø 大多時候,“得道”的標誌是:你可以自信地停止重構。如果你再也無法前進一步,也許就在你停止時遇到了這種挫折,如果代碼比重構之前好,就集成到系統中發佈成果,如果代碼沒有變好就果斷放棄這些無用的工作,回到起始點,然後爲自己學到一課而高興,不久的將來,靈感總會來的。這有點像在懸崖峭壁上的小徑行走:只要有光,你就可以前進,雖然謹慎卻仍然自信。但是一旦太陽下山,你就應該停止前進;夜晚你應該睡覺,並且相信明天早晨太陽仍然升起。
Ø 學習重構:
Ø 1、隨時挑一個目標,某個地方的代碼開始發愁了,你就應該將問題解決掉。
Ø 2、沒把握就停下來,如果代碼已經改善就發佈成果,如果沒有就撤銷所有修改。
Ø 3、學習原路返回,回到最近一個沒有出錯的狀態,然後逐一重複剛纔做過的重構想,每一次重構之後一定要運行所有測試。重現做過的重構比調試平均時間短。
Ø 4、二重奏,和別人一起重構,可以收到更好的效果。