重構改善既有代碼
- 第一次做某件事情的時候盡管去做,第二次做類似的事會產生反感,第三次再做類似的事,你就應該重構,
- 小型函式優美動人
- 一個類最好是常量類,任何的改變都是呼叫該類本身的介面實作,
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移動方法
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/39891.html
標籤:架構設計
下一篇:分布式事務之基本概念

























































