重構改善既有代碼 第一次做某件事情的時候儘管去做,第二次做類似的事會產生反感,第三次再做類似的事,你就應該重構。 小型函數優美動人 一個類最好是常量類,任何的改變都是調用該類本身的介面實現。 0 壞代碼的味道 1、重覆代碼 Duplicated Code 同一類中的兩個函數含有相同的表達式,提取到方 ...
重構改善既有代碼
- 第一次做某件事情的時候儘管去做,第二次做類似的事會產生反感,第三次再做類似的事,你就應該重構。
- 小型函數優美動人
- 一個類最好是常量類,任何的改變都是調用該類本身的介面實現。
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
- 就是讓每個類的職責更明確更單一,當一個類的職責混亂時,通過繪製職責圖來分離職責,並創建另一個超類,將相關的欄位和方法都移動到另一個超類
- 動機
- 混亂的繼承體系是一個嚴重的問題,會導致重覆代碼,而後者正是程式員生涯的致命毒藥。還會使修改變得困難,因為特定問題的解決決策被墳山到了整個繼承體系。
- 什麼時候做
- 某個繼承體系同時承擔兩項責任
- 怎麼做
- 建立兩個繼承體系,並通過委托關係讓其中一個可以調用另一個
- 首先識別出繼承體系所承