關於重構的總結

《重構_改善既有代碼的設計》這本書還沒有讀完,因爲內容太多了。但是項目已重構完成。因此,有一些感悟,順便查閱一些資料,寫下這篇文章,加深一下自己對重構的認知。

認識重構

所謂重構,就是在不改變軟件系統外部行爲的前提下,改善它的內部結構。

重構是對軟件內部的一種調整,目的是在不改變軟件可觀察行爲的前提下,提高可理解性,降低其修改成本。

重構是一種經千錘百煉形成的有條不紊的程序整理方法,可以最大限度地減少整理過程中引入錯誤的機率。

本質上說,重構就是在代碼寫好之後改進它的設計。

重構不會改變軟件可觀察的行爲 —— 重構之後軟件功能一如以往。

爲什麼要重構

重構有風險,它必須修改運行中的程序,這可能引入一些不易察覺的錯誤。那麼,爲什麼我們還要重構呢?

我們希望的程序是這樣的:

  • 容易閱讀
  • 所有的邏輯都只在唯一地點指定 (單一原則,去重,提取)
  • 新的改動不會危及現有行爲 (可拓展性,重用性)
  • 儘可能簡單表達條件邏輯

重構是這樣一個過程:它在一個目前可運行的程序上進行,在不改變程序行爲的前提下使其具備上述的美好性質,使我們能夠繼續保持高速開發,從而增加程序的價值。

重構的目的

  • 重構改進軟件設計
  • 重構使軟件更容易理解
  • 重構幫助找到bug
  • 重構提高編程速度

總結,爲了高效率的編程,爲了減少bug率,爲了提高代碼質量;越是複雜的項目,重構的好處就越明顯。

重構有助於軟件的迭代開發和二次開發。

重構的原則

  • 隨時可以停止
  • 對外表現的功能一致,相同輸入得到相同輸出
  • 重構時不要添加新的功能
  • 合適就行,考慮具體情況,不要太執着於重構
  • 需要對重構的代碼進行測試
  • 符合編程語言代碼規範

何時重構

  • 三次法則:事不過三,三則重構
  • 添加功能時重構
  • 修補錯誤時重構
  • 複審代碼時重構

重構的難點

  • 數據庫:程序與數據庫結構緊密耦合在一起;數據遷移
  • 修改接口:謹慎修改接口,如果接口已發佈,必須維護舊的接口

何時不該重構

  • 重寫:現有代碼根本不能正常運作
  • 代碼太混亂
  • 項目已接近最後期限

關於測試

  • 確保所有測試都完全自動化,讓它們檢查自己的測試結果
  • 考慮可能出錯的邊界條件,把測試火力集中在那兒
  • 編寫未完善的測試並運行,好過對完美測試的無盡等待
  • 當事情被認爲應該出錯時,別忘了檢查是否拋出了預期的異常
  • 不要因爲測試無法捕捉所有的bug就不寫測試,因爲測試的確可以捕捉到大多數bug

代碼的壞味道

名稱 備註
重複代碼 同一個類的兩個函數有相同表達式,提取方法到超類或獨立類
過長函數 當需要用註釋來說明一段代碼時,就需要把這部分代碼寫入一個獨立的函數中
過大的類 爲每一種使用方式提取出一個接口
過長的參數列 將參數設置爲對象
發散式變化 一個類受到多種變化的影響
散彈式修改 一個變化引起多個類修改
依戀情結 一個函數對某個類的興趣高於對自己所處類的興趣,通常是過多訪問其它類的數據
數據泥團 有些數據經常一起出現,比如兩個類具有相同的字段、許多函數有相同的參數
基本類型偏執 使用類往往比使用基本類型更好
switch 驚悚現身 面向對象中的多態概念可爲此帶來優雅的解決方法
平行繼承體系 每當爲某個類增加一個子類,必須也爲另一個類相應增加一個子類
冗餘類 如果一個類沒有做足夠多的工作,就應該消失
誇誇其談未來性 有些內容是用來處理未來可能發生的變化,但是往往會造成系統難以理解和維護
令人迷惑的暫時字段 某個字段僅爲某種特定情況而設,這樣的代碼不易理解,因爲通常認爲對象在所有時候都需要它的所有字段
過度耦合的消息鏈 一個對象請求另一個對象,然後再向後者請求另一個對象,然後...,這就是消息鏈
中間人 中間人負責處理委託給它的操作,如果一個類中有過多的函數都委託給其它類,那就是過度運用委託
狎暱關係 兩個類多於親密,花費太多時間去探討彼此的 private 成分。
異曲同工的類 兩個函數做同一件事,卻有着相同的簽名
不完美的類庫 類庫往往不可能滿足我們所有的工作
純稚的數據類 它只擁有一些數據字段。
被拒絕的饋贈 子類繼承超類的所有函數和數據,但是它只想要一部分。
過多的註釋 糟糕的代碼導致過多的註釋

重構的方法

重新組織函數

名稱 解釋 動機
提煉函數 將這段代碼放進一個獨立函數中,並讓函數名稱解釋該函數的用途。 函數過長;多次出現的代碼塊
內聯函數 在函數調用點插入函數本體,然後移除該函數。 重用率不高且簡單的代碼、太多間接層
內聯臨時變量 將所有對該變量的引用動作,替換爲對他賦值的那個表達式自身。 臨時變量妨礙到其他重構手法時
以查詢取代臨時變量 將表達式提煉到一個獨立的函數中,將有改表達式的地方替換爲新函數 類中可全局查詢,表達式可能變動時
引入解釋變量 將一個複雜的表達式或其一部分的結果放進臨時變量,以此變量來解釋表達式的用途。 表達式非常複雜、難以閱讀時;表達式太長時
分解臨時變量 針對每次賦值,創造一個獨立、對應的臨時變量 臨時變量有不同用途且被多次賦值時;臨時變量承擔多個責任時
移除對參數的賦值 以一個臨時變量取代該參數的位置 代碼對參數進行賦值時
以函數對象取代函數 將這個函數放進一個單獨的對象中,如此一來局部變量就成了對象內的字段,然後就可以將這個大型函數分解爲多個小型的函數。 一個大型的函數,有太多局部變量時
替換算法 將函數本體替換爲另一種算法 想要將某個算法替換爲另一個更清晰的算法時

在對象之間搬移特性

名稱 解釋 動機
搬移函數 將函數遷移到最常引用的類中,舊函數變成單純的委託或者移除掉。 一個類有太多行爲,或與另一個類有太多合作形成高度耦合時
搬移字段 將字段遷移到目標類中,將源字段所有引用都改用爲目標類的新字段 在其所駐之類之外的另一個類更多的使用到該字段時
提煉類 建立新類,將相關的字段和函數遷移到新類中 某個類做了應該由兩個類做的事時
將類內聯化 將這個類的所有特性搬移到另一個類中,然後移除原類 當一個類不再承擔足夠責任、不再有單獨存在的理由時
隱藏“委託關係” 在服務類上建立客戶所需的所有函數,用以隱藏委託關係 客戶通過一個委託類調用另一個對象
移除中間人 讓客戶直接調用委託類 某個類做了過多的簡單的委託動作
引入外加函數 在客戶類中建立一個函數,並以第一參數形式傳入一個服務類實例。 你需要爲提供服務的類增加一個函數,但你無法修改這個類
引入本地擴展 建立一個新類,使他包含這些額外函數 你需要爲提供服務的類提供一些額外的函數,但你無法修改這個類

重新組織數據

名稱 解釋 動機
自封裝字段 爲這個字段設置set/get函數,並且以這些函數來訪問字段 降低與字段之間的耦合
以對象取代數據值 將數據項變成對象 一些數據項需要和其他數據和行爲一起使用纔有意義
將值對象改成引用對象 將這個值對象變成引用對象 給對象增加一些可修改數據,並確保對任何一個對象的修改都能影響到所有的引用此對象的地方時
將引用對象改爲值對象 將它變成一個值對象 一個引用對象很小且不可變且不易管理時
以對象取代數組 已對象替換數組,對於數組的每一個元素都以一個字段表示 一個數組中的元素各自代表不同東西時
賦值“被監視數據” 將數據複製到一個領域對象中,建立Observer模式,用以同步領域對象和GUI對象內的重複數據 一些領域對象數據置身於GUI控件中,而領域對象函數需要訪問這些數據
將單向關聯改爲雙向關聯 添加一個反向指針,並使修改函數同時更新兩條連接 兩個類都需要使用到對方特性時
將雙向關聯改爲單向關聯 去除不必要的關聯 雙向關聯的類變成單向依賴時
以字面常量取代魔法數 爲字面數值設置常量,並將數值替換爲這個常量 如果有特殊意義的字面數值時
封裝字段 將它聲明爲private,並提供相應的訪問函數 類中存在public字段時
封裝集合 讓函數返回只讀副本,並在這個類上提供增加/刪除集合元素的函數 降低集合擁有者與用戶之間的耦合
以數據類取代記錄 爲該記錄創建一個“啞”數據對象 面對一個遺留程序程序時;需要與傳統API交流時;處理從數據庫讀出來的記錄時
以類取代類型碼 以一個新的類替換該類型數值碼 類中有數值類型碼但不影響類的行爲
以子類取代類型碼 以子類取代類型碼 有一個不可變的類型碼,且會影響到類的行爲時
以 State/Strategy 取代類型碼 以狀態取代類型碼 有一個不可變的類型碼,且會影響到類的行爲,但無法用繼承手法消除時
以字段取代子類 修改這些函數,使他們返回超類的某個字段,然後銷燬子類 當各個子類唯一的差別只在“返回數據常量”的函數身上時

簡化條件表達式

名稱 解釋 動機
分解條件表達式 從if/then/else三個段落中分別提煉出獨立函數 當條件語句太複雜時
合併條件表達式 將處理結果一致條件合併爲一個條件,並提煉成爲一個獨立的函數 有一系列條件,得到相同的處理方式時
合併重複的條件片段 將這段重複的代碼搬移到條件表達式之外 在條件表達式的每一個分支有着相同的一段代碼時
移除控制標記 以break或return取代控制標誌 某個變量帶有“控制標記”的作用
以衛語句取代嵌套條件表達式 時使用衛語句表現所有的特殊情況 當條件邏輯有太多嵌套,難以看清執行路徑時
以多態取代條件表達式 將條件表達式的每一個分支放進一個子類內的複寫函數中,然後將原始函數聲明爲抽象函數 有一個條件表達式,根據對象類型的不同而選擇不同的行爲時
引入Null對象 將null值替換爲null對象 你需要再三檢查某對象是否爲null時
引入斷言 以斷言明確表現這種假設 某一段代碼需要對程序狀態做出某種假設時

簡化函數調用

名稱 解釋 動機
函數改名 修改函數名稱 函數名稱未能揭示函數的用途時
添加參數 爲函數添加一個參數對象參數,讓該對象帶進函數所需的信息 某個函數需要從調用端得到更多的信息
移除參數 將該參數去掉 函數本體不再需要某個參數時
將查詢函數和修改函數分離 建立兩個不同的函數,其中一個負責查詢,另一個負責修改 某個函數既返回對象狀態值,又修改對象狀態時
令函數攜帶參數 建立單一函數,以參數表達那些不同的值 若干函數做了類似的工作,但函數本體中卻包含了不同的值
以明確函數取代參數 針對參數的每一個可能值,建立一個獨立的函數 有一個函數,行爲的區別完全取決於不同的參數
保持對象完整 改爲傳遞整個對象 當需要從某個對象取出若干值作爲某個函數的參數時
以函數取代參數 讓參數接受者去除該項參數,並直接調用前一個函數 當一個函數的返回值作爲另一個函數的參數,且另一函數能調用該函數時
引入參數對象 以一個對象取代這些參數 某些參數總是很自然地同時出現時
移除設值函數 去掉該字段的所有設值函數 類中某個字段應該在該對象創建時被設值,然後不再改變
隱藏函數 將這個函數設置爲private 有一個函數,從來沒有被其他任何類用到
以工廠函數取代構造函數 將構造函數替換爲工廠函數 當創建對象時不僅僅是需要做簡單的構建動作時
封裝向下轉型 將向下轉型動作移到函數中 某個函數轉型的對象,需要由函數調用者向下轉型,如抽象類強制轉換爲具體類時
以異常取代錯誤碼 改用異常 某個函數返回一個特定的代碼,用以表示某種錯誤情況
以測試取代異常 修改調用者,使它在調用函數之前先做檢查 面對調用者可以預先檢查的條件,你拋出了異常時

處理概括關係

名稱 解釋 動機
字段上移 將該字段移至超類 子類擁有相同的字段時
函數上移 將該函數移至超類 有些函數,在子類中產生相同的結果
構造函數本體上移 在超類中新建一個構造函數,並在子類構造函數中調用它 各個子類的構造函數擁有幾乎一致的本體時
函數下移 將函數移到相關子類中去 超類中的某個函數只與部分子類有關
字段下移 將字段移到需要它的那些子類中去 超類中的某個字段只被部分子類用到
提煉子類 新建一個子類,將上面所說的那一部分特性移到子類中去 類中的某些特性只被某些實例用到
提煉超類 爲相似的類建議一個超類,將相同的特性移至超類 一些類有相似的特性時
提煉接口 將相同的子集提煉到一個獨立的接口中 一些類具有相同的行爲時
摺疊繼承體系 將子類和超類合爲一體 子類和超類無太大區別時
塑造模板函數 將這些操作分別放進獨立函數中,並保持他們都有相同的簽名,於是原函數也變得相同了,再將原函數移至超類 一些子類相應的某些函數以相同的順序執行某些操作,但各個操作的細節上有些不同時
以委託取代繼承 在子類新建一個字段用以保存超類,調整子類函數,改爲委託超類,然後去掉兩者之間的繼承關係 某個子類只使用了超類接口中的一部分,或是根本不需要繼承而來的數據時
以繼承取代委託 讓委託繼承受託類 兩個類之間存在委託關係,且極簡的委託函數太多時

大型重構

四個大型重構

名稱 解釋 動機
梳理並分解繼承體系 建立兩個繼承體系,並通過委託關係讓其中一個可以調用另一個 某個繼承體系同時承擔兩項責任
將過程設計轉化爲對象設計 將數據記錄變成對象,將大塊的行爲分成小塊,並將行爲移入相關對象中 有一些傳統過程化的代碼
將領域和表述/顯示分離 將領域邏輯分離出來,爲他們建立獨立的領域類 某些GUI類之中包含了領域邏輯
提煉繼承體系 建立繼承體系,以一個子類表示一種特殊情況 某個類做了太多工作,其中一部分工作是以大量條件表達式完成的

總結

總之,重構得分情況,需要了解動機。某些重構手法是對立的,這時候就更加需要具體情況具體分析了,適合的纔是最好的。

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