讀書筆記_重構,改善既有代碼的設計

重構,改善既有代碼的設計

個人總結:以前對面向對象總覺得很一般的感覺,無法體會出面向對象真正帶來的便利。而對於繼承和多態,也只是瞭解知道怎麼用,但是看了這本書才真正對於繼承和多態有了進一步的理解,“原來繼承和多態是這麼用的”這種感覺,對於好的代碼應該是怎樣子的,也有了更具體一點的認知。對於以前自己寫的代碼,回想起來,根本就不是面向對象的,不堪回首……當然以後我還會不斷髮現自己以前寫的代碼很爛,這樣的話,說明我在不斷進步。

 

爲什麼重構:改善既有代碼的設計;持續糾偏和改進軟件設計;幫助發現隱藏的代碼缺陷;從長遠來看,有助於提高編程效率。(使代碼更易於理解、擴展和修改)

第一章 重構,第一個案例

任何一個傻瓜都能寫出計算機可以理解的代碼。唯有寫出人類容易理解的代碼,纔是優秀的程序員。

Ø  代碼被閱讀和修改的次數遠遠多於它被編寫的次數。

Ø  外國人代碼:各種類、接口、繼承、方法調用。每個方法做的事情少,通過類圖就可以看出邏輯。一個方法或類做的事情太多了,修改起來就很可能帶來很多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、二重奏,和別人一起重構,可以收到更好的效果。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章