重構改善既有代碼
- 第一次做某件事情的時候盡管去做,第二次做類似的事會產生反感,第三次再做類似的事,你就應該重構。
- 小型函數優美動人
- 一個類最好是常量類,任何的改變都是調用該類本身的接口實現。
0 壞代碼的味道
1、重復代碼
- Duplicated Code
- 同一類中的兩個函數含有相同的表達式,提取到方法
- 互為兄弟的子類含有相同表達式,將兩個子類的相同代碼提取方法推入超類
- 如果有相似代碼,通過提煉方法將相似和差異部分分割開,並使用疏鑿模板方法,並將模板方法上移到超類中。
- 如果兩個毫不相關的類出現重復代碼,將重復代碼提煉到一個提煉類中,兩個類都使用這個提煉類。
2、過長函數
- Long Method
- 間接層所能帶來的全部利益——解釋能力、共享能力、選擇能力
- 小函數的價值是巨大的
- 每當感覺需要注釋來說明什么的時候,就需要把說明的東西寫進一個獨立函數中,並以其用途命名。
- 函數內有大量的臨時變量和參數。需要運用提煉方法,可以將臨時變量作為參數傳入,也可以使用以查詢替代臨時變量,當方法參數特別多的時候可以提煉參數類,傳遞參數類實體。如果這么做還有很多的參數,那么就應該用方法對象來取代方法了。
- 選擇提煉哪一段代碼
- 尋找注釋,有注釋的地方都在提醒你需要提煉方法了,注釋名稱就是很好的方法名
- 條件表達式和循環也是型號,可以用 分解條件表達式,循環應該將循環中的代碼提煉到獨立函數中。
3、過大的類
- Large Class
- 如果單個類做太多是事情,往往會導致出現太多的實例變量,一旦如此,重復代碼就接踵而至了。
- 可以使用提煉類將幾個變量和方法提煉出來,如果數個變量存在着相同的前綴或字尾,就以為着有機會可以把它們提煉到某個組件中。如果這個組件適合一個子類,還可以使用提煉子類。
- 如果一個擁有太多代碼,可以先確定客戶端如何使用它們,然后運用提煉接口,為每一種使用方法提煉出一個接口,這可以看清楚如何分解這個類。
- 如果超大類是一個GUI類,可以把數據和行為移到一個獨立的領域對象去,可能需要兩邊保留一些重復代碼,並保持兩邊同步。
4、過長的參數列
- Long Parameter List
- 如果向已有的對象發出一條請求就可以取代一個參數,那么就可以使用用方法取代參數方法。
- 還可以使用保持整個對象,傳遞整個對象,
- 提煉參數對象
- 造成函數關聯需要慎重考慮
5、發散式變化
- Divergent Chane
- 軟件再怎么說就應該是軟的,一旦需要修改,希望能夠跳到系統的某一點,只在該處做修改。如果不能的化就有一種刺鼻味道了。
- 某個類經常因為不同原因在不同不同方向上發生變化發散式變化就出現了,
- 一旦出現這種發散式變化那么就需要將對象分解成多個對象或者會更好,當出現多個類后還可以提煉超類等。
6、霰彈式修改
- Shotgun Surgery
- 正對某一項變化需要在許多不同類種做出需要小修改,所面臨的味道就是霰彈式修改,
- 這種情況應該使用移動方法和移動字段,把所有修改的代碼放進同一個類,如果沒有現存的類可以按值這些代碼就創造一個,使用內聯類可以將一系列相關行為放進同一個類。
- 這也可能造成少量的發散式變化,
7、依戀情結
- Feature Envy
- 對象技術的全部要點在於:這是一種將數據和對數據的操作行為包裝在一起的技術,有一中經典的氣味是:函數對某個類的興趣高於對自己所處類的興趣。
- 使用移動方法把某些方法移動帶它該去的地方,有的時候還需要提煉方法
- 如果某個函數需要需要幾個類的功能,判斷哪個類擁有最多被此函數使用的數據,然后就把這個函數和那些數據擺在一起,可以先將函數分解成多個較小函數分別置於不同地點。
- 將總是一起變化的東西放在一塊,數據和引用這些數據的行為總是一起變化的。
- 策略和訪問者模式可以輕松修改函數行為,付出了多一層的代價
8、數據泥團
- Data Clumps
- 數據項會成群結隊出現。
- 如果刪除總舵數據中的一項,其他數據有沒有失去意義,如果它們不再有意義,就是一個明確的信號,應該產生一個新對象。
9、基本類型偏執
- Primitive Obsession
- 結構類型允許你將數據組織成有意義的形式,對象的極大價值在於打破了橫亘於基本數據和較大類之間的界限。
- 積極的使用使用對象替換數據值,用類替換類型碼,用狀態/策略模式替代類型碼
10、swithc驚悚現身
- Switch Statements
- 面向對象程序的最明顯特征就是少用switch,使用switch的問題在於重復,在修改上,如果switch散布於不同地點,就要添加新的case子句
- 如果看到switch語句的時候需要考慮用多態來替換它,問題在於多態出現在哪兒
- 使用提煉函數將switch提煉到獨立函數中,再用移動方法將它搬移到需要多態性的類中,用子類替代類型碼或者使用state/strategy替代類型碼,完成之后再用使用多態替代條件。
11、平行繼承體系
- Parallel Inheritance Hierarchies
- 如果為某個類增加一個子類的時候必須要為另一類相應增加一個子類。
- 如果某個繼承體系的類名稱前綴和兩一個繼承體系的類的名稱前綴完全相同
- 讓一個繼承體系的實例引用另一個繼承體系的實例,再使用移動方法和字段,就可以將引用端的繼承體系消除。
12、冗贅類
- Lazy Class
- 創建的每個類都有人去理解它維護它,如果一個類不值得其身價就應該消失。
13、誇誇其談的未來性
- Speculative Generality
- 總有一天需要做這件事,企圖以各式各樣的勾子和特殊情況來處理一些非必要事情會造成程序難以理解。不需要
14、令人迷惑的暫時字段
- Temporary Field
- 某個實例變量僅為某種特定情況而設置。
- 使用提煉類給這些孤兒創造一個家,然后把所有和這個變量相關的代碼都放進這個新家,還可以使用空對象方法創建一個空對象。
15、過度耦合的消息鏈
- Message Chains
- 一個對象請求一個對象,然后后者請求另一個對象,等等
- 使用隱藏委托。
16、中間人
- Middle Man
- 對象的基本特征之一就是封裝,對外部世界隱藏內部細節,封裝往往伴隨着委托,但有可能過度使用委托,移除中間人
17、狎昵關系
- Inappropriate Intimacy
- 兩個類過於親密,移動方法和字段讓他們划清界限。如果划清不了就使用提煉類讓他們融為一體吧
18、異曲同工類
- Alternative Classes with Different Interfaces
- 重命名方法,提煉子類
19、不完美的庫類
- Incomplete Library Class
- 給庫類加入新的方法,外部方法和本地擴展。
20、純稚的數據類
- Data Class
- 不會說話的數據容器一定被其他類過分的操控着,運用封裝字段封裝,移動設置方法,移動方法,提煉方法。
21、被拒絕的遺贈
- Refused Bequest
- 子類不願全部繼承,為這個子類創建一個兄弟類,在運用下移方法和字段把用不到的函數下推給那個兄弟,這樣一來,超類就只持有所有子類共享的東西。
- 用委托替換繼承
22、過多注釋
- Comments
- 提煉方法。
1 重新組織函數
對函數的重構方法
1、提煉函數
- ExtractMethod
- 動機
- 每個函數的顆粒度都比較小,高層函數讀起來就像是注釋
- 顆粒度比較小覆寫也比較容易
- 什么時候需要提煉函數
- 當函數體的語義與函數名稱偏離的時候就需要提取
- 怎么提取
- 將代碼提取出來用函數的意圖來命名(做什么)
- 如果該代碼段中有讀取或改變臨時變量
- 該臨時變量在原函數中有沒有使用,
- 優先考慮用查詢取代臨時變量
- 沒有直接將臨時變量的聲明移植到函數體中
- 在函數體之前使用,作為參數傳入
- 在函數體之后使用,作為函數返回值返回
- 之前之后都使用,作為參數傳入,在作為返回值返回
- 該臨時變量在原函數中有沒有使用,
- 如果臨時變量非常多,
- 需要考慮這個函數體是否真的屬於這個類
- 以查詢替代臨時變量
2、內聯函數
- Inline Method
- 什么時候需要內聯
- 當函數的本體和名稱同樣清楚易懂的時候
- 當有一大群組織不太合理的函數,想重構的時候,將一大群函數內聯然后重新提取
- 有太多的間接層,所有函數似乎都是對另一個函數的簡單委托
- 怎么內聯
- 檢查函數,確定它不具有多態。
- 找出該函數的所有引用點,用函數體替換(最好用文本查找的方式找)
3、內聯臨時變量
- Inline Temp
- 動機
- 什么時候做
- 有一個臨時變量,只被簡單表達式賦值一次,而它妨礙其他重構手法
- 怎么做
4、以查詢取代臨時變量*
- Replace Temp with Query
- 動機
- 臨時變量是暫時的,如果這個臨時變量需要被使用多次就考慮需要用查詢取代,這邊的查詢可以直接使用.net中的屬性。
- 臨時變量會驅使函數變長,如果變成查詢,類中的其他成員也可以訪問。
- 什么時候需要查詢取代
- 用一個臨時變量保存其某一表達式的運算結果,需要一個查詢函數取代臨時變量
- 怎么取代
- 需要分解臨時變量(臨時變量被賦值超過一次),以查詢取代臨時變量,然后再替換臨時變量
- 首先應該將查詢設置為私有的,當日后需要的時候再開放保護。
- 不用考慮細微的性能問題,因為首先需要良好的架構才能使得程序正常運行。然后再考慮性能問題。
5、引入解釋性變量
- Introduce Explaining Variable
- 在引入解釋性變量之后,可以使用導出方法或者用查詢取代臨時變量將臨時變量替換掉。

- 動機
- 使得復雜表達式可以閱讀和管理
- 什么時候需要引入
- 有一個復雜的表達式
- 怎么引入
- 講一個復雜表達式(或一部分)的結果放進一個臨時變量,以此變量名稱來解釋表達式的用途
- 與提煉函數的區別
- 再提煉函數需要花費更大的工作量的時候
6、分解臨時變量
- Split Temporary Variable
- 動機
- 如果一個臨時變量承擔太多的職責,會使得閱讀者糊塗
- 什么時候分解
- 程序中有某個臨時變量被賦值超過一次,它既不是循環變量也不是收集計算結果。
- 怎么分解
- 修改臨時變量的名稱並聲明為常量
7、移除對參數的賦值*
- Remove Assignments to Parameters
- 這邊的是針對函數參數體成員
- 對參數的賦值的想法是比較危險的,一旦為參數進行賦值如果混淆值類型和引用類型非常容易產生不易察覺的錯誤。

- 動機
- 因為面向對象的方式,所以數值類型的改變並不會改變原來傳入的值,但是引用類型就會變化
- 導致混用按值傳遞和按引用傳遞
- 什么時候移除
- 代碼對函數的一個參數進行賦值時
- 怎么移除
- 通過建立一個臨時變量,對臨時變量進行修改,然后返回臨時變量。
- 如果需要返回一大堆函數,可以將返回的一大堆函數變成一個單一的對象,或者為每個返回值設置一個獨立函數。
- 還可以在函數的每個參數中增加一個const,這個方法只是在函數體較長的時候才可以使用。
8、以函數對象取代函數
- Replace Method with Method Object
- 動機
- 小型函數優美動人
- 什么時候取代
- 有一個大型函數,對其中的局部變量的使用無法采用提煉方法的手段
- 怎么提取
- 建立一個新類,將所有的局部變量變成字段,然后將原函數體中的邏輯變成方法。
9、替換算法
- Substitute Algorithm
- 動機
- 發現一個算法的效率更高的時候
- 什么時候替換
- 想法把某個算法換成另一個更為清晰的算法
2 在對象之間搬移特性
在面向對象的設計中,決定把責任放在哪里。
先使用移動字段,在移動方法
1、搬移函數
- Move Method
- 動機
- 一個類與另一個類高度耦合,就會搬移函數,通過這種手段,可以使得類更加簡單。
- 什么時候搬移
- 有個函數與其所屬類之外的另一個類有更多的交流。
- 當不能肯定是否需要移動一個函數,需要繼續觀察其他函數,先移動其它函數就會使決定變得容易一些。
- 怎么搬移
- 檢查所有字段,屬性和函數,考慮是否應該被搬移
- 在該函數最常用引用中建立一個有類似行為的新函數
- 將舊函數變成一個單純的委托函數,或是將舊函數完全移除。
- 有多個函數使用這個需要搬移的特性,應考慮使用該特性的所有函數被一起搬移。
- 檢查所有子類和超類,看看是否有該函數其他聲明
- 如果目標函數使用了源類中的特性,可以將源對象的引用當作參數(多個參數或則存在方法需要調用),傳給新建立的目標函數。
- 如果目標函數需要太多源類特性,就得進一步重構,會將目標函數分解並將其中一部分移回源類。
2、搬移字段
- Move Field
- 動機
- 隨着項目類的增加和擴充,有一些字段放在原來的類中已經不太合適
- 什么時候搬移
- 某個字段在另一個類中被更多的用到
- 怎么搬移
- 修改源字段的所有用戶,令它們改用新字段
- 決定如何在源對象中引用目標對象,方法,新建字段引用
- 新類中自我封裝SetValue, GetValue。
3、提煉類*?
- Extract Class
- 動機
- 將復合類的職責提煉出新的類
- 或者需要將類的子類化,分解原來的類
- 什么時候提煉
- 某個類做了應該由兩個類做的事
- 怎么提煉
- 建立一個新類,將相關的字段和函數從舊類搬移到新類
- 有可能需要一個雙向連接, 但是在真正需要它之前,不要建立從新類往舊類的連接,如果建立起雙向連接,檢查是否可以將它改為單向連接。
4、將類內聯化
- Inline Class
- 動機
- 一個類不再承擔足夠責任,不再由單獨存在的理由。
- 什么時候內聯
- 某個類沒有做太多的事情
- 怎么內聯
- 將這個類是多有特性搬移到另一個類中,然后移除原類
- 修改所有源類引用點,改而引用目標類
5、隱藏“委托關系”
-
Hide Delegate
-
局限性是每當客戶要使用受托類的新特性時,就必須在服務段添加一個簡單委托函數,受托類的特性越來越多,這一過程會越來越痛苦。
-

-
簡單委托關系

-
-
動機
- 封裝意味着每個對象都應該盡可能少的了解系統的其他部分,
- 如果客戶調用對象字段得到另一個對象,然后再調用后者的函數,那么客戶就必須知道這一層關系。將委托關系隱藏起來不會波及客戶。
-
什么時候隱藏
- 客戶通過一個委托類來調用另一個對象
-
怎么隱藏
- 在服務類上建立客戶所需的所有函數,用以隱藏委托關系

- manager=john.getDepartment().getManager();隱藏=>manager=john.getManager();隱藏了調用關系。
6、移除中間人
- Remove Middle Man
- 與隱藏委托關系相反

- 動機
- 針對隱藏委托的局限性,當委托的方法越來越多時,服務類就完全變成一個中間人,此時應該讓客戶直接調用受托類。
- 什么時候移除
- 某個類做了過多的簡單委托動作
- 怎么移除
- 讓客戶直接調用受托類
7、引入外加函數
- Introduce Foreign Method
- 動機
- 發現一個好用的工具類不能修改工具類,添加方法
- 但外加函數終歸是權益之計,
- 什么時候需要引入外加函數
- 需要為提供服務的類增加一個函數,但無法修改這個類。
- 怎么引入
- 在客戶類中建立一個函數,並以第一參數形式傳入一個服務類實例
8、引入本地擴展
- Introduce Local Extension
- 動機
- 在不能修改的類中添加方法,方法的數量超過2個的時候外加函數難以控制,需要將函數組織到一起,通過兩種標准對象技術:子類化和包裝,子類化和包裝叫做本地擴展。
- 在子類化和包裝中優先選擇子類,
- 使用包裝會造成A=B,B不等於A的邏輯,子類等於包裝類,包裝類不等於子類
- 什么時候引入
- 需要為服務類提供一些額外函數,但無法修改類。
- 怎么引入
- 建立一個新類,使它包含這些額外函數,讓這個擴展品成為源類的子類或包裝類。
- 子類化方案,轉型構造函數應該調用適當的超類構造函數
- 包裝類方案,轉型構造函數應該傳入參數以實例變量的形式保存起來,用作接受委托的原對象。
3 重新組織數據
對於這個類的任何修改都應該通過該類的方法。類擁有一些數據卻無所覺,擁有一些依賴無所覺是非常危險的。所以才要封裝字段,封裝集合,監視數據,用對象替代數組,用對象替代集合,關聯改動。
1、自封裝字段
- Self Encapsulate
- 動機
- 直接訪問變量的好處:子類可以通過覆寫一個函數而改變獲取數據的途徑,它還支持更靈活的數據管理方式,如延遲初始化等,
- 直接訪問變量的好處:代碼比較容易閱讀,
- 優先選擇直接訪問的方式,直到這種訪問方式帶來麻煩位置。
- 什么時候需要自封裝字段
- 直接訪問一個字段,但與字段之間的耦合關系逐漸變得笨拙。
- 怎么自封裝
- 為這個字段建立取值/設值函數,並且只以這些函數來訪問字段。
2、以對象取代數據值
- Replace Data Value with Object
- 動機
- 簡單數據不再簡單,
- 注意:原來的數據值是值對象,改成對象可能變成引用類型,這樣面臨的問題是多個實例就不是同一個對象。需要用將引用對象改成值對象方法,
- 什么時候需要對象取代
- 有一個數據項,需要與其他數據和行為一起使用才有意義。
- 怎么對象取代
- 為替換值新建一個新類,其中聲明final字段,修改原字段的引用,都修改為對象。
3、將值對象改成引用對象
- Change Value to Reference
- 對於值類型來說,equals和==的功能是相等的都是比較變量的值、
- 對於引用類型來說,==是b比較兩個引用是否相等,equals是比較的引用類型的內容是否相等,而使用equals是需要重寫的,不然就是調用object中的equals

- 動機
- 值對象一般是基本數據類型,並不在意是否有副本的存在,
- 引用對象是否相等,直接使用==操作符
- 什么時候改引用
- 一個類衍生出許多彼此相等的實例,希望將它們替換為同一個對象
- 類的每個實例中的字段都是獨立,就是值類型,每個實例都對應一個字段對象。
- 引用類型多個實例可以共用一個字段對象。不是所有
- 怎么改
- 創建簡單工廠和注冊表,工廠負責生產字段對象,注冊表負責保存所有的字段對象
- 類實例通過工廠請求字段實例,工廠通過訪問注冊表返回字段實例引用。
- 例子


-

- 目前為止customer對象還是值對象,即使多個訂單屬於同一客戶但每個order對象還是擁有自己的customer對象。
- 使用工廠方法替代構造函數



- 此時值對象才變成引用對象,多個實例間都共享同一個引用對象
4、將引用對象改成值對象
- Change Reference to value
- 這邊引用對象改成值對象並不是說需要把引用類型改成基本類型,而是即使引用類型是不同副本,那么相同內容的引用內容也是相等(重寫Equals())

- 動機
- 如果引用對象開始變得難以使用,或許就應該將它改成值對象。
- 引用對象必須被某種方式控制,而且必須向其控制者請求適當的引用對象,會造成區域之間錯綜復雜的關聯。
- 值對象應該是不可變的(無論何時,調用同一個對象的同一個查詢函數都應該得到相同的結果),如果需要改變就需要重新創建一個所屬類的實例,而不是在現有對象上修改。
- 什么時候更改
- 有一個引用對象,很小且不可變,而且不易管理。
- 怎么更改
- 檢查重構目標是否為不可變對象,建立equals和hashcode方法
- new Currency("USD").equals(new Currency("USD"));返回false。重寫equal和hashcode使其返回true,這樣對象就是值對象,不可變。
5、以對象取代數組
- Replace Array with Object
- 動機
- 數組是常見的組織數據的結構,只用於以某種順序容納一組相似對象。
- 什么時候需要取代
- 有一個數組,其中的元素各自代表不同的東西
- 怎么取代
- 將數組的每個不同意思都抽象稱字段
6、復制被監視的數據
- Duplicate Observed Data
- 動機
- 一個分層良好的系統,用戶界面和處理業務邏輯的代碼分開
- MVC模式
- 什么時候需要復制
- 有一些領域數據置身於GUI控件中,而鄰域函數需要訪問這些數據
- 怎么復制
- 將該數據復制到一個領域對象中,建立一個Observer模式,用以同步領域對象和GUI對象內的重復數據
7、將單向關聯改成雙向關聯
-
Change Unidirectional Association to Bidirectional
- 有點像觀察者模式,控制者是訂閱端,被控制者是主題,主題存在輔助函數,用於修改反向指針,訂閱端調用輔助函數來修改反向指針。

-
動機
- 隨着項目時間的推移需要雙向關聯
-
什么時候改動
- 兩個類都需要使用對方特性,但其間中有一條單向連接
-
怎么實現
-
添加一個反向指針,並使修改函數能夠同時更新兩條連接。
-
在被引用的類中增加一個字段,保存反向指針。
-
控制端和被控制端
- 一對多的關系,可以使用單一引用的一方(就是多的那一方)承擔控制者的角色。
- 對象是組成另一對象的部件,該部件負責控制關聯關系。
- 如果兩者都是引用對象,多對多,那么無所謂。
-
在被控端建立一個輔助函數,負責修改反向指針
-
如果既有的修改函數在控制端,讓它負責控制修改反向指針
-
如果既有的修改函數在被控端,就在控制端建立一個控制函數,並讓既有的修改函數調用這個新建的控制函數,來控制修改反向指針。
-
8、將雙向關聯改為單向關聯
- Change Bidirectional Association to Unidirectional
- 動機
- 雙向關聯必須要符出代價,維護雙向關聯,確保對象被正確創建和刪除而增加的復雜度。
- 雙向關聯還會造成僵屍對象,某個對象已經死亡卻保留在系統中,因為它的引用還沒有完全清楚。
- 雙向關聯也會迫使兩個類之間有了依賴,對其中任一個類的修改,都可能引發另一個類的變化。
- 什么時候需要
- 兩個類之間有雙向關聯,但其中一個類不再需要另一個的特性
- 怎么修改
- 去除不必要的關聯
- 將私有字段去掉,需要依賴的函數,將依賴類作為參數傳入,然后調用。
- 創建一個靜態字典保存所有的依賴類,通過取值函數來獲得字段遍歷對比依賴的引用是否相同來獲取依賴類。
9、以字面常量取代魔法數
- Replace Magic Number with Symbolic Constant
- 動機
- 什么時候取代
- 有一個字面數值,並帶有特別含義
- 怎么取代
- 創造一個常量,根據其意義為它命名,並將上述的字面數值替換為這個常量。
10、封裝字段
- Encapsulate Field
- 動機
- 數據聲明為public被看做一種不好的做法,會降低模塊化程度。
- 擁有該數據對象卻毫無察覺,不是一件好事
- 什么時候封裝
- 類中存在一個public字段
- 怎么封裝
- 將原字段聲明為private,並提供相應的訪問函數
11、封裝集合
- Encapsulate Collection
- 除非通過封裝的集合類,不然沒有任何實例能夠修改這個集合。

- 動機
- 在一個類中使用集合並將集合給取值函數,但類不應該返回集合自身,因為這回讓用戶得以修改集合內容而對集合的使用者一無所知。
- 不應該為集合提供一個設值函數,但應該為集合添加/移除元素的函數,這樣集合的擁有者就可以控制集合元素的添加和移除。
- 什么時候封裝
- 有一個函數返回一個集合
- 怎么封裝
- 讓這個函數返回該集合的一個只讀副本,並在這個類中提供添加/移除集合元素的函數
12、以數據類取代記錄
- Replace Record with Data Class
- 動機
- 從數據庫讀取的記錄,需要一個接口類,用來處理這些外來數據。
- 什么時候做
- 需要面對傳統編程環境中的記錄結構
- 怎么做
- 為該記錄創建一個啞數據對象。
- 新建一個類,對於記錄匯總的每一項數據,在新建的類中建立一個對應的private字段,並提供相應的取值和設值函數。
13、以類取代類型碼
- Replace Type Code with Class
- 原來的類型碼可能是int類型,建立一個類型碼的類,所有的int轉換成類型碼的類,其實有點像創建一個枚舉類型,然后用枚舉類型取代int。

- 動機
- 類型碼或枚舉值很常見,但終究只是一個數值,如果是一個類就會進行類型檢驗,還可以為這個類提供工廠函數,保證只有合法的實例才會被創建出來。
- 如果有switch必須使用類型碼,但任何switch都應該使用多態取代條件去掉。為了進行這樣的重構還需要使用子類取代類型碼,用狀態或策略替換類型碼。
- 什么時候做
- 類之中有一個數值類型碼,但它並不影響類的行為
- 怎么做
- 以一個新的類替換該數值類型碼
- 用以記錄類型碼的字段,其類型應該和類型碼相同,還應該有對應的取值函數,還應該用一組靜態變量保存允許被創建的實例,並以一個靜態函數根據原本的類型碼返回合適的實例。
14、以子類取代類型碼
- Replace Type Code with Subclasses
- 動機
- 什么時候做
- 有一個不可變的類型碼,它會影響類的行為
- 如果類型碼會影響宿主類的行為,最好的做好就是用多態來處理變化行為。就是switch和if else結構。
- 類型碼值在對象船艦之后發生變化,類型碼宿主類已經擁有子類,這兩種情況下就需要使用狀態/策略設計模式
- 怎么做
- 以子類取代這個類型碼
15、以State/Strategy取代類型碼
- Replace Type Code with State/Strategy
- 每個狀態有特定的數據和動作。

- 動機
- 什么時候做
- 有一個類型碼,它會影響類的行為,但無法通過繼承手法消除它
- 怎么做
16、以字段取代子類
- Replace Subclass with Fields
- 動機
- 什么時候做
- 各個子類的唯一差別只在返回常量數據的函數身上
- 直接用該字段的不同值表示子類就可以了。
- 怎么做
- 修改這些函數,使它們返回超類中某個(新增字段,然后銷毀子類)
4 簡化條件表達式
1、分解條件表達式
- Decompose Conditional
- 動機
- 復雜的條件邏輯是最常導致復雜度上升的地點之一,
- 什么時候做
- 有一個復雜的條件語句
- 怎么做
- 從if,then,else三個段落中分別提煉出獨立函數
- 將其分解為多個獨立函數,根據每個小塊代碼的用途分解而得的新函數命名。
- 很多人都不願意去提煉分支條件,因為這些條件非常短,但是提煉之后函數的可讀性很強,就像一段注釋一樣清楚明白。
2、合並條件表達式
- Consolidate Conditional Expression
- 其實就是用一個小型函數封裝一下,小型函數的名字可以作為注釋。

- 動機
- 合並后的條件代碼會使得檢查的用意更加清晰,合並前和合並后的代碼有着相同的效果。
- 什么時候做
- 有一系列條件測試,都得到相同結果
- 怎么做
- 將這些測試合並為一個條件表達式,並將這個條件表達式提煉成為一個獨立函數。
3、合並重復的條件片段
- Consolidate Duplicate Conditional Fragments
- 動機
- 什么時候做
- 在條件表達式的每個分支上都有着相同的一段代碼
- 怎么做
- 將這段重復代碼搬移到條件表達式之外。
4、移除控制標記
- Remove Control Flag
- 動機
- 單一出口原則會迫使讓媽中加入討厭的控制標記,大大降低條件表達式的可讀性,
- 什么時候做
- 在一系列布爾表達式中,某個變量帶有"控制標記"(control flag)的作用
- 怎么做
- 以break語句或return語句取代控制標記
5、以衛語句取代嵌套條件表達式
- Replace Nested Conditional with Guard Clauses
- 動機
- 單一出口的規則其實並不是那么有用,保持代碼清晰才是最關鍵的。
- 什么時候做
- 函數中條件邏輯使人難以看清正常的執行路徑
- 怎么做
- 使用衛語句表現所有特殊情況。
6、以多態取代條件表達式
- Replace Conditional with Polymorphism
- 動機
- 如果需要根據對象的不同類型而采取不同的行為,多態使你不必編寫明顯的條件表達式。
- 同一組條件表達在程序許多地點出現,那么使用多態的收益是最大的。
- 什么時候做
- 有一個條件表達式,根據對象類型的不同而選擇不同的行為
- 怎么做
- 將這個體哦阿健表示式的每個分支放進一個子類的覆寫函數中,然后將原始函數聲明為抽象函數。
7、引入Null對象
- Introduce Null Object
- 動機
- 多態的最根本好處就是不必要想對象詢問你是什么類型而后根據得到的答案調用對象的某個行為,只管調用該行為就是了。
- 空對象一定是常量,它們的任何成分都不會發生變化,因此可以使用單例模式來實現它們。
- 什么時候做
- 需要再三檢查對象是否為Null
- 怎么做
- 將null對象替換成null對象。
8、引入斷言
- Introduce Assertion
- 動機
- 斷言是一個條件表達式,應該總是為真,如果它失敗,表示程序員犯了一個錯誤。因此斷言的失敗應該導致一個非受控異常(unchecked exception)。
- 加入斷言永遠不會影響程序的行為。
- 用它來檢查一定必須為真的條件。
- 什么時候做
- 某一段代碼需要對程序狀態做出某種假設
- 怎么做
- 以斷言明確表現這種假設
5 簡化函數調用
所有的數據都應該隱藏起來。
1、函數改名
- Rename Method
- 動機
- 將復雜的處理過程分解成小函數。
- 什么時候做
- 函數名稱未能揭示函數的用途
- 怎么做
- 修改函數名稱
2、添加參數
- Add Parameter
- 動機
- 什么時候做
- 某個函數需要從調用端得到更多信息
- 在添加參數外常常還有其他的選擇,只要有可能,其他選擇都比添加參數要好(查詢),因為它們不會增加參數列的長度,過長的參數列是一個不好的味道。
- 怎么做
- 為此函數添加一個對象參數,讓該對象帶進函數所需信息。
3、移除參數
- Remove Parameter
- 動機
- 可能經常添加參數卻很少去除參數,因為多余的參數不會引起任何問題,相反以后可能還會用到它。請去除這些想法。
- 什么時候做
- 函數本體不需要某個參數
- 怎么做
- 將該參數去除。
4、將查詢函數和修改函數分離
- Separate Query from Modifier
- 動機
- 在多線程系統中,查詢和修改函數應該被聲明為synchronized(已同步化)
- 什么時候做
- 某個函數既返回對象狀態值,又修改對象狀態
- 任何有返回值的函數,都不應該又看得到的副作用。
- 常見的優化是將某個查詢結果放到某個字段或集合中,后面如何查詢,總是獲得相同的結果。
- 怎么做
- 建立兩個不同的函數,其中一個負責查詢,另一個負責修改。
5、令函數攜帶參數
- Parameterize
- 動機
- 去除重復代碼
- 什么時候做
- 若干函數做了類似的工作,但在函數本體中卻飽含了不同的值
- 怎么做
- 建立單一函數,以參數表達那些不同的值
6、以明確函數取代參數
- Replace Parameter with Explicit Methods
- 動機
- 避免出現條件表達式,接口更清楚,編譯期間就可以檢查,
- 如果在同一個函數中,參數是否合法還需要考慮
- 但是參數值不會對函數的行為有太多影響的話就不應該使用本項重構,如果需要條件判斷的行為,可以考慮使用多態。
- 什么時候做
- 有一個函數,其中完全取決於參數值不同而采取不同行為
- 怎么做
- 針對該參數的每一個可能值,建立一個獨立函數
7、保持對象完整
- Preserve While Object
- 動機
- 不適用完整對象會造成重復代碼
- 事物都是有兩面性,如果你傳的是數值,被調用函數就只依賴於這些數值,如果傳的是對象,就要依賴於整個對象。如果依賴對象會造成結構惡化。那么就不應該使用保持對象完整。
- 如果這個函數使用了另一個對象的多項數據,這可能以為着這個函數實際上應該定義在那些數據所屬的對象上,應該考慮移動方法。
- 什么時候做
- 從某個對象中取出若干值,將它們作為某一次函數調用時的參數
- 怎么做
- 改為傳遞整個對象
8、以函數取代參數
- Replace Parameter with Methods
- 動機
- 盡可能縮減參數長度
- 什么時候做
- 對象調用某個函數,並將所有結果作為參數傳遞給另一個函數,而接受該參數的函數本省也能夠調用前一個函數。
- 怎么做
- 讓參數接受者去除該項參數,並直接調用前一個函數。
9、引入參數對象
- Introduce Parameter Object
- 動機
- 特定的一組參數總是一起被傳遞,可能有好幾個函數都使用這一組參數,這些函數可能隸屬於同一個類,也可能隸屬於不同的類。這樣的參數就是所謂的數據泥團,可以運用一個對象包裝所有的這些數據,再以該對象取代它們。
- 什么時候做
- 某些參數總是很自然地同時出現
- 怎么做
- 以一個對象取代這些參數
10、移除設值函數
- Remove Setting Method
- 動機
- 使用了設值函數就暗示了這個字段值可以被改變。
- 什么時候做
- 類中某個字段應該在對象創建時被設值,然后就不再改變。
- 怎么做
- 去掉該字段的所有設值函數。
11、隱藏函數
- Hide Method
- 動機
- 面對一個過於豐富、提供了過多行為的接口時,就值得將非必要的取值函數和設置函數隱藏起來
- 什么時候做
- 有一個函數,從來沒有被其他任何類用到
- 怎么做
- 將這個函數修改為private
12、以工廠函數取代構造函數
- Replace Constructor with Factory Method
- 動機
- 使用以工廠函數取代構造函數最顯而易見的動機就是在派生子類的過程中以工廠函數取代類型碼。
- 工廠函數也是將值替換成引用的方法。
- 什么時候做
- 希望在創建對象時不僅僅是做簡單的構建動作
- 怎么做
- 將構造函數替換為工廠函數
- 使用工廠模式就使得超類必須知曉子類,如果想避免這個可以用操盤手模式,為工廠類提供一個會話層,提供對工廠類的集合對工廠類進行控制。
13、封裝向下轉型
- Encapsulate Downcast
- 動機
- 能不向下轉型就不要向下轉型,但如果需要向下轉型就必須在該函數中向下轉型。
- 什么時候做
- 某個函數返回對象,需要由函數調用者執行 向下轉型
- 怎么做
- 將向下轉型動作移到函數中
14、以異常取代錯誤碼
- Replace Error Code with Exception
- 動機
- 代碼可以理解應該是我們虔誠最求的目標。
- 什么時候做
- 某個函數返回一個特定的代碼,用以表示某種錯誤情況
- 怎么做
- 改用異常
- 決定應該拋出受控(checked)異常還是非受控(unchecked)異常
- 如果調用者有責任在調用前檢查必要狀態,就拋出非受控異常
- 如果想拋出受控異常,可以新建一個異常類,也可以使用現有的異常類。
- 找到該函數的所有調用者,對它們進行相應調整。
- 如果函數拋出非受控異常,那么就調整調用者,使其在調用函數前做適當檢查,
- 如果函數拋出受控異常,那么就調整調用者,使其在try區段中調用該函數。
15、以測試取代異常
- Replace Exception with Test
- 動機
- 在異常被濫用的時候
- 什么時候做
- 面對一個調用者可以預先檢查的體哦阿健,你拋出一個異常
- 怎么做
- 修改調用者,使它在調用函數之前先做檢查
6 處理繼承關系
1、字段上移
- Pull Up Field
- 動機
- 減少重復
- 什么時候做
- 兩個子類擁有相同的字段
- 怎么做
- 將該字段移至超類
2、函數上移
- Pull Up Method
- 動機
- 滋生錯誤
- 避免重復
- 什么時候做
- 有些函數在各個子類中產生完全相同的結果
- 怎么做
- 將該函數移至超類
- 最煩的一點就是,被提升的函數可能會引用子類中出現的特性,如果被引用的是一個函數可以將這個函數一同提升至超類,或則在超類中建立一個抽象函數。
- 如果兩個函數相似但不相同,可以先借助塑造模板函數。
3、構造函數本體上移
- Pull Up Constructor Body
- 引用
- 如果重構過程過於復雜,可以考慮使用工廠方法。
- 什么時候做
- 在各個子類中擁有一些構造函數,它們的本體機會完全一致
- 怎么做
- 在超類中新建一個構造函數,並在子類構造函數中調用它。
4、函數下移
- Push Down Method
- 動機
- 把某些行為從超類移動到特定的子類中。
- 什么時候做
- 超類中某個函數只與部分子類有關
- 怎么做
- 將這個函數移到相關的那些子類中
- 如果移動的函數需要使用超類中的某個字段,則需要將超類中的字段的開放protected.
5、字段下移
- Push Down Field
- 動機
- 什么時候做
- 超類中的某個字段只被部分子類用到
- 怎么做
- 將這個字段移到需要它的那些子類去
6、提煉子類*?
- Extract Subclass
- 動機
- 類中的某些行為只被一部分實例用到,其他實例不需要,有時候這些行為上的差異是通過類型碼分區的,可以使用子類替換類型碼,或則使用狀態或策略模式替代類型碼。
- 抽象類和抽象子類則是委托和繼承之間的抉擇
- 抽象子類會更加容易,但是一旦對象建立完成,無法再改變與類型相關的行為。
- 什么時候做
- 類中的某些特性只被某些實例用到
- 怎么做
- 新建一個子類,將上面所說的那一部分特性移到子類中
- 為源類定一個新的子類
- 為這個新的子類提供構造函數
- 讓子類構造函數接受與超類構造函數相同的參數,並通過super調用超類的構造函數。
- 用工廠替換構造函數
- 找出調用結果超類構造函數的所有地點,新建子類
- 下移方法和字段
7、提煉超類*?
-
Extract Superclass
-
動機
-
什么時候做
- 兩個類有相似特性
-
怎么做
- 為這兩個類建立一個超類,將相同特性移至超類。
-
新建一個空白抽象類
- 上移字段和方法
- 先搬移字段
- 子類函數中有相同的簽名,但函數體不同,可以抽象函數
- 如果方法中有相同算法,可以使用提煉算法,將其封裝到同一個函數中。
8、提煉接口
- Extract Interface
- 動機
- 類之間彼此互用的方式有若干種,某一種客戶只使用類責任區的一個特定子集。
- 某個類在不同環境下扮演截然不同的角色,使用接口就是一個好主意。
- 什么時候做
- 若干客戶使用類接口中同一個子集,或者兩個類的接口有部分相同
- 怎么做
- 將相同的子類提煉到一個獨立接口中。
- 上移字段和方法
9、折疊繼承關系
- Collapse Hierarchy
- 動機
- 什么時候做
- 超類和子類之間無太大區別
- 怎么做
- 將它們合為一體
10、塑造模板函數
- Form Template Method
- 動機
- 既避免重復也保持差異。
- 什么時候做
- 有一些子類,其中相應的某些函數以相同順序執行類似的操作,但各個操作的細節上有所不同。
- 怎么做
- 將這些操作分別放進獨立函數中,並保持它們都有相同的簽名,於是原函數也就變得相同的,然后將原函數上移至超類
11、以委托取代繼承
- Replace Inheritance with Delegation
- 動機
- 超類中有許多操作並不真正適用於子類,這種情況下,你所擁有的接口並未真正反映出子類的功能。
- 什么時候做
- 某個子類只使用超類接口中的一部分,或是根本不需要繼承而來的數據
- 怎么做
- 在子類中新建一個字段用以保存超類,調整子類函數,令它改而委托超類,然后去掉兩者之間的繼承關系。
- 在子類中新建一個字段,使其引用超類的實例
- 修改子類中的所有函數,讓它們不再使用超類,轉而使用上述那個受托字段。
12、以繼承取代委托
- Replace Delegation with Inheritance
- 動機
- 如果並沒有使用受托類的所有函數,就不應該使用用繼承替換委托,
- 可以使用去除中間層的方法讓客戶端自己調用受托函數。
- 什么時候做
- 在兩個類之間使用委托關系,並經常為整個接口編寫許多極簡單的委托函數。
- 怎么做
7 大型重構
1、梳理並分解繼承體系
- Tease Apart Inheritance
- 就是讓每個類的職責更明確更單一,當一個類的職責混亂時,通過繪制職責圖來分離職責,並創建另一個超類,將相關的字段和方法都移動到另一個超類

- 動機
- 混亂的繼承體系是一個嚴重的問題,會導致重復代碼,而后者正是程序員生涯的致命毒葯。還會使修改變得困難,因為特定問題的解決決策被墳山到了整個繼承體系。
- 什么時候做
- 某個繼承體系同時承擔兩項責任
- 怎么做
- 建立兩個繼承體系,並通過委托關系讓其中一個可以調用另一個
- 首先識別出繼承體系所承擔的不同責任,然后建立一個二維表格(或則三位乃至四維表格),並以坐標軸標示不同的任務,
- 判斷哪一項責任更重一些,並准備將它留在當前的繼承體系中,准備將另一項責任移到另一個繼承體系中。
- 使用抽象類方法從當前的超類提煉出一個新類,用以表示重要性稍低的責任,並在原超類中添加一個實例變量,用以保存新類的實例。
- 對應於原繼承體系中的每個子類,創建上述新類的一個子類,在原繼承體系的子類中,將前一步驟所添加的實例變量初始化為新建子類的實例。
- 針對原繼承體系中的每個子類,使用搬移函數的方法遷移到與之對應的子類中。





2、將過程化設計轉化為對象設計
- Convert Procedural Design to Objects
- 動機
- 什么時候做
- 有一些傳統過程化風格的代碼
- 怎么做
- 將數據記錄變成對象,將大塊的行為分為小塊,並將行為移入相關對象之中。
- 針對每一個記錄類型,將其轉變為只含訪問函數的啞數據對象
- 針對每一處過程化風格,將該出的代碼提煉到一個獨立類中。
- 針對每一段長長的程序,試試提煉方法將長方法分解並將分解后的方法移動到相關的啞數據類。
3、將領域和表訴/顯示分離
-
Separate Domain from Presentation
-
動機
- MVC模式最核心的價值在於,它將用戶界面代碼(即視圖:亦即現今常說的展現層)和領域邏輯(即模型)分離了,展現類只含用以處理用戶界面的邏輯:領域類包含任何與程序外觀相關的代碼,只含業務邏輯相關代碼,將程序中這兩塊復雜的部分加以分離,程序未來的修改將變得更加容易,同時也使用同意業務邏輯的多種展現方式稱為可能。
-
什么時候做
- 某些GUI類中包含了領域邏輯
-
怎么做
-
將領域邏輯分離出來,為它們建立獨立的鄰域類。
-
為每個窗口建立一個領域類,
-
如果窗口內含有一張表格,新建一個類來表示其中的行,再以窗口所對應之領域類中的一個集合來容納所有行領域對象
-
檢查窗口中的數據,如果數據只被用於UI,就把它留着,如果數據被領域邏輯使用,而且不顯示與窗口上,我們就可以使用移動方法將它搬移到領域類中,如果數據同時被UI和領域邏輯使用,就對它實施復制被監視數據,使它同時存在於兩處,並保持兩處之間的同步。
-
展現類中的邏輯,實施提煉方法將展現邏輯從鄰域邏輯中分開,一旦隔離了鄰域邏輯,在運用搬移方法將它移到鄰域類。
-

-

-
4、提煉繼承體系
- Extract Hierarchy
- 動機
- 一開始設計者只想以一個類實現一個概念,但隨着設計方案的演化,最后可能一個類實現兩個三乃至十個不同的概念。
- 什么時候做
- 有某個類做了太多的工作,其中一部分工作是以大量條件表達式完成的
- 怎么做
- 建立繼承體系,以一個子類表示一種特殊情況。
- 有兩種重構的手法
- 無法確定哪些地方會發生變化
- 不確定哪些地方會發生變化
- 鑒別出一中變化情況,
- 如果這種拜年話可能在對象聲明周期的不同階段而有不同體現就用提煉方法將它提煉為一個獨立的類
- 針對這種變化情況,新建一個子類,並對原始類實施工廠方法替代構造函數,再次修改工廠方法,令它返回適當的子類實例。
- 將含有條件邏輯的函數,一個個復制到子類
- 有必要隔離函數中的條件邏輯和非條件邏輯。
- 刪除超類中那些被所有子類覆寫的函數本體,並將它們聲明為抽象函數。
- 鑒別出一中變化情況,
- 確定原始類中每一種變化
- 針對原始類中每一種變化情況,建立一個子類,
- 使用工廠方法替代構造函數將原始類的構造函數轉變成工廠函數,並令它針對每一種變化情況返回適當的子類實例。
- 如果原始類中的各種變化情況是以類型碼標示,使用子類替換類型碼,如果那些變化情況在對象周期的不同階段會有不同體現,使用狀態和策略模式替換類型碼
- 針對帶有條件邏輯的函數,實施用多態替換條件如果非整個函數的行為有所變化,請先運行提煉方法將變化部分和不變部分分隔開來
8 案例
- 有一個影片商店客戶端,需要計算每一個客戶的消費,常客積分
- 客戶customer
- 租賃rental
- 影片movie,普通Regular,兒童Children,新片Release
- Regular:2天內2元,大於2天1.5一天
- Release:每天三元
- Childrens:3天內1.5元,大於3天1.5一天
- 計費函數

提煉方法
這個計費函數太復雜


- 修改參數名

搬移方法
- amountfor沒有使用customer任何信息,只是使用了rental類的,將其搬移到rental類中

- 修改原customer中函數調用

用查詢替換臨時變量

- 用同樣的方法來處理計算常客積分的部分


使用多態替換Switch
- 原來的switch,在rental類中

- 不要再另一個對象屬性上使用switch,將其移動到方法中
- 移動過后

- 常客積分

- 繼承機制

- 一個影片可以再生命周期內修改自己的分類,一個對象卻不能再生命周期內修改自己所屬的類,這里需要使用用狀態或策略模式替換type,搬移方法到超類,用多態替換條件


- 提煉超類


- 修改原來movie中的getcharge方法,
- 首先getcharge移動方法

- 用多態替換getcharge方法




- 首先getcharge移動方法

























































