《架構整潔之道》&《 重構改善既有代碼的設計》 整合

 

一、重構定義:

在不改變軟件可觀察行爲的前提下,對軟件內部進行調整(使用重構手法),以提高其可理解性,降低其修改成本。 -- 重點在兩點: 1. 不改變軟件的可觀察行爲。2. 提高其可理解性。

兩個目的:1. 添加新功能。 2. 重構。 --重構就只管修改程序結構,不要添加新功能。 添加新功能就不要修改既有代碼。兩者混合進行會使得程序朝不可理解的方向發展。

ps: 重構與設計模式具有辯證的關聯性,模式是目的,重構是到達之路。重構促進設計模式的形成與穩定,模式爲重構提供前進方向,二者相輔相成,具有統一性。


二、爲什麼需要重構

1. 面對迅速變化的需求,對原有的代碼進行修改十分困難(邏輯複雜,條理不清晰,很難兼顧;改動接口多,測試困難), 尤其對於某些無法限定影響面的接口修改。
2. 使得原有設計保持本真意義。代碼結構的流失具有累積性,原有的設計及意圖難以保持,閱讀源代碼很難理解原來的設計。
3. 消除重複代碼,重複代碼越多,修改的風險越大,修改帶來的不一致性可能越大(修改一處,未修改另一處)
4. 使得軟件更容易理解,結構更清晰,代碼更簡潔。
5. 幫助找到bug
6. 提高編程速度 -- 維持良好設計,清晰的意圖,從而使得編程更加容易。
7. 重構與新功能 -- 如果你發現自己需要添加一個特性,而代碼結構使你無法很方便地達成目的,那就先重構那個程序,使特性比較容易進行,然後再添加特性。
8. 重構與性能 -- 重構調整代碼結構,使性能調優更加方便的進行。

 

2.1    架構整潔之道 ——Robert C. Martin

2.1.1. 綜述

爲什麼要進行架構設計: 採用好的架構可以大大節省軟件項目構建與維護的人力成本。每次變更都短小簡單,易於實施,並且避免缺陷,用最小的成本,最大程度的滿足功能性的需求。在軟件的全生命週期內,最大化程序員的生產力,同時最小化系統的總運營成本。

怎樣纔是良好的架構設計: 良好的架構設計讓系統便於理解,易於修改,方便維護,輕鬆部署,不依賴於成堆的腳本與配置文件,也不需要用戶創建有嚴格要求的目錄與文件。

怎樣纔是優秀的架構設計師:架構師的職責在場景需求中,整理出核心的業務流程。對業務進行邊界劃分,同時管理好各個邊界的依賴。優秀的架構師往往對業務場景有較好的抽象能力,能夠cover住場景類各業務實體變化的需求。 同時優秀的架構師能夠預留出不完全邊界,在以後業務拆分重構中以微小易實施的改動進行。最後優秀的架構師爲系統預留了許多的選項(方便的添加),將技術手段的決策延後(使用哪種框架等決策),並做到設備無關,使業務的發展不依賴於具體的框架、語言、數據庫等技術細節。

2.1.2 兩個維度的價值:

1. 行爲價值: 按照需求文檔寫代碼,修復Bug
2. 架構價值: 軟件必須可以以一種靈活的方式來改變機器的工作行爲 (ps: 這種靈活體現在對需求的擴展性上, 而場景抽象能力是一位架構師的基礎能力)

總結: 一次性消費的代碼,在需求變更過程中價值趨於0, 而良好設計的架構能夠滿足不斷變化的需求(場景內), 從而帶來持續的價值。
一個軟件架構師,更應該關注系統的整體架構,而不是具體的功能和系統行爲的實現。軟件架構師必須創建出一個可以讓功能實現起來容易、修改起來更簡單、擴展起來更輕鬆的軟件架構。
如果系統越來越難維護,終導致無法修改,說明軟件工程師沒有完成自己應盡的責任,是軟件開發的失敗。

2.1.3. 編程範式

2.1.3.1  結構化編程: 對程序控制器直接轉移的限制

將一段程序遞歸降解爲一系列可證明的小函數,然後通過編寫相關的測試來證明這些函數都是錯誤的。如果無法證僞這些函數,那麼久可以認爲這些函數是足夠正確的。
可以說結構化編程,功能性降解,然後證僞仍然是軟件架構設計領域的最佳實踐。

2.1.3.2 面向對象編程: 對程序控制器間接轉移的限制

依賴反轉:讓對源代碼實現的依賴變成對接口的依賴,而源代碼實現依賴接口,讓源代碼實現可以隨意替換。
多態可以對源代碼的中的依賴關係進行控制,從而讓代碼成爲一種插件式的架構,對於組件的替換基本不需要進行代碼改動。經過高層策略組件與底層實現的分離,從而做到真正的獨立部署、獨立開發。
同時面向對象的多態通過隱式的方式來使用函數指針,從而保證了函數指針的安全性。

2.1.3.3 函數式編程: 對賦值操作的限制

如果計算能力很大,存儲能力足夠。對於任何可變變量,都可以通過函數計算來獲取。這樣程序始終處於無狀態,無鎖競爭之中,處於不變之中。這就是函數式編程。

2.1.4  設計原則

        (其最終目的都是爲了建立一個松耦合的結構,讓系統可以靈活的擴展,以滿足場景內需求的變化)

2.1.4.1 solid原則

在構建中層模塊結構時候,將數據和函數進行合適的分類組合。主要目標: 使軟件可容忍被改動;使軟件容易被理解;構建可在多個軟件系統中複用的組件


1. 單一職責原則: 每個軟件模塊都有且只有一個需要被改變的理由->任何一個模塊都應該只對一個用戶,或系統利益相關者負責->任何一個模塊都應該只對某一個行爲者負責。
軟件模塊:一組緊密相關的函數和數據結構, 更籠統的說 就是指一個類。 用戶:更抽象的說是一個需求。
單一職責讓一個模塊或一個函數,只對一個用戶或者高層負責。 避免不同用戶對同一模塊的依賴從而造成混亂。

2. OCP開閉原則: 設計可以通過新增加代碼來修改系統行爲,而不是靠修改原來的代碼。對修改關閉,對新增開放。
這需要保存高層抽象的穩定性,讓底層實現依賴於高層代碼,只需要增加底層的不同實現即可。

3. LSP裏式替換原則:如果想用可替換的組件來構建軟件系統,那麼這些組件必須遵守同一個約定。任何使用父類的地方,都可以用其子類替換。

4. 接口隔離原則:在設計過程中避免不必要的依賴,組件不應該依賴,其不必要的東西(所依賴的類包含無關的東西)
類似迪米特原則:在類的層級依賴關係中,當前類只與上下兩層的類發生依賴,不隔層依賴,不同層依賴。這樣避免了不必要的依賴,從而減少修改的擴散風險,同時爲分離部署和單獨編譯帶來可可能。

5. DIP依賴反轉: 高層策略性代碼不應該依賴底層細節的代碼。底層實現細節代碼應該依賴高層策略性代碼。

ps: 單一職責,與接口隔離的統一點: 單一職責使得某一個類或組件只服務於某一功能,另一個類或組件服務於另一功能,這自然做到了接口隔離,從而可獨立發展,部署。
但單一職責體現變更層面的意思:某一個類只由於一個原因而變更,不因爲不同原因而變更。
而接口隔離體現依賴層面的意思:某一個類不依賴其他不相干的類。既然類都是單一職責了,自然不會產生依賴,即使有依賴,可以通過導演類,全局變量來解決。


2.1.4.2 組件聚合原則

REP原則(複用/發佈): 軟件複用的最小粒度,等同於其發佈的最小粒度。 REP原則指組件中的類與模塊彼此緊密相關,他們必須有一個共同的主題或者大方向,從而成爲一個可獨立發佈的可複用模塊
CCR原則(共同閉包):將那些會同時修改,並且爲相同目的而修改的類放到同一個組件中,與單一職責類似,只不過其服務的層次更高,更抽象。
CRP原則(共同複用): 比較常見的情況是多個類同時作爲某個可複用的抽象定義,被共同複用。應該將這些類放在同一個組件中。

2.1.4.3 組件耦合原則

無環依賴原則: 如果組件間的依賴形成一個環,則這些組件必須共同修改,共同發佈,共同打tag,同時無法進行分解測試,必須集成測試。相反如果單向依賴,則各組件可以單獨發佈版本,依賴他的模塊只需控制版本即可。依賴反轉可以解決循環依賴。
穩定性依賴原則: 從依賴的上游,到依賴的下游,穩定性一次遞增。對於需要具有高穩定性的下游模塊,其變更的工作量,遠遠大於新增工作量。抽象的高階策略代碼,往往具有場景穩定性,所以需要根據組件的穩定性程度來覺得其抽象程度。換句話說:穩定性依賴指明依賴應該指向更加抽象的方向。

不穩定性計算公式: 出依賴/(出依賴+入依賴) 抽象化程度: 組件中接口與抽象類的數量/類的總數量

可以指出,一個組件的抽象化程度與被依賴數據是相關的,一個組件如果被依賴數量較大,則其應該被設計的更加抽象;相反其依賴數量較少,則其需要更容易變動。
一個完全抽象的組件是毫無用處的,只有可變更,可擴展的組件才能支撐業務的不斷髮展。一個優秀的組件應該在其所處的業務層次對兩者進行權衡。《架構整潔之道》設計了一套方法來衡量一個組件的依賴設計

 

2.1.5 整潔架構設計

        整潔架構的設計,應該遵從上述職責單一,依賴反轉等主要原則。以業務爲中心,讓其他技術手段直接或間接依賴於業務。在系統開發過程中,要時刻關注模塊間的依賴關係,在必要時進行邊界拆分重構。

                       圖2.1:整潔架構設計

2.1.5.1 用例驅動

        以業務實體爲中心設計用例,對用例進行分類從而劃分出場景下的不同業務流程。同時根據用例可以方便的進行設計測試案例,

2.1.5.2 邊界劃分

        架構設計最核心的部分就是邊界劃分。對於業務流程,如何劃分邊界使得一部分業務獨立發展和部署是架構設計最關注的地方。

        單體架構降低了團隊間項目研發的效率,所以往往將業務分模塊獨立開發和部署。這便要求需要有很好的業務邊界,使得整塊業務獨立,跨邊界依賴清晰並且服務依賴穩定性原則。

        在同一個業務模塊內,對於部分更小的、具有獨立性的業務,應該進行預留邊界劃分,以便於以後重構需要業務拆分的時候,方便的獨立出去。

2.1.5.3 場景抽象

        在業務場景中,包括整個系統的和模塊內部的,應該意識到業務實體的可變性。需要在各自的場景類進行抽象,以保持業務策略的穩定性,包容業務實體的易變性。如:打車場景下打車的用戶可以是人,也可以是動物或者貨物;開車的司機不一定是人,也可能是機器人(自動駕駛)等。

2.1.5.4 接口設計

        業務邊界的劃分本質上就是設計良好的開放接口,以便跨邊界調用。同時在內部的不完全邊界設計良好的開放API。 在技術實現上通過設計良好的接口,讓多種技術手段依賴於接口做到依賴反轉,從而形成一種插件式的架構。例如數據庫的實現:首先爲業務設計好需要的數據庫操作接口,而不管使用哪種數據庫,怎麼樣實現。不同的數據庫通過實現這個接口定義,從而將自己做成一種插件,可以隨時替換進來。

2.1.6. 常用架構設計

2.1.6.1  分層架構

        整個系統按照層次進行劃分如:展示層,控制層,模型層,持久層。每一層囊括多種業務。但是業務混雜在一起,很難進行隔離 如 進程隔離,線程池隔離等。

2.1.6.2  功能架構

        整個系統按照功能進行劃分,一個功能可能包含鏈路的多個層面。但是劃分太細、依賴太複雜從而難於管理。

2.1.6.2 SOA架構

        SOA架構是按照組件進行劃分,一個模塊內可囊括多種功能,包含多個層次。但是這些業務組合在一起形成一個單一的服務,支撐一類業務。 比較常見的SOA架構是按照業務鏈路進行分層解耦,每一個業務層是一個獨立的服務。

2.1.6.3 微服務架構

        微服務架構與SOA架構同類,但是微服務的邊界劃分更細。微服務不是隻要劃分更加細的服務,微服務的核心在業務的橫向拆分,將多個垂直應用的共同業務劃分成一個獨立的部分,從而提高組件複用率,方便進行統一的管理,獨立的開發和部署。這個跨多個應用的服務,通常表現的比SOA服務更加微小。

2.1.6.4 小結架構設計方法步驟

        1. 根據上述整潔架構原則,首先要分析整個系統要支撐的業務場景,畫出場景下的用例圖。根據用例圖分析,該用例下需要進行的數據轉換流程。數據流圖可以清楚的描繪數據的輸入、輸出已經轉換過程。

        2. 根據用例圖和數據流圖,已經能清楚的描繪一個場景下,系統應該支撐的業務流程。此時就行業務流程建模,描繪該系統從輸入數據到輸出的所有業務流程。(注意在這個階段不要考慮用何種技術手段去實現這個過程,比如: web還是pc,何種數據庫,輸出哪些字段等問題)。

        3. 將多個業務流程劃分邊界。不管如何劃分邊界,主要考慮提高團隊的協作開發效率,資源利用率。同時爲未來可獨立開發部署的業務預留出不完全邊界。

        4. 進行邊界劃分後,可以進一步用時序圖來表達場景下,某類用例業務各邊界的交互依賴。用活動圖表達邊界內業務處理的具體動作流程。

        5. 明確邊界內的業務動作後,可以對業務進行抽象設計,從而保持業務策略代碼的穩定性,支撐業務實體的變化性。

        6. 明確邊界間的依賴關係後,可以對業務邊界設計詳細的接口。 在不完全邊界上也要設計接口或者抽象的API來調用。

三、壞代碼的味道(什麼樣的形式纔是壞代碼)

重複代碼
過長函數
過大的類
發散式變化: -- 一個類收到多個因素的影響,將朝着多個不同的方向變化,可以將類拆解成多個不同的類,然後橋接起來。
散彈式變化: -- 一個類的變動會印象多個其他的類,則可以將受此類影響的代碼都放入這個類中。
同步式變化: -- 兩個東西總是一起變化,如引用與數據,則將這兩個同時變化的東西放入同一個類中。
數據泥團: -- 一些零散的數據項出現在許多不同的類,或者函數的參數列表中。則可以將這些相同數據項抽取到一個類中。
基本類型偏執 -- 對於可以運用對象來表述一個物體,不用基本類型。如日期,時間等
多態擴展: -- 替換switch case 根據類型多態擴展。
平行繼承體系 -- 如果爲一個類增加子類,必須爲另一個類增加子類。可讓一個體系引用另一個體系實例
冗贅類 -- 一個類沒有多大的實際意義,沒必要讓人花時間、精力來了解
誇誇奇談的未來性 -- 某些代碼沒有多大作用,只是覺得未來可能有用,從而使得系統更難維護和理解
令人迷惑的暫時字段 -- 某些字段只在特殊情況下才被使用到,讓人不知其設置目的
過長的調用鏈路 -- 客戶端需要很長的調用鏈來獲取一個值,可以通過委託來獲取最終值
中間人 -- 存在過多不必要的委託
狎暱關係 -- 類之間的調用關係較多,依賴較強。可以將一些方法或字段,移動到需要的類中。
異曲同工的類 -- 兩個函數做同一件事情,卻有着不同的簽名。可以根據用途來命名,並抽離重複代碼。
不完美的類庫 -- 類庫缺少需要的功能
純稚的數據類 -- 未對數據進行封裝,數據接口暴露
被拒絕的遺贈 -- 子類完全未使用父類的代碼
過多的註釋 -- 讓代碼自身具有註釋功能

四、何時重構:

1. 三次法則 --代碼忍受了3次,就不要再忍受了,重構吧!
2. 添加新特性時候重構 -- 新特性的添加需要修改原先代碼,且修改方案複雜
3. 複審代碼時重構 -- 代碼評審 談談如何重構
4. 重複代碼過多
5. 函數過長 -- 一個函數應該只包含一個獨立的邏輯功能。 這樣將邏輯單元打散,可以減少代碼耦合,並且更加清晰。
6. 過大的類 -- 一個類中出現太多實例變量,容易出現重複混亂的代碼,可以考慮重構出子類。
7. 參數列表過長

五、重構難題

1. 數據庫改動 -- 可以增加數據庫接入層,使得數據庫的變化和業務的變化分離
2. 接口改動 -- 對於已經發布的接口,並被不可控系統使用的時候,則不能重構接口
3. 設計的改動 -- 對系統已有的設計進行重構,不如重新設計並實現。
4. 項目十分緊張 -- 對於業務十分緊張的項目,不應該重構。

 

六、如何重構

6.1 重構的一些原則

1. 可靠的測試環境。 --構建自動化單元測試套件(首推TestNG,儘量做到邏輯、邊界全覆蓋, 預期拋出的異常也要測試),並有獨立的團隊進行功能(黑盒)測試,測試出現bug再通過單元測試定位bug。ps:在寫單測的過程中可以適當關注下代碼運行性能。

2. 以微小的步伐修改程序。犯錯可以很容易發現,配合可靠的測試。 重構節奏: 小修改,測試;小修改,測試;小修改,測試........ -- 重構十分強調微小的修改步伐, 便於測試和審查,以減少引發bug的風險
3. 在重構函數內部修改變量名稱,清晰貼切。
4. 函數應該與它所使用的數據結構在同一對象內。
5. 查找引用 -- 通過文本工具查找你需要修改地方的引用,同時利用編譯器發現錯誤。對於被反射引用的地方,一定要記錄並進行詳細的測試。

ps:如何面對不斷變化的世界,如何面對不斷變化的需求。 只有深層次的抽象出不變的對象,然後將變化東西放在葉子類中,與其維護的數據結構同在, 通過繼承與多態來應對這種變化。

6.2 重構手法

6.2.1、重新組織函數

6.2.1.1  提煉函數 Extract Method

* 概述:將一段代碼放入一個獨立函數中,並讓函數名稱解釋該函數的用途。
* 動機:厭惡過長的代碼。如果函數過長,則很難被理解(用途不清晰,需要大段的註釋描述意圖),測試也困難;相反,如果一個函數夠小、有清晰意圖的命名,則被複用的機會更大,函數也更容易被修改。
* 做法:編寫一個新函數,根據這個函數的意圖來命名 -- 如果能對一段代碼給予一個更好表達其意圖的命名,都可以提煉她。
* 注意:這個重構手法最困難的地方在於提煉局部變量:
1. 如果需要讀取源函數的局部變量,則可將其作爲參數傳入新函數。
2. 如果某些局部變量需要在目標函數後面使用,可能需要新函數返回該局部變量改變的值。
3. 對於需要修改多個局部變量的情況,則可不提煉此段代碼,或者將所有臨時變量抽取爲一個類。

6.2.1.2  內聯函數 Inline Method

* 概述:在函數調用點插入函數本體,然後移除該函數
* 動機:1.有些函數的內部代碼比函數名本身更加清晰易讀,則可以內聯。2.函數調用混亂,可以先全部內聯在一起,然後重新提煉。
* 做法:檢查多態覆蓋,檢查函數引用,引用點直接替換爲函數。

6.2.1.3. 內聯臨時變量 Inline Temp

* 概述:將所有對該變量的引用動作,替換爲對她賦值表達式。
* 動機:臨時變量可能妨礙其他的內聯手法。
* 做法:確保只被一個簡單的表達式賦值了一次

6.2.1.4. 以查詢取代臨時變量 Replace Temp with Query

* 概述:程序以某個臨時變量保存表達式的運算結果。則將一個表達式提煉到一個函數中,將將對這個臨時變量的引用替換爲對新函數的調用
* 動機:臨時變量總是驅使你寫更長的函數,因爲臨時變量只在函數內可以訪問到,所以將一些運算得到的臨時變量提煉爲一個函數,可以在任何時候訪問。ps: 導致重複計算,但是使代碼清晰。
* 做法:確保該臨時變量只被賦值一次,然後用新函數替換引用處。可以先將該臨時變量聲明爲final,讓編譯器保證只賦值一次。

ps: 1,2,3,4的操作都讓代碼結構變得更加清晰、簡潔,使得函數名稱本身便具有自注釋功能。

6.2.1.5. 引入解釋性變量 Introduce Explaining Variable

* 概述:將複雜表達式(或其中一部分)的結果放入一個臨時變量,以這個變量的名稱來解釋表達式用於
* 動機:將一個複雜的表達式進行拆解,用臨時變量的名稱來表達部分過程的意圖。

6.2.1.6. 分解臨時變量 Split Temporary Variable

* 概述:程序中某個臨時變量有多個用途,既不是循環變量,也不是收集計算結果變量,則應該對每個用途編寫一個臨時變量
* 動機:臨時變量有不同的用途,或者由不同含義的操作計算而得到,則不應該使用同一臨時變量。

ps: 5,6的拆解都是爲了使得表達式的意圖更加清晰,各種複用臨時變量,複雜計算過程等代碼都可能讓代碼變得含混。

6.2.1.7. 移除對參數的賦值 Remove Assignments to Parameters

* 概述:代碼對一個參數進行賦值,應該用一個臨時變量取代參數的位置。
* 動機:對參數的賦值可能改變其引用的對象(引用參數), 從而丟失了原來的入參引用。所以將參數賦值給一個臨時變量以改變這種含混性。

6.2.1.8. 以函數對象取代函數 Replace Method with Method Object

* 概述: 有一個大型函數,其中過多的局部變量無法進行函數提煉,則將這個放入一個單獨對象中,這樣局部變量就成了對象的成員變量,然後在對象內將大型函數分解爲多個小函數
* 動機: 局部變量太複雜,根本無法拆解, 則使用一個對象來描述該方法。 -- 該對象需要有一個與方法意圖對應的命名。
* 做法: 1. 建立一個新類,並將原類作爲該類的一個常量引用
2. 新類中提供構造函數接收原對象,及原函數的所有參數。
3. 在新類中提供一個函數,將原函數的代碼複製到其中,通過原對象引用相應變量。

6.2.1.9. 替換算法 Substitute Algorithm

* 概述:將函數本體替換爲另一個算法
* 動機:原函數的算法實現比較複雜,切不清晰,或者性能不好。如果你發現有更好的實現方式,勇敢的壯士斷腕,替換該算法吧。


6.2.2、在對象間搬移特性


6.2.2.1.  搬移函數 Move Method

* 概述:將一個函數移動到另一個類中去
* 動機:類中一個函數與其他的類進行頻繁的交流,而與本類交流較少,則將函數移到另一個類中,從而減少調用關係。
* 做法:搬移一個或者一組相關的函數;檢查源類的子類和超類是否有該函數的其他聲明(覆蓋,委託等問題);修改源函數使之成爲一個委託函數。

6.2.2.2. 搬移字段 Move Field

* 概述:將類中一個字段搬移到其他類中
* 動機:本類中的一個字段,被其他類中的字段使用次數更多,搬移字段減少調用關係,間接提高信息隱藏性。
* 做法:如果字段被很多地方引用,則先使用自我封裝(Self-Encapsulation)將字段變成設/取值函數,然後搬移字段,最後修改設/取值函數爲一個委託。

6.2.2.3. 提煉類 Extract Class

* 概述:建立一個新的類,將相關的字段和函數從舊類中搬移到新類
* 動機:1. 一個類的方法過多、責任過大做了由兩個類做的事情。主要是由於當前類對於場景的抽象不夠細緻,導致過於複雜。
2. 一個類中的數個特性朝着不同方向發展,從而變得無所關聯。最主要的表現是子類化,某些特性需以一種方式子類化,某些特性以另一種方式子類化 
* 做法:從一個類中搬移函數和字段到一個新類中,使得新類作爲一個事物的更細緻抽象。
* 例如:一個person類有地址字段和函數,比較複雜。可以將地址抽象爲一個新類,然後person調用這個地址類。

6.2.2.4. 內聯類 Inline Class

* 概述: 將一個類的所有特性搬移到另一個類中,然後移除該類
* 動機: 這個類沒有做太多事情
* 做法: 上述的反過程

6.2.2.5. 隱藏委託關係 Hide Delegate

* 概述: 在服務類建立客戶所需的所有函數,用以隱藏委託關係
* 動機: 對內部進行封裝,通過一個委託類來調用其他對象,這樣調用者可以不用瞭解內部變化,從而降低修改風險。
* 做法: 正如設計模式中的代理模式等, 不僅隱藏調用關係,還可以對調用進行統計等管理。

6.2.2.6. 移除中間人 Remove Middle Man

* 概述: 直接調用受託類
* 動機: 某個類做了過多的簡單委託,每添加一個新特性都需要在委託類中進行相應添加。
* 做法: 上述反過程

6.2.2.7. 引入外加函數 Introduce Foreign Method

* 概述: 在客戶類中建立一個函數,並以第一參數形式傳入一個服務類實例
* 動機: 由於代碼修改權不在自己手裏,但是需要爲某個類添加新特性。可以在外層寫這樣一個函數,並所需要的值都以參數傳入。
* 做法: 在擁有代碼所有權後,將此函數返回她該去的地方

6.2.2.8. 引入本地擴展 Introduce Local Extension

* 概述: 當需要爲一個服務類提供一些額外的函數,但是你無法修改。可建立一個新類,使她包含這些額外的函數。讓這個擴展類成爲源類的子類或者包裝類
* 動機: 爲類做擴展,可以使用裝飾模式或者繼承,使用繼承請確保必定有繼承關係。

ps: 從以上重構方法可以看出,其最終都是爲了減少了代碼中的調用關係,提高代碼的意圖表達。

6.2.3、 重新組織數據

6.2.3.1. 自封裝字段 &封裝字段 &封裝集合 Self Encapsulate Field & Encapsulate Field & Encapsulate Collection

* 概述:將字段進行封裝,然後提供設/取值函數,對於集合取值只能返回可讀的副本
* 動機:隱藏內部數據接口,這樣就算字段數據結構發生變化,只需要調整設/取值函數,從而不影響其他類的調用; 對於集合需要防止外部對引用的修改; 方便上syncrized關鍵字

6.2.3.2. 以對象取代數據值 Replace Data Value with Object

* 概述:將數據項變爲字段
* 動機:數據項爲對一個事物屬性的描述,你無法估量這個場景下該事物屬性的複雜性,所以用一個類構建該數據項,能夠抗擊未來的變化。

6.2.3.3. 將值對象改爲引用對象 Change Value to Reference

* 概述:將這個值對象變成引用對象
* 動機:對一個對象修改的數據,能夠影響引用這個對象的地方

6.2.3.4. 將對象引用改爲值對象 Change Reference to Value

* 概述:將引用對象變爲一個值對象
* 動機:利用其不可變性質

6.2.3.5. 用對象取代數組 Replace Array with Object

* 概述:以對象替換數組,對於數組中的每個元素,以一個字段來表示
* 動機:有些其他語言,將一個對象的屬性存放在一個數組中了。將其重構爲對象,從而可用字段名逐一描述

6.2.3.6. 複製被監視數據 Duplicate Observed Data

* 概述:將數據複製到一個領域對象中,建立一個observer模式,用於同步領域對象和GUI對象內的數據
* 動機:分離界面顯示和業務邏輯,在界面更新數據的時候同步更新領域對象數據

6.2.3.7. 將單向關聯改爲雙向關聯 Change Unidirectional Association to Bidirectional

* 概述:添加一個反向指針,並使修改函數能夠同時更新兩條連接
* 動機:使得兩個類相互依賴,方便訪問對方的成員。 雖然方便,但是調用關係複雜,容易產生殭屍對象,修改時相互影響。

6.2.3.8. 將雙向關聯改爲單項關聯 Change Bidirectional Association to Unidirectional

* 概述:去掉不必要的關聯
* 動機:降低類間複雜度

6.2.3.9. 以字面常量取代魔法數 Replace Magic Number with Symbolic Constant

* 概述:創造一個常量,根據其意義命名,並將有特殊意義的字面數值替換爲這個常量
* 動機:魔數-有特殊意義的數字,使用一個常量來表示這個數字的意義

6.2.3.10. 以數據類取代記錄 Replace Record with Data Class

* 概述:面對傳統編程的記錄結構,爲該記錄創建一個'啞'數據對象
* 動機:與數據庫的記錄進行交互,ORM

6.2.3.11. 以類取代類型碼 Replace Type Code with Class

* 概述:用一個新的類替換該類中用常量數值表示的類型
* 動機:其實就是使用枚舉類型來代替常量枚舉,這樣使得類型被更有意義的描述,並且提供了對應的操作函數,對於複雜的類型十分方便。

6.2.3.12. 以子類取代類型碼 Replace Type Code with Subclasses

* 概述:爲類型碼建立一個繼承宿主類的子類,將根據這個類型碼執行的操作,放入到一個子類中去。
* 動機:將不同類型碼的操作都放入對應的子類中,這樣不經讓代碼更清晰,同時防止錯亂。

6.2.3.13. 用狀態或者策略取代類型碼 Replace Type Code with State/Strategy

* 概述: 類中有類型碼會影響類的行爲,但是無法通過繼承手法來消除
* 動機: 無法直接繼承宿主類,則自己定義一個抽象的類去繼承,與上述一致。

6.2.3.14. 以字段取代子類 Replace Subclass with Fields

* 概述: 各個子類的差別不是打,只在一些常量數據上
* 動機: 將子類中的共同行爲搬移到超類,並在超類中定義這組差異的常量。減少無必要的編碼

6.2.4、 簡化條件表達式

6.2.4.1. 分解條件表達式 Decompose Conditional

* 概述:有一個複雜的條件表達式,從if then else中分別提煉出獨立函數
* 動機:複雜的條件邏輯常常導致代碼複雜度的提高,提煉不同分支,並按照意圖命名,是代碼可讀性和清晰提高

6.2.4.2. 合併條件表達式 Consolidate Conditional Expression

* 概述:有一些列的條件測試,但是都返回相同結果,則合併這些表達式
* 動機:有一些表達式雖然條件不同,但是結果一樣,將這些條件合併

6.2.4.3. 合併重複的條件片段 Consolidate Duplicate Conditional Fragment

* 概述:條件表達式的每個分支有相同的一段代碼,將這些代碼抽取出來,放入表達式之外
* 動機:將每個分支都執行的代碼,搬移到代碼執行分支之外,簡化分支表達式,以使代碼更加清晰。

6.2.4.4. 以衛語句取代嵌套條件表達式 Replace Nested Conditional with Guard Clauses

* 概述:用衛語句處理特殊情況,衛語句就是if then return這種形式的表達式,常用於函數入口處,保護函數體只接受正確的參數。
* 動機:對於情況比較特使的邏輯,使用此表達式可以簡化代碼,使之更加清晰

6.2.4.5. 以多態取代條件表達式 Replace Conditional with Polymorphism

* 概述:將條件表達式的分支,放入每個子類的覆寫函數中,並將原始函數聲明爲abstract
* 動機:子類覆蓋超類的條件表達式,在子類中只關注與自己相關的條件和行爲。這樣將集中在一起的邏輯打散到各個子類中。

6.2.4.6. 引入Null對象 Introduce Null Object

* 概述:當需要再三檢查對象是否爲null,則將null值替換爲一個null對象
* 動機:爲原類建立一個子類,其行爲就是原類的null版本,可以方便多態的進行。空對象的存在,可以省去判空邏輯(判空邏輯只寫在Null對象內部,其他寫在超類)。

6.2.4.7. 引入斷言 Introduece Assertion

* 概述:某段代碼需要對程序狀態做出某種假設,以斷言明確表現這種假設
* 動機:斷言可以幫助閱讀程序代碼所做的假設,可以在調試和調試,測試中廣泛使用

ps: 這些方法都爲了減少條件表達式的複雜性,提高其清晰度和意圖,使用多態和空對象的方式,在一定程度上爲編程提供方便。

6.2.5、 簡化函數調用

6.2.5.1. 函數改名 Rename Method

* 概述:修改函數名稱
* 動機:函數的名稱未能表示函數的意圖

6.2.5.2. 增加參數 Add Parameter

* 概述:爲函數添加一個對象參數,讓這個對象帶進函數所需的信息
* 動機:某個函數需要從調用端獲取更多的信息

6.2.5.3. 移除參數 Remove Parameter

* 概述:將函數參數移除
* 動機:函數本體不在需要某個參數,而參數列表卻有

6.2.5.4. 將查詢函數和修改函數分離 Separate Query from modifier

* 概述:建立兩個不同的函數,其中一個負責修改,另一個負責查詢
* 動機:獲取值得時候會修改函數值,會使得其他只需要取狀態值得調用者關心更多的東西,從而產生副作用

6.2.5.5. 令函數攜帶參數 Parameterize Method

* 概述:建立單一函數,以參數表達那些不同的值。
* 動機:多個函數做着類似的工作,只是因爲少數幾參數,或者參數個數不同。則可以使用一個單一函數將他們統一起來,爲這個函數增加攜帶參數

6.2.5.6. 以明確函數取代參數 Replace Parameter with Explicit Methods

* 概述:針對該參數的每一個可能值,建立一個獨立函數
* 動機:有一個函數,取決於不同的參數值而採取不同的行爲,這時可以爲每個獨立之建立一個獨立函數。 某種程度與上一條相反

6.2.5.7. 保持對象完整 Preserve Whole Object

* 概述:改爲傳遞整個對象
* 動機:從某個對象中取出若干值,並將他們作爲某一次函數調用參數的時候,可以將參數修改爲傳遞整個對象,以減少參數列表和對抗未來改動的風險。

6.2.5.8. 以函數取代參數 Replace Parameter with Methods

* 概述:讓參數接受者去除該項參數,並直接調用前一個函數
* 動機:如果函數可以通過其他途徑取得參數值,那麼久不應該通過參數值獲取。這樣可以減少參數列表,從而減少局部變量。

6.2.5.9. 引入參數對象 Introduce Parameter Object

* 概述:以一個對象取代這些參數
* 動機:某些參數總是很自然的同時出現,當一組參數總是被同時傳遞給多個函數時候,則將這些參數整合成一個對象

6.2.5.10. 移除設置函數 Remove Setting Method

* 概述:去掉字段中的所有設置函數
* 動機:對象中的某個字段只應該在對象創建的時候被設置,然後就不再改變,則不因該提供設值函數,否則可能導致其值被修改,且容易混淆

6.2.5.11. 以工廠函數取代構造函數 Replace Constructor with Factory Method

* 概述:將構造函數替換爲工廠函數
* 動機:通過靜態工廠函數來生產對象,從而將對象生產權利把控在本類中,調用者完成不需要關係這個新對象的構建過程

6.2.5.12. 封裝向下轉形 Encapsulate Downcast

* 概述:將向下轉形動作移到函數中
* 動機:某個函數返回的對象需要調用者自己強轉類型。這是可以將強轉類型封裝在函數內部,使得調用者無需關心實際類型。需要該函數返回類型有限且確定。

6.2.5.13. 以異常取代錯誤碼 Replace Error Code with Exception

* 概述:將錯誤碼改用異常
* 動機:拋出異常能夠更加清楚知道代碼執行過程產生的問題。

6.2.5.14. 以測試取代異常 Replace Exception with Test

* 概述:修改調用者,使它在調用函數之前先做檢查
* 動機:異常只應該用於哪些產生意料之外的錯誤行爲,而不應該成爲條件檢查的替代品。可以提供一個可重複執行的測試函數,讓調用者者調用前先檢查某個條件,在測試函數中處理try catch

ps: 從上述方法可以看出,其最終都是爲了減少函數間傳參的複雜度,從而減少了局部變量個數,也就提高的代碼清晰度。

6.2.6、處理概括關係

6.2.6.1. 字段上移 Pull Up Field

* 概述:將字段移到超類
* 動機:兩個子類擁有相同的字段

6.2.6.2. 函數上移 Pull Up Method

* 概述:將函數移動至超類
* 動機:有些函數各個子類中產生完全相同的結果

6.2.6.3. 構造函數本體上移 Pull Up Constructor Body

* 概述:在超類中新建一個構造函數,並在子類中調用她
* 動機:各個子類中擁有本體幾乎一致的構造函數

6.2.6.4. 字段下移 Push Down Field

* 概述:將這個字段移動到需要她的類中
* 動機:超類中某個字段只被部分子類用到,讓數據與操作在同一個類中。

6.2.6.5. 提煉子類 Extract Subclass

* 概述:新建一個子類,將只被某些實例用到特性移動該子類中
* 動機:子類劃分不夠具體,不夠細緻,導致某個類包含過多本不該自己管理的東西,這是可以提煉一個新的子類。

6.2.6.6. 提煉超類 Extract Superclass

* 概述:爲兩個類建立一個超類,將相同的特性移至超類
* 動機:兩個類具有相似的特性

6.2.6.7. 提煉接口 Extract Interface

* 概述:將相同的子集提煉到一個獨立接口中
* 動機:1. 若干客戶端使用類接口中的同一子集;2. 兩個類的接口有部分相同。這兩種情況都可以抽出一套共有接口。

6.2.6.8. 摺疊繼承體系 Collapse Hierachy

* 概述:將繼承體系合爲一體,以消除繼承體系
* 動機:超類和子類,並沒有太大的區別。 ps:

6.2.6.9. 塑造模板函數 Form Template Method

* 概述:模板模式,不再概述

6.2.6.10. 以委託取代繼承 Replace Inheritance with Delegation

* 概述:裝飾模式,不再概述
* 動機:沒有繼承關係的兩個類使用繼承,不僅沒有代碼複用,還繼承了父類大堆不相干方法,容易造成混淆;對於抽象的方法還必須覆寫。而裝飾模式可以自主選擇需要複用的方法。ps:非繼承關係的擴展請使用裝飾模式;有繼承關係的擴展請使用繼承

6.2.6.11. 以繼承取代委託 Replace Delegation with Inheritance

* 概述:對於有繼承體系的仍然使用繼承體系,這樣可以複用父類的方法和字段。

6.2.7、大型重構

6.2.7.1. 梳理並分解繼承體系 Tease Apart Inheritance

* 概述:建立兩個繼承體系,並通過委託關係讓其中一個可以調用另一個
* 動機:某個繼承體系同時承擔兩項責任,將此繼承體系拆解,使得抽象的分類更清楚、細緻從而提高類的複用率,並使代碼更簡潔。

6.2.7.2. 將過程設計轉化爲對象設計 Convert Procedural Design to Objects

* 概述:將數據記錄變成對象,將大塊的行爲分成小塊,並將行爲移入相關的對象中
* 動機:將面向過程的代碼重構爲面向對象的風格。

6.2.7.3. 將領域和表述/顯示分離 Separate Domain from Presentation

* 概述:將領域邏輯分離出來,爲他們建立獨立的領域類
* 動機:從GUI中抽離領域邏輯,從而做到與顯示的分離。如MVC模式

6.2.7.4. 提煉繼承體系 Extract Hierachy

* 概述:建立繼承體系,以一個子類表示一種特殊情況
* 動機:有一個類做了太多的工作,其中一部分是大量的條件表達式完成的。


ps: 大型重構需要較高的對業務場景的抽象能力,使得分類更精確細緻,從而提高代碼的利用率,使得編碼更簡單,結構更加清楚。
這樣一個細緻的體系:
1. 具有很強的對抗變化能力,她將未來所有可能變化的風險,限定在一個子類,甚至一個函數當中;
2. 具有很強的擴展能力,只需要通過實現不同的子類來增加新的功能,既不影響原有結構,更不影響其清晰度。

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