本文已同步發表在CSDN:http://blog.csdn.net/wenxin2011/article/details/50790939
第1章 重構,第一個案例
- 代碼塊俞小,代碼的功能就俞容易管理,代碼的處理和移動也就俞輕松。(功能也就越單一)
- 任何不會被修改的變量都可以被當成參數傳入新的函數,至於會被修改的變量需要慎重。如果只有一個變量會被修改,可以把它當做返回值。
- 絕大多數情況下,函數應該放在它所使用的數據的所屬對象內。
- 最好不要在另一個對象的屬性基礎上運用switch語句。如果不得不使用,也應該在對象自己的數據上使用,而不是在別人的數據上使用。
- 使用繼承來適當組織類關系后,可以用多態取代switch語句。
- 一部影片可以在生命周期內修改自己的分類,一個對象卻不能在生命周期內修改自己所屬的類。
第2章 重構原則
- 三次法則:第一次做某件事時只管去做;第二次做類似的事會產生反感,但無論如何還是可以去做;第三次在做類似的是,你就應該重構。事不過三,三則重構。
- 在重構中引入間接層的某些價值:
- 允許邏輯共享
- 分開解釋意圖和實現
- 隔離變化
- 封裝條件邏輯
- 封裝條件邏輯。對象有一種奇妙的機制:多態消息,可以靈活而清晰地表達條件邏輯。將條件邏輯轉化為消息形式,往往能降低代碼的重復、增加清晰度並提高彈性。
第3章 代碼的壞味道
- 重復代碼:設法合二為一
- 同一個類的兩個函數還有相同的表達式,這時需要提煉出重復代碼。
- 兩個互為兄弟的子類內含有相同的表達式,可以提煉相同代碼,並放到父類中。如果只是代碼間相似,並非完全相同,那么可以將相似部分和差異部分拆開,構成單獨的函數。然后你可以使用模板方法的設計模式。
- 如果兩個毫不相關的類中出現重復代碼,則可以將重復代碼提煉成一個函數放到一個獨立類中或者只放在某一個類中(總之要放在合適的地方),然后其他類都去調用這個函數。
- 過長函數
一條原則:每當感覺需要以注釋來說明點什么的時候,我們就把需要說明的東西寫進一個獨立函數中,並以其用途(而非實現手法)命名。哪怕替換后的函數調用動作比函數自身還長,只要函數名稱能夠解釋其用途,我們也該毫不猶豫的那么做。關鍵不在於函數的長度,而在於函數“做什么”和“如何做”之間的語義距離。 - 過大的類
類內如果有太多代碼,也是代碼重復、混亂並最終走向死亡的源頭。最簡單的解決方案是把多余的東西消弭於類內部。如果有五個“百行函數”,它們之間很多代碼都相同,那么或許你可以把它們編程五個“十行函數”和十個提煉出來的“雙行函數”。 - 過長參數列:用對象做參數來減少參數個數
- 有了對象,就不必把函數需要的所有東西都以參數傳遞給它了,只需傳給它足夠的、讓函數能從中獲得自己需要的東西就行了。函數需要的東西多半可以在函數宿主類中找到。如果將對象傳遞給函數,大多數修改都將沒有必要,因為你很可能只需(在函數內)增加一兩條請求,就能得到更多的數據。
- 這里有一個重要的例外:有時候你明顯不希望造成“被調用對象”與“較大對象”間的某種依賴關系。這時候將數據從對象中拆解出來單獨作為參數,也很合情合理。
- 發散式變化:一個類受多種變化的影響
如果某個類經常因為不同的原因在不同的方向上發生變化,發散式變化的壞味道就出現了。針對某一外界變化的所有相應修改,都只應該發生在單一類中,而這個新類內的所有內容都應該反應次變化。 - 霰彈式修改:一種變化引發多個類相應修改
如果每遇到某種變化,你都必須在許多不同的類內做出許多小修改,你所面臨的壞味道就是霰彈式修改。這種情況下,可以把所有需要修改的代碼放進同一個類,如果沒有合適的類可放,就創造一個。 - 依戀情節
- 對象技術的全部要點在於:這是一種“將數據和對數據的操作行為包裝在一起”的技術。
- 有一種經典的氣味是:函數對某個類的興趣高過對自己所處類的興趣。這種孺慕之情最通常的焦點便是數據。
- 處理這種壞味道的原則是:判斷哪個類擁有最多被此函數實用的數據,然后就把這個函數和那些數據擺在一起。
- 最根本的原則是:將總是一起變化的東西放在一塊。
- 數據泥團:總是綁在一起的數據應該擁有屬於它們自己的對象
一個好的評判辦法是:刪掉眾多數據中的一項,其他數據有沒有因此而失去意義?如果他們不再有意義,這就是一個明確信號:你應該為它們產生一個新對象。 - 基本類型偏執:將數據組織成有意義的形式,不偏執於基本類型
- 對象技術的新手通常不願意在小任務上運用小對象。
- 如果你有一組應該總是被放在一起的字段(基本類型的數據),那么可以嘗試將這組數據放到一個單獨類中變成結構類型的數據
- switch語句
面向對象程序的一個最明顯特征就是:少用switch(或case)語句。從本質上說,switch語句的問題在於重復。面向對象中的多態概念可為此帶來優雅的解決辦法。 - 平行繼承體系
- 如果你發現某個繼承體系的類名稱前綴和另一個繼承體系的類名稱前綴完全相同,便是問到了這種壞味道。
- 消除這種重復性的一般策略是:讓一個繼承體系的實例引用另一個繼承體系的實例。
- 冗贅類
- 誇誇其談未來性
當有人說“噢, 我想我們總有一天需要做這件事”,並因而企圖以各式各樣的鈎子和特殊情況來處理一些非必要的事情,這種壞味道就出現了。 - 令人迷惑的臨時字段
- 過度耦合的消息鏈
- 中間人
- 狎昵關系
- 異曲同工的類
- 不完美的類庫
- 純稚的數據類
- 純稚的數據類是指:它們擁有一些字段,以及用於訪問(讀寫)這些字段的函數,除此之外一無長物。
- 這種類如果get/set方法均是public的,則需要引起注意,應該進行適當的封裝,而不是全部公有化。
- 被拒絕的遺贈
子類應該繼承超類的函數和數據。但如果他們不想或不需要繼承所有的函數和數據,則應該為這個子類新建一個兄弟類,把所有用不到的函數和數據放到兄弟類中,他們共享的數據和函數則放到共同的超類中。 - 過多的注釋
- 當你感覺需要撰寫注釋時,請先嘗試重構,試着讓所有注釋都變得多余
- 如果你不知道該做什么的,這才是注釋的良好運用時機。
第4章 構築測試體系
確保所有測試都完全自動化,讓他們檢查自己的測試結果。
第5章 重構列表
第6章 重新組織函數
- 提煉函數
- 動機:首先,如果每個函數的粒度都很小,那么函數被服用的機會就更大;其次,這回是高層函數讀起來就像一系列注釋;再次,如果函數都是細粒度,那么函數的覆寫也會更容易些。函數的長度不是問題,關鍵在於函數名稱和函數本體之間的語義距離。如果提煉可以強化代碼的清晰度,那么就去做,就算函數名稱比提煉出來的代碼還長也無所謂。
- 做法:創造一個新函數,根據這個函數的意圖來對它命名(以它“做什么”來命名,而不是以它“怎樣做”命名)。
- 內聯函數
- 內聯臨時變量
- 引入解釋性變量
將復雜表達式(或其中一部分)的結果放進一個臨時變量,以此變量名稱來解釋表達式用途。 - 分解臨時變量
- 移除對參數的賦值
- 以函數對象取代函數
- 替換算法
第7章 在對象之間搬移特性
- 在對象的設計過程中,“決定把責任放在哪兒”即使不是最重要的事,也是最重要的事之一。
- 搬移函數
- 搬移字段
- 提煉類
- 將類內聯化
- 隱藏委托關系
- 移除中間人
- 引入外加函數
- 引入本地擴展
第8章 重新組織數據
- 自封裝字段
- 以對象取代數據值
- 將值對象改為引用對象
- 將引用對象改為值對象
- 值對象有一個非常重要的特性:它們應該是不可變的。
- 定義了equals(),就必須同時定義hashCode()。實現hashCode()有個簡單的辦法:讀取equals()使用的所有字段的hash碼,然后對它們進行按位異或(^)操作。
- 以對象取代數組
小結:用對象替代數組的好處就是就對數組的各種操作都可以封裝起來。因此,對於那些需要各種操作的數組,最好封裝起來。
- 復制“被監視數據”
- 將單向關聯改為雙向關聯
- 將雙向關聯改為單向關聯
- 以字面常量取代魔法數
- 封裝字段
- 封裝集合
- 以數據類取代記錄
- 以類取代類型碼
- 以子類取代類型碼
- 以State/Strategy取代類型碼
- 以字段取代子類
第9章 簡化條件表達式
- 分解條件表達式
如果條件表達式中的條件太多太長,則可以把條件抽取成一個函數,增加代碼可讀性。
- 合並條件表達式
- 如果檢查條件各不相同,最終行為卻一致,就應該使用“邏輯或”和“邏輯與”將他們合並為一個條件表達式。
- 如果你認為這些檢查的確彼此獨立,的確不應該被視為同一次檢查,那么就不要使用本項重構。
- 合並重復的條件片段
- 移除控制標記
適當的運用break語句和continue語句可以取代標志位的使用 - 以衛語句取代嵌套條件表達式
- 衛語句,是指在方法最終返回語句前,加入其它返回語句(return語句)
- 拆分條件,然后加入適當的衛語句,以減少條件的嵌套層數
- 將條件反轉,然后加入適當的衛語句,以減少條件的嵌套層數
- 以多態取代條件表達式
- 引入
Null
對象 - 引入斷言
第10章 簡化函數調用
- 函數改名
- 添加參數
- 移除參數
- 將查詢函數和修改函數分離
- 令函數攜帶參數
- 以明確函數取代參數
- 保持對象完整
- 有時候,你會將來自同一對象的若干項數據作為參數,傳遞給某個函數。這樣做的問題在於:萬一將來被調用的函數需要新的數據項,你就必須查找並修改對此函數的所有調用。如果你把這些數據所屬的整個對象傳給函數,可以避免這種尷尬的處境,因為被調用函數可以向那個參數對象請求任何它想要的信息。
- 如果被調用函數使用了來自另一個對象的很多項數據,這可能意味該函數實際上應該被定義在那些數據所屬的對象中。
- 以函數取代參數
- 如果函數可以通過其他途徑獲得參數值,那么它就不應該通過參數取得該值。
- 過長的參數列會增加程序閱讀者的理解難度,因此我們應該盡可能縮短參數列的長度。
- 引入參數對象
某些參數總是很自然地同時出現,這時,可以用一個對象取代這些參數。 - 移除設值函數
- 類中的某個字段應該在對象創建時被設值,然后就不再改變。這樣的字段應該去掉其對應的設值函數。
- 如果你為某個字段提供了設值函數,這就暗示這個字段值可以被改變。如果你不希望在對象創建之后此字段還有機會被改變,那就不要為它提供設值函數(同時該字段設為
final
)。這樣你的意圖會更加清晰,並且可以排除其值被修改的可能性——這種可能性往往是非常大的。
- 隱藏函數
- 不會被其他任何類用到的函數的訪問類型應該為
private
。 - 盡可能降低所有函數的可見度。
- 不會被其他任何類用到的函數的訪問類型應該為
- 以工廠函數取代構造函數
- 在派生子類的過程中以工廠函數取代類型碼。
- 如果要根據類型來創建不同的對象,這些對象有共同的父類,則可以在父類中添加一個工廠函數來創建不同的對象。
- 封裝向下轉型
如果某個函數返回的對象,需要由函數調用者執行向下轉型,則可以將向下轉型動作移到函數中。 - 以異常取代錯誤碼
- 以測試取代異常
第11章 處理概括關系
-
字段上移
- 如果兩個子類擁有相同的字段,則將該字段移至超類。
- 如果這些字段是
private
的,你必須將超類的字段聲明為protected
,這樣子類才能引用它。
-
將函數上移
- 有些函數在各個子類中產生完全相同的結果,則將該函數移至超類。
- 如果你使用的是一種強類型語言,而待提升函數又調用了一個只出現於子類而未出現於超類的函數,你可以在超類中為被調用函數聲明一個抽象函數。
-
構造函數本體上移
如果在各個子類中擁有一些構造函數,並且它們的本體幾乎完全一致。那么可以在超類中新建一個構造函數,並在子類構造函數中調用它。 -
函數下移
如果超類中的某個函數只與部分(而非全部)子類有關,則將這個函數移到相關的那些子類中去。 -
字段下移
超類中的某個字段只被部分(而非全部)子類用到,則將這個字段移到需要它的那些子類去。 -
提煉子類
如果類中的某些特性只被某些(而非全部)實例用到,則新建一個子類,將上面所說的那一部分特性移到子類中。 -
提煉超類
如果兩個類有相似特性,則為這兩個類建立一個超類,將相同特性移至超類。 -
提煉接口
如果某各類在不同環境下扮演截然不同的角色,使用接口就是個好主意。 -
折疊繼承體系
如果超類和子 類之間無太大區別,則將它們合為一體。 -
塑造模板函數
你有一些子類,其中相應的某些函數以相同的順序執行類似的操作,但各個操作的細節上有所不同。將這些操作分別放進獨立函數中,並保持它們都有相同的簽名,於是原函數也就變得相同了。然后將原函數上移至超類。 -
以委托取代繼承
-
以繼承取代委托
第12章 大型重構
- 梳理並分解繼承體系
- 如果某個繼承體系同時承擔兩項責任,則可以建立兩個繼承體系,並通過委托關系讓其中一個可以調用另一個。
- 要指出繼承體系是否承擔了兩項不同的責任並不困難:如果繼承體系中的某一特定層級上的所有類,其子類名稱都以相同的形容詞開始,那么這個體系可能就是承擔着兩項不同的責任。
- 將過程化設計轉化為對象設計
- 將領域和表述/顯示分離
某些GUI類之中包含了領域邏輯,將領域邏輯分離出來,為它們建立獨立的領域類。 - 提煉繼承體系
如果你有某個類做了太多工作,其中一部分工作是以大量條件表達式完成的,那么可以建立繼承體系,以一個子類表示一種特殊情況。
第13章 重構,復用與現實
- 現實的檢驗
- 為什么開發者不願意重構他們的程序
- 再論現實檢驗
- 重構的資源和參考資料
- 從重構聯想到軟件復用和技術傳播
第14章 重構工具
- 使用工具進行重構
- 重構工具的技術標准
- 重構工具的實用標准
第15章 總結
要點列表
- 如果你發現自己需要為程序添加一個特性,而代碼結構使你無法很方便地達成目的,那就先重構那個程序,使特性的添加比較容易進行,然后再添加新特性。
- 重構前,先檢查自己是否有一套可靠的測試機制。這些測試必須有自我檢驗能力。
- 重構技術就是以微小的步伐修改程序。如果你犯下錯誤,很容易便可發現它。
- 任何一個傻瓜都能寫出計算機可以理解的代碼。唯有寫出人類容易理解的代碼,才是優秀程序員。
- 重構(名詞):對軟件內部結構的一種調整,目的是在不改變軟件可觀察行為的前提下,提高其可理解性,降低其修改成本。
- 重構(動詞):使用一系列重構手法,在不改變軟件可觀察行為的前提下,調整其結構。
- 事不過三,三則重構
- 不要過早發布接口。請修改你的代碼所有權政策,使重構更順暢。
- 當你感覺需要撰寫注釋時,請先嘗試重構,試着讓所有注釋都變得多余。
- 確保所有測試都完全自動化,讓它們檢查自己的測試結果。
- 一套測試就是一個強大的
bug
偵測器,能夠大大縮減查找bug
所需要的時間。 - 頻繁地運行測試。每次編譯請把測試也考慮進去——每天至少執行每個測試一次。
- 每當你收到
bug
報告,請先寫一個單元測試來暴露這只bug
。 - 編寫未臻完善的測試並實際運行,好過對完美測試的無盡等待。
- 考慮可能出錯的邊界條件,把測試火力集中在那兒。
- 當事情被大家認為應該會出錯時,別忘了檢查是否拋出了預期的異常。
- 不要因為測試無法捕捉所有
bug
就不寫測試,因為測試的確可以捕捉到大多數bug
。
對於比較簡單或者筆者暫時還沒有很好理解的部分,只是一筆帶過;而對於筆者在工作中遇到過相似問題的地方,則摘錄地比較詳細。《重構:改善代碼的既有設計》一書非常經典,需要閱讀、實踐、再閱讀、再實踐。如果你也是一個注重代碼簡潔、美觀和高效的程序員,那么這本書一定不能錯過,深入閱讀一定能提升代碼的質量。本文會在后續繼續更新完善,剔除簡單和不常用的部分,完善和保留實用的部分。
聲明:本文歡迎轉載和分享,但是請尊重作者的勞動成果,轉載分享時請注明出處:http://www.cnblogs.com/davidsheh/p/5239745.html 。同時,碼字實在不易,如果你覺得筆者分享的筆記對你有點用處,請順手點擊下方的 推薦,謝謝!