软件配置管理(五)常用重构技巧
文章目錄
- 一、重新組織函數
- 1.提煉函數
- 2.內聯函數
- 3.內聯臨時變量
- 4.以查詢取代臨時變量
- 5.引入解釋性變量
- 6.分解臨時變量
- 7.移除對參數的賦值
- 8.以函數對象取代函數
- 9.替換算法
- 二、在對象之間搬移特性
- 1.搬移函數
- 2.搬移字段
- 3.提煉類
- 4.將類內聯化
- 5.隱藏“委托關系”
- 6.移除中間人
- 7.引入外加函數
- 8.引入本地擴展
- 三、重新組織數據
- 1.自封裝字段
- 2.以對象取代數據值
- 3.將值對象改為引用對象
- 4.將引用對象改為值對象
- 5.以對象取代數組
- 6.復制“被監視的數據”
- 7.將單向關聯改為雙向關聯
- 8.將雙向關聯改為單向關聯
- 9.以字面常量取代魔法數
- 10.封裝字段
- 11.封裝集合
- 12.以數據類取代記錄
- 13.以類取代類型🐎
- 14.以子類取代類型🐎
- 15.以State(狀態模式)/Strategy(策略模式)取代類型🐎
- 16.以字段取代子類
- 四、簡化條件表達式
- 1.分解條件表達式
- 2.合并條件表達式
- 3.合并重復的條件片段
- 4.移除控制標記
- 5.以衛語句取代嵌套條件表達式
- 6.以多態取代條件表達式
- 7.引入Null對象
- 8.引入斷言
- 五、 簡化函數調用
- 1.函數改名
- 2.添加參數
- 3.移除參數
- 4.將查詢函數和修改函數分離
- 5.令函數攜帶參數
- 6.以明確函數取代參數
- 7.保持對象完整
- 8.以函數取代參數
- 9.引入參數對象
- 10.移除設值函數
- 11.隱藏函數
- 12.以工廠函數取代構造函數。
- 13.封裝向下轉型
- 14.以異常取代錯誤碼
- 15.以測試取代異常
- 六、處理概括關系
- 1.字段上移
- 2.函數上移
- 3.構造函數本體上移
- 4.函數下移
- 5.字段下移
- 6.提煉子類
- 7.提煉超類
- 8.提煉接口
- 9.折疊繼承體系
- 10.塑造模板函數。
- 11.以委托取代繼承
- 12.以繼承取代委托
- 七、大型重構
- 1.梳理并分解繼承體系
- 2.將過程化設計轉化為對象設計
- 3.將領域和表述/顯示分離
- 4.提煉繼承體系
一、重新組織函數
1.提煉函數
將一段代碼組織并獨立出來,放進一個獨立函數中,并讓函數名稱解釋用途。
2.內聯函數
若一個函數的函數體足以解釋函數作用,其解釋性與函數名稱相差無幾。那么可以移除函數,將函數體插入函數調用點。
3.內聯臨時變量
若一個臨時變量只被一個簡單的表達式賦值一次,且這個變量妨礙了其他的重構手段。那么可以將所有對該變量的引用替換為表達式本身。
4.以查詢取代臨時變量
若在一個函數中,一個表達式的運算結果被賦值給了一個臨時變量,而大量的臨時變量會導致理解性變差,而且臨時變量無法被其他函數使用。此時可以把表達式獨立為一個函數(稱為查詢式),并將所有臨時變量的引用替換為對函數的調用。
5.引入解釋性變量
在復雜的表達式中,可以將表達式的中間結果賦給一個臨時變量,用變量名解釋表達式的用途。
6.分解臨時變量
在一段程序中,某個臨時變量被多次賦值,但每次賦值的原因都不同(既不是循環變量,也不是用來收集計算結果的變量)。此時可以針對每次賦值都創造一個獨立的臨時變量,增強解釋性。
7.移除對參數的賦值
不要對函數的參數進行賦值,用臨時變量取代被重新賦值的參數。
8.以函數對象取代函數
只要將相對獨立的代碼從大函數提煉出來,就可以大大提高代碼的可讀性。但局部變量(臨時變量)的存在會增加函數分解的難度。如果一個函數中的局部變量泛濫成災,那么想分解這個函數是非常困難的?!耙圆樵兲鎿Q臨時變量”手法可以幫助減輕負擔,但有時候還是會發現根本無法拆解一個需要拆解的函數。這種情況就應該考慮使用函數對象來解決。
在一個大型函數中,臨時變量的使用導致無法提煉函數,此時可以將這個函數放進一個單獨的對象中,將臨時變量作為對象的成員變量,這樣就可以將新對象中的大型函數分解。
示例代碼
9.替換算法
在一個函數體內,將某個算法替換為另一個算法。
二、在對象之間搬移特性
1.搬移函數
在一個類中的某個函數相較于所在類,其與另一個類有著更加多的聯系(依戀情結)??梢栽谄渌缿俚念愔薪⒁粋€有類似行為的新函數,將舊函數變成一個委托函數,或直接移除。
2.搬移字段
在一個類中的某個字段相較于所在類,其與另一個類有著更加多的聯系??梢栽谄淠繕祟愔行陆ㄒ粋€字段,并將對舊字段的引用轉移到新字段中。
3.提煉類
若一個類不符合單一職責原則,那么可以新建一個類,并將相關的字段和函數轉移。
4.將類內聯化
若一個類的職責過小,那么可以將這個類中的所有字段和函數轉移到另一個類中,并移除原類。
5.隱藏“委托關系”
“封裝”意味每個對象都應該盡可能少了解系統的其他部分。
如果某個客戶先通過委托類的字段得到另一個對象,然后調用后者的函數,那么客戶必須知曉這一層委托關系。萬一委托關系發生變化,客戶也得相應變化。
可以在服務對象上放置一個簡單的委托函數,將委托關系隱藏起來,從而去除這種依賴。這么一來,即便將來發生委托關系上的關系,變化也將被限制在服務對象中,不會涉及客戶。
示例代碼
6.移除中間人
在“隱藏委托關系”中,談到了“封裝受托對象的好處”。但是這層封裝是需要付出代價的:每當客戶要使用受托類的新特性時,你就必須在服務器端添加一個簡單委托函數。但是,隨著受托類的特性越來越多,這一過程就會讓你變得痛苦。
這時,服務類做了過多的簡單委托動作,完全變成了一個“中間人”,應該讓客戶直接調用受托類。
該手法和“隱藏委托關系”正好相反,正是由于相反,才能夠在實際的應用中進行靈活的變通。可能一些委托關系需要保留,而另一些卻需要移除,讓客戶直接使用受托對象。這些都是可以隨之變通的。
7.引入外加函數
你正在使用一個類,它真的很好,為你提供了需要的所有服務。
而后,你又需要一項新服務,這個類卻無法供應。于是你開始咒罵“為什么不能做這件事?”如果可以修改源碼,你便可以自行添加一個新函數;
若無法修改此類的源代碼(不完美的庫類),此時可以在客戶端建立一個函數,并在第一個參數傳入服務類的實例 示例代碼
適配器模式也可完成這個任務,但會增加一個額外的類。
8.引入本地擴展
我們都無法預知一個類的未來,它們常常無法為你預先準備一些有用的函數。如果可以修改源碼,那就太好了,那樣就可以直接加入自己需要的函數。但是你經常無法修改源碼。如果只是需要一兩個函數,可以引入外加函數進行處理。但如果需要多個函數,外加函數就很難控制它們了。
所以,需要將這些額外函數組織起來,放到一個恰當的地方去。要達到這樣的目的,需要用到子類化(適配器模式)和包裝(裝飾器模式)這兩種技術。這種情況下,把子類或包裝類統稱為本地擴展。
三、重新組織數據
1.自封裝字段
不要直接訪問類中的成員變量(字段),這會增大耦合。為這些字段創造getter和setter方法,且自能用這些方法訪問字段。
2.以對象取代數據值
開發初期,我們以簡單的數據項表示簡單的情況。但是隨著開發的進行,你可能會發現,這些簡單數據項不再那么簡單了。比如說,一開始你可能會用一個字符串來表示“電話號碼”概念,但是隨后就會發現,電話號碼需要“格式化”、“抽取區號”之類的特殊行為。如果這樣的數據有一兩個,還可以把相關函數放進數據項所屬的對象里;但是Duplicate Code(重復代碼)壞味道和Feature Envy(依戀情結)壞味道很快就會從代碼中散發出來。
當這些壞味道開始出現,就應該將數據值變成對象。
3.將值對象改為引用對象
系統中的對象可以分為引用對象和值對象.
引用對象每個對象代表真實世界的一個實物,你可以直接以“==”檢查兩個對象是否相等。
值對象像是“錢”、“日期”這樣的東西,它們完全由其所含的數據值來定義,你并不在意副本的存在。值對象有一個非常重要的特性,它們應該是不可變的
有時候,你會從一個簡單的值對象開始,在其中保存少量的不可修改的數據。而后,你可能會希望給這個對象加入一些可修改數據,并確保對任何一個對象的修改都能影響到所有引用此一對象的地方,這時候,你就希望將這個對象變為一個引用對象。
4.將引用對象改為值對象
若一個引用對象很小,且確定其是不可變的,那么可以將其變為值對象,方便管理。
5.以對象取代數組
若一個數組中的各個元素代表不同的東西,那么將這個數組替換為對象,使用成員變量標識數組的不同元素。
6.復制“被監視的數據”
一個分層良好的系統,應該將處理用戶界面(GUI)和處理業務邏輯(Business Logic)的代碼分開。
若一些領域數據存在GUI里,而業務邏輯函數需要訪問這些數據。此時可以將這些數據復制到一個領域對象中,并用觀察者模式同步領域對象和GUI對象的重復數據。
7.將單向關聯改為雙向關聯
若兩個類都需要使用對方的屬性,但其中只有一條單向連接。此時可以添加一個方向指針,并使修改雙方關系的函數同時更新這兩條連接。
8.將雙向關聯改為單向關聯
在擁有雙向關聯的兩個類間,當其中一個類不再需要另一個類的特性使,可以去除不必要的關聯。
9.以字面常量取代魔法數
若一個字面常量帶有特殊含義,此時可以創造一個常量,賦值為字面常量,并使用有意義的變量名。
10.封裝字段
將public字段聲明為private并提供訪問函數。
11.封裝集合
在函數返回集合時,讓函數返回這個集合的只讀副本,并提供修改集合元素的函數。
示例代碼
12.以數據類取代記錄
記錄型結構是許多編程環境的共同性質。有一些理由使它們被帶進面向對象程序之中:你可能面對的是一個遺留程序,也可能需要通過一個傳統API來與記錄結構交流,或是處理從數據庫讀出的記錄。這些時候你就有必要創建一個接口類,用以處理這些外來數據。最簡單的做法就是先建立一個看起來類似外部記錄的類,以便日后將某些字段和函數搬移到這個類中。
新建一個類(“啞”數據對象),表示這個記錄。對應記錄中的每一項數據,在新建的類中建立對應的一個private字段。并提供相應的取值/設值函數。
13.以類取代類型🐎
若一個類有一個不影響類行為的類型碼,可以以一個新的類替換這個數值類型碼。
在使用Replace Type Code with Class (以類取代類型碼)之前,你應該先考慮類型碼的其他替換方式。
只有當類型碼是純粹數據時(也就是類型碼不會在switch語句中引起行為變化時),你才能以類來取代它。
更重要的是:任何switch語句都應該運用Replace Conditional with Polymorphism (以多態取代條件表達式)去掉。
為了進行那樣的重構,你首先必須運用 Replace Type Code with Subclass (以子類取代類型碼)或Replace Type Code with State/Strategy (以狀態策略取代類型碼),把類型碼處理掉。
14.以子類取代類型🐎
若一個類有一個會影響類行為的類型碼,可以以一個子類取代這個類型碼。
示例代碼
15.以State(狀態模式)/Strategy(策略模式)取代類型🐎
若一個類有一個會影響行為的類型碼,但無法通過繼承消除它(類型碼的值在對象生命期中發生變化或其他原因使得宿主類不能被繼承),此時可以用狀態對象或具體策略對象取代類型碼。
16.以字段取代子類
若一個類的各個子類間唯一的差別只在“返回常量數據”的函數身上,此時可以修改這些函數,是它們返回超類的某個(新增)字段,然后銷毀子類。
建立子類的目的是為了增加新特性或變化父類行為,如果其帶來的影響只是返回的數據值不同,就沒有存在的必要
四、簡化條件表達式
1.分解條件表達式
若一個條件表達式過于復雜,可以將if-then-else三個段落分別提煉獨立的函數。
2.合并條件表達式
若在并列的條件表達式中,它們的then語句有相同操作,可以用邏輯與和邏輯或語句將各條件中的if語句聯立,并將then語句合并。
合并后的條件代碼會告訴你“實際上只有一次條件檢查,只不過有多個并列條件需要檢查而已”,從而使這一次檢查的用意更清晰。
其次,這項重構往往可以為你使用“提取方法”做好準備。
3.合并重復的條件片段
若在條件表達式的每個分支上有相同的代碼,此時可以將這段重復代碼搬移到條件表達式之外。
4.移除控制標記
若在一系列布爾表達式中,某個變量帶有控制標記的作用,可以用break、continue和return語句取代控制標記。
5.以衛語句取代嵌套條件表達式
條件表達式通常有2種表現形式。第一:所有分支都屬于正常行為。第二:條件表達式提供的答案中只有一種是正常行為,其他都是不常見的情況。
這2類條件表達式有不同的用途。如果2條分支都是正常行為,就應該使用形如if……else……的條件表達式;
如果某個條件極其罕見,就應該單獨檢查該條件,并在該條件為真時立刻從函數中返回。這樣的單獨檢查常常被稱為“衛語句”。
給某個分支以特別的重視。它告訴閱讀者:這種情況很罕見,如果它真的發生了,請做一些必要的整理工作,然后退出。
6.以多態取代條件表達式
若一個條件表達式根據對象類型的不同而選擇不同的行為,此時應將條件表達式的每個分支拆分為一個子類內的override函數中,然后將原始函數聲明為抽象函數。
7.引入Null對象
將null值替換為null對象,這樣就不用在每一個引用處判斷對象是否為空。
示例代碼
8.引入斷言
為一段需要對程序狀態做出某種假設的代碼添加斷言,以明確表現這種假設。
五、 簡化函數調用
1.函數改名
修改那些不能明確揭示函數用途的函數名稱。
2.添加參數
但某個函數需要從調用端獲得更多信息時,為這個函數添加一個對象參數,讓該對象帶進函數所需信息。
除了添加參數外,你常常還有其他選擇。只要可能,其他選擇都比添加參數要好,因為它們不會增加參數列的長度。過長的參數列是不好的味道,因為程序員很難記住那么多參數而且長參數列往往伴隨著壞味道:數據泥團(Data Clumps)。
3.移除參數
移除函數體不再用到的參數。
4.將查詢函數和修改函數分離
將既返回對象狀態值,又能修改對象狀態的函數分離為兩個函數,一個負責查詢,一個負責修改。
5.令函數攜帶參數
若兩個任務類似的函數,只因少數幾個值使行為略為不同,這種情況下,可以建立單一的函數,并通過參數來處理那些變化,用以簡化問題,去除重復代碼,并提高靈活性。
6.以明確函數取代參數
若一個函數完全取決于參數值而采取不同的行為,可以針對參數的每一個可能值建立一個單獨的函數。
7.保持對象完整
若函數參數是某個對象中的若干字段,此時可以改為傳遞整個對象。
8.以函數取代參數
如果函數可以通過其他途徑獲得參數值,那么它就不應該通過參數取得該值。
如果對象調用了某個函數,并將這個函數的返回值作為參數傳遞給另一個參數,但后者其實可以直接調用前者,這時應讓后者在函數體中直接調用前者,并獲取前者返回值,而不需要使用參數傳遞。
9.引入參數對象
若某些參數總是同時出現,則可以以一個對象取代這些參數。
10.移除設值函數
若類中某個字段只在對象被創建時被設值,之后不再改變,則應去掉該字段的setter函數
11.隱藏函數
如果一個函數從來沒有被其他類用到,那么應將這個函數修改為privete。
12.以工廠函數取代構造函數。
如果創建對象時不僅僅要做簡單的構建動作,那么應將構造函數替換為工廠函數。
13.封裝向下轉型
向下轉型也許是無法避免的,但你仍然應該盡可能少做。如果你的某個函數返回一個值,并且你知道所返回的對象類型比函數簽名所昭告的更特化,你便是在函數用戶身上強加了非必要的工作。這種情況下你不應該要求用戶承擔向下轉型的責任,應該盡量為他們提供準確的類型。
以上所說的情況,常會在返回迭代器或集合的函數身上發生。此時你就應該觀察人們拿這個迭代器干什么用,然后針對性地提供專用函數。
將向下轉型的動作轉移到函數體中,而不是讓用戶完成這個動作。
14.以異常取代錯誤碼
對于一個成員方法而言,它要么執行成功,要么執行失敗。成員方法執行成功的情況很容易理解,但如果執行失敗了卻沒有那么簡單,因為我們需要將執行失敗的原因通知調用者。拋出異常和返回錯誤代碼都是用來通知調用者的手段
當我們想要告示調用者更多細節的時候,就需要與調用者約定更多的錯誤代碼。于是。錯誤代碼飛速膨脹,直到看起來似乎無法維護,因為我們需要查找并確認錯誤代碼。
使用了CLR異常機制后,代碼更加清晰,易于理解。
而在某些情況下,錯誤代碼將無用武之地,如構造函數、操作符重載及屬性。語法的特性決定了其不具備任何返回值,于是異常處理被當做取代錯誤代碼的首要選擇。
將函數返回錯誤碼改為使用異常。
示例代碼
15.以測試取代異常
異常只應該被用于異常的,罕見的行為,也就是那些產生意料之外錯誤的行為,而不應該成為條件檢查的替代品
如果調用者有能力在異常被拋出之前檢查拋出條件,那么應該讓調用者在調用函數前檢查條件。
示例代碼
六、處理概括關系
1.字段上移
當兩個子類擁有相同字段時,將該字段移至超類。
2.函數上移
當函數在各個子類產生完全相同的結果時,將該函數移至超類。
3.構造函數本體上移
當各個子類中的構造函數幾乎完全移至時,在超類新建一個構造函數,并在子類構造函數中調用它。
4.函數下移
當超類的某個函數只與部分子類有關時,將這個函數移到相關子類中。
5.字段下移
當超類中的某個字段只被部分子類用到時,將這個字段移到需要它的子類中。
6.提煉子類
當類中某些特性只被部分實例用到時,新建一個子類,將那部分特性移到子類中。
Extract Class(提煉類)是Extract Subclass之外的另一種選擇,兩者之間的抉擇其實就是委托和繼承之間的抉擇。Extract Subclass通常更容易進行,但它也有限制:一旦對象創建完成,你無法再改變與類型相關的行為。此外,子類只能用以表現一組變化。如果你希望一個類以幾種不同的方式變化,就必須使用委托。
7.提煉超類
當兩個類有相似特性時,為這兩個類建立一個超類,將相同特性移至超類中。
8.提煉接口
如果若干用戶使用類中相同的特殊方法,或兩個類的方法有部分相同,可以將相同方法提煉到一個接口中,并讓這些類實現接口。
實例代碼
9.折疊繼承體系
如果超類和子類沒有太大區別,那么將他們合為一體。
10.塑造模板函數。
如果一些子類中相應的某些函數以相同順序執行類似的操作,但操作細節在不同子類間有所不同。這時可以將子類中對應函數修改為相同的函數名,并在超類中創建對應的被重寫的函數,最后在超類中創建一個新的函數,按順序執行這些函數完成操作。
實例代碼
11.以委托取代繼承
如果某個子類只是用了超類的一部分接口,或者沒有使用超類的數據,那么應去除繼承關系,轉而在子類中新建一個持有超類的字段,并調整子類函數,改為委托超類。
代碼示例
12.以繼承取代委托
如果在兩個類中使用委托關系會導致編寫許多極簡單的委托函數,這時應考慮讓委托類繼承受托類。
七、大型重構
1.梳理并分解繼承體系
如果某個繼承體系同時承擔兩項責任,這時應建立兩個繼承體系,并通過委托關系使其中一個調用另一個。
2.將過程化設計轉化為對象設計
針對以傳統過程化分隔編寫的代碼,將它的數據記錄變成對象,將大塊行為分成小塊并移入相關對象中。
3.將領域和表述/顯示分離
如果某些GUI類包含了領域邏輯,那么應將領域邏輯分離出來,建立單獨的領域類。
4.提煉繼承體系
如果某個類做了太多的、以大量條件表達式完成的工作,那么應建立繼承體系,以一個子類表示其中的一種特殊情況。
總結
以上是生活随笔為你收集整理的软件配置管理(五)常用重构技巧的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 软件配置管理(四)代码味道与重构
- 下一篇: 软件项目组织管理(一)项目管理概述