整理自 RefactoringGuru
代碼異味
——什么?代碼如何“聞味道”??
——它沒有鼻子...但它肯定會發(fā)臭!
代碼膨脹
【代碼膨脹】是代碼、方法和類,它們的規(guī)模已經(jīng)增加到了難以處理的地步。通常,這些異味不會立即出現(xiàn),而是隨著程序的演化而積累(尤其是當(dāng)沒有人努力根除它們的時候)。
過長方法
方法包含的代碼行太多。一般來說,任何超過十行的方法都會讓你產(chǎn)生疑問。
過大的類
一個類包含許多字段/方法/代碼行。
基本類型偏執(zhí)
在簡單任務(wù)中使用基本類型而不是小對象(例如貨幣、范圍、電話號碼的特殊字符串等)
使用常量來編碼信息(例如常量USER_ADMIN_ROLE=1表示具有管理員權(quán)限的用戶。)
在數(shù)據(jù)數(shù)組中使用字符串常量作為字段名。
過長參數(shù)列表
一個方法有三個或四個以上的參數(shù)。
數(shù)據(jù)泥團
有時,代碼的不同部分包含相同的變量組(例如用于連接數(shù)據(jù)庫的參數(shù))。這些組應(yīng)該轉(zhuǎn)化為它們自己的類。
面向?qū)ο鬄E用
所有這些異味都是面向?qū)ο缶幊淘淼牟煌暾虿徽_應(yīng)用。
switch語句
你有一個復(fù)雜的switch運算符或if語句序列。
臨時字段
臨時字段僅在特定情況下獲取其值(因此對象需要它)。除此之外,它們是空的。
被拒絕的繼承
如果子類只使用從其父類繼承的一些方法和屬性,那么層次結(jié)構(gòu)就不正常。不需要的方法可能只是不使用,或者被重新定義并發(fā)出異常。
具有不同接口的備選類
兩個類具有相同的函數(shù),但方法名不同。
更改的阻礙
這些異味意味著,如果你需要在代碼的某個地方更改某些內(nèi)容,那么你也必須在其他地方進行許多更改。因此,程序開發(fā)變得更加復(fù)雜和昂貴。
發(fā)散式更改
在更改類的時候,你發(fā)現(xiàn)自己必須更改許多不相關(guān)的方法。例如,添加新產(chǎn)品類型時,必須更改查找、展示和訂購產(chǎn)品的方法。
散彈式更改
修改任何東西都需要對許多不同的類做出許多小的更改。
平行繼承體系
每當(dāng)你為一個類創(chuàng)建一個子類時,你就會發(fā)現(xiàn)自己需要為另一個類創(chuàng)建一個子類。
可有可無的東西
可有可無的東西是毫無意義和不必要的,如果沒有它,代碼就會更干凈、更高效、更容易理解。
注釋
方法中充滿了解釋性注釋。
重復(fù)代碼
兩段代碼看起來幾乎相同。
冗余類
理解和維護類總是需要花費時間和金錢。因此,如果一個類不足以吸引你的注意力,它應(yīng)該被刪除。
數(shù)據(jù)類
數(shù)據(jù)類是指只包含字段和用于訪問字段的方法(獲取器和設(shè)置器)的類。這些只是其他類使用的數(shù)據(jù)容器。這些類不包含任何附加功能,并且不能獨立操作它們所擁有的數(shù)據(jù)。
死代碼
變量、參數(shù)、字段、方法或類已不再使用(通常是因為它已過時)。
夸大通用性
存在未使用的類、方法、字段或參數(shù)。
耦合器
這一組中的所有異味都會導(dǎo)致類之間的過度耦合,或者顯示如果耦合被過度委托所取代會發(fā)生什么。
功能依賴
一個方法訪問另一個對象的數(shù)據(jù)多于它自己的數(shù)據(jù)。
過度親密
一個類使用另一個類的內(nèi)部字段和方法。
消息鏈
在代碼中可以看到一系列類似于$a->b()->c()->d()的調(diào)用。
中間人
如果一個類只執(zhí)行一個操作,將工作委托給另一個類,那么它為什么存在呢?
其他異味
不完善的庫類
庫遲早會停止?jié)M足用戶需求。由于庫是只讀的,所以問題的唯一解決方案,也就是更改庫,通常是不可能的。
重構(gòu)技巧
組合方法
很多重構(gòu)都致力于正確地組合方法。在大多數(shù)情況下,過長的方法是萬惡之源。這些方法中變幻莫測的代碼隱藏了執(zhí)行邏輯,使得該方法極難理解,甚至更難更改。
這一組中的重構(gòu)技巧簡化了方法,消除了代碼重復(fù),并為未來的改進鋪平了道路。
提取方法
問題:你有一個可以組合在一起的代碼片段。
解決方案:將此代碼移動到一個單獨的新方法(或函數(shù)),并用對該方法的調(diào)用替換舊代碼。
內(nèi)聯(lián)函數(shù)
問題:當(dāng)方法主體比方法本身更明顯時,請使用此技巧。
解決方案:用方法的內(nèi)容替換對方法的調(diào)用,并刪除方法本身。
提取變量
問題:你的表達式很難理解。
解決方案:將表達式或其部分的結(jié)果放在獨立的變量中,這些變量是自解釋的。
內(nèi)聯(lián)臨時變量
問題:你有一個臨時變量,它被分配了一個簡單表達式的結(jié)果,僅此而已。
解決方案:用表達式本身替換對變量的引用。
用查詢替換臨時變量
問題:將表達式的結(jié)果放在局部變量中,以便以后在代碼中使用。
解決方案:將整個表達式移動到一個單獨的方法,并從中返回結(jié)果。查詢方法,而不是使用變量。如有必要,在其他方法中加入新方法。
拆分臨時變量
問題:你有一個局部變量,用于在方法中存儲各種中間值(循環(huán)變量除外)。
解決方案:對不同的值使用不同的變量。每個變量應(yīng)該只負責(zé)一個特定的事情。
移除參數(shù)賦值
問題:某些值被賦給了方法體中的參數(shù)。
解決方案:使用局部變量而不是參數(shù)。
用方法對象替換方法
問題:你有一個很長的方法,其中局部變量相互交織,以至于你不能應(yīng)用【提取方法】。
解決方案:將該方法轉(zhuǎn)換為一個單獨的類,以便局部變量成為該類的字段。然后可以將該方法拆分為同一類中的多個方法。
替代算法
問題:所以你想用一個新的算法替換現(xiàn)有的算法?
解決方案:用新算法替換實現(xiàn)算法的方法體。
在對象間移動功能
即使你在不同的類之間,以不太完美的方式分布了功能,仍然存在希望。
這些重構(gòu)技術(shù)展示了如何在類之間安全地移動功能,創(chuàng)建新的類,以及隱藏實現(xiàn)細節(jié)以防公開訪問。
移動方法
問題:一個方法在另一個類中使用的次數(shù)多于在它自己的類中使用的次數(shù)。
解決方案:在使用該方法最多的類中創(chuàng)建一個新方法,然后將代碼從舊方法移動到這里。將舊方法的代碼轉(zhuǎn)換為對另一個類中新方法的引用,或者將其完全刪除。
移動字段
問題:一個字段在另一個類中使用的次數(shù)比在它自己的類中使用的次數(shù)多。
解決方案:在新類中創(chuàng)建一個字段,并將舊字段的所有使用重定向到該字段。
提取類
問題:當(dāng)一個類做兩個類的工作時,會非常笨拙。
解決方案:相反,創(chuàng)建一個新類,并將負責(zé)相關(guān)功能的字段和方法放在其中。
內(nèi)聯(lián)類
問題:一個類幾乎什么都不做,也不負責(zé)任何事情,也沒有為它規(guī)劃額外的責(zé)任。
解決方案:將所有功能從該類移動到另一個類。
隱藏委托關(guān)系
問題:客戶端從對象 A 的字段或方法中獲取對象 B。然后客戶端調(diào)用對象 B 的方法。
解決方案:在類 A 中創(chuàng)建一個新方法,將調(diào)用委托給對象 B。現(xiàn)在客戶端不知道也不依賴于類 B。
移除中間人
問題:一個類有太多的方法,這些方法只是委托給其他對象。
解決方案:刪除這些方法,并強制客戶端直接調(diào)用最終方法。
引入外部方法
問題:實用程序類不包含所需的方法,并且無法將該方法添加到該類中。
解決方案:將該方法添加到客戶端類,并將實用程序類的對象作為參數(shù)傳遞給它。
引入本地擴展
問題:實用程序類不包含你需要的某些方法。但不能將這些方法添加到類中。
解決方案:創(chuàng)建一個包含這些方法的新類,并使其成為實用程序類的子類或包裝器。
組織數(shù)據(jù)
這些重構(gòu)技術(shù)有助于數(shù)據(jù)處理,用豐富的類功能替換基本類型。
另一個重要的結(jié)果是解開了類的關(guān)聯(lián),這使得類更具可移植性和可重用性。
自封裝字段
問題:你直接訪問類內(nèi)的私有字段。
解決方案:為字段創(chuàng)建一個獲取器和設(shè)置器,并僅使用它們訪問字段。
用對象替換數(shù)據(jù)值
問題:一個類(或一組類)包含一個數(shù)據(jù)字段。該字段有自己的行為和相關(guān)數(shù)據(jù)。
解決方案:創(chuàng)建一個新類,將舊字段及其行為放在該類中,并將該類的對象存儲在原始類中。
將值更改為引用
問題:所以你有單個類的許多相同實例,并需要用單個對象替換它。
解決方案:將相同的對象轉(zhuǎn)換為單個引用對象。
將引用更改為值
問題:你有一個太小且很少更改的引用對象,因此無法管理其生命周期。
解決方案:將其轉(zhuǎn)化為值對象。
用對象替換數(shù)組
問題:你有一個包含各種類型數(shù)據(jù)的數(shù)組。
解決方案:將數(shù)組替換為每個元素都有單獨字段的對象。
重復(fù)的被觀測數(shù)據(jù)
問題:存儲在類中的領(lǐng)域數(shù)據(jù)是否負責(zé)GUI?
解決方案:那么最好將數(shù)據(jù)分成不同的類,確保領(lǐng)域類和GUI之間的連接和同步。
將單向關(guān)聯(lián)改為雙向關(guān)聯(lián)
問題:你有兩個類,每個類都需要使用另一個類的功能,但它們之間的關(guān)聯(lián)只是單向的。
解決方案:將缺少的關(guān)聯(lián)添加到需要它的類中。
將雙向關(guān)聯(lián)改為單向關(guān)聯(lián)
問題:類之間存在雙向關(guān)聯(lián),但其中一個類不使用另一個類的功能。
解決方案:刪除未使用的關(guān)聯(lián)。
用符號常量替換幻數(shù)
問題:你的代碼使用了一個具有特定含義的數(shù)字。
解決方案:將這個數(shù)字替換為一個常量,該常量有一個人類可讀的名稱來解釋數(shù)字的含義。
封裝字段
問題:你有一個公共字段。
解決方案:將字段設(shè)置為私有,并為其創(chuàng)建訪問方法。
封裝集合
問題:一個類包含一個集合字段和一個用于處理集合的簡單獲取器和設(shè)置器。
解決方案:將獲取器的返回值設(shè)為只讀,并創(chuàng)建用于添加/刪除集合元素的方法。
用類替換類型代碼
問題:一個類有一個包含類型代碼的字段。這種類型的值不用于運算符條件,也不會影響程序的行為。
解決方案:創(chuàng)建一個新類,并使用其對象而不是類型代碼的值。
用子類替換類型代碼
問題:你有一個直接影響程序行為的代碼類型(此字段的值觸發(fā)條件中的各種代碼)。
解決方案:為代碼類型的每個值創(chuàng)建子類。然后將相關(guān)行為從原始類提取到這些子類中。用多態(tài)替換控制流代碼。
用狀態(tài)/策略替換類型代碼
問題:你有一個影響行為的代碼類型,但不能使用子類來消除它。
解決方案:用狀態(tài)對象替換類型代碼。如果需要用類型代碼替換字段值,則另一個狀態(tài)對象為“已插入”。
用字段替換子類
問題:你的子類只在(常量返回)方法上有所不同。
解決方案:用父類中的字段替換方法,并刪除子類。
簡化條件表達式
隨著時間的推移,條件的邏輯往往變得越來越復(fù)雜,還有更多的技術(shù)可以解決這個問題。
分解條件
問題:你有一個復(fù)雜的條件(if-then/else或switch)。
解決方案:將條件的復(fù)雜部分分解為單獨的方法:條件、then和else。
合并條件表達式
問題:你有多個條件產(chǎn)生相同的結(jié)果或操作。
解決方案:將所有這些條件合并到一個表達式中。
合并重復(fù)的條件片段
問題:在條件語句的所有分支中都可以找到相同的代碼。
解決方案:將代碼移到條件之外。
移除控制標(biāo)志
問題:有一個布爾變量充當(dāng)多個布爾表達式的控制標(biāo)志。
解決方案:使用break、continue和return代替變量。
使用守衛(wèi)子句來代替嵌套的條件判斷
問題:有一組嵌套的條件,很難確定代碼執(zhí)行的正常流程。
解決方案:將所有特殊檢查和邊界情況隔離到單獨的子句中,并將其放在主要檢查之前。理想情況下,你應(yīng)該有一個條件列表,一個接一個。
用多態(tài)替換條件
問題:你有一個條件,根據(jù)對象類型或?qū)傩詧?zhí)行各種操作。
解決方案:創(chuàng)建與條件的分支相匹配的子類。在它們中,創(chuàng)建一個共享方法,并將代碼從條件的相應(yīng)分支移動到它。然后用相關(guān)的方法調(diào)用替換條件。結(jié)果是,根據(jù)對象類,可以通過多態(tài)實現(xiàn)正確的實現(xiàn)。
引入空對象
問題:由于一些方法返回null而不是真實對象,所以在代碼中有很多null檢查。
解決方案:返回一個顯示默認行為的空對象,而不是null。
引入斷言
問題:要使部分代碼正常工作,某些條件或值必須為true。
解決方案:用特定的斷言檢查替換這些假設(shè)。
簡化方法調(diào)用
這些技術(shù)使方法調(diào)用更簡單、更容易理解。這反過來簡化了用于類之間交互的接口。
重命名方法
問題:方法的名稱不能解釋該方法的功能。
解決方案:重命名該方法。
添加參數(shù)
問題:方法沒有足夠的數(shù)據(jù)來執(zhí)行某些操作。
解決方案:創(chuàng)建一個新參數(shù)來傳遞必要的數(shù)據(jù)。
刪除參數(shù)
問題:方法體中沒有使用某個參數(shù)。
解決方案:刪除未使用的參數(shù)。
將查詢與修改分開
問題:是否有一個方法可以返回一個值,但也可以更改對象內(nèi)部的某些內(nèi)容?
解決方案:將該方法分為兩種不同的方法。正如你所料,其中一個應(yīng)該返回值,另一個則修改對象。
將方法參數(shù)化
問題:多個方法執(zhí)行類似的操作,這些操作只在其內(nèi)部值、數(shù)字或操作上有所不同。
解決方案:通過使用一個將傳遞必要特殊值的參數(shù)來組合這些方法。
用顯式方法替換參數(shù)
問題:一個方法被分成幾個部分,每個部分的運行取決于一個參數(shù)的值。
解決方案:將方法的各個部分提取到它們自己的方法中,并調(diào)用它們,而不是原始方法。
保存整個對象
問題:從一個對象中獲取多個值,然后將它們作為參數(shù)傳遞給一個方法。
解決方案:相反,嘗試傳遞整個對象。
用方法調(diào)用替換參數(shù)
問題:調(diào)用一個查詢方法并將其結(jié)果作為參數(shù)傳遞給另一個方法,而該方法可以直接調(diào)用該查詢。
解決方案:不要通過參數(shù)傳遞值,而是嘗試在方法體中放置一個查詢調(diào)用。
引入?yún)?shù)對象
問題:你的方法包含一組重復(fù)的參數(shù)。
解決方案:用對象替換這些參數(shù)。
移除設(shè)置方法
問題:字段的值應(yīng)該只在創(chuàng)建時設(shè)置,之后任何時候都不能更改。
解決方案:刪除設(shè)置字段值的方法。
隱藏方法
問題:一個方法不被其他類使用,或者只在它自己的類層次結(jié)構(gòu)中使用。
解決方案:將方法設(shè)置為私有或受保護。
用工廠方法代替構(gòu)造器
問題:你有一個復(fù)雜的構(gòu)造器,它的功能不僅僅是在對象字段中設(shè)置參數(shù)值。
解決方案:創(chuàng)建一個工廠方法并使用它替換構(gòu)造器調(diào)用。
用異常替換錯誤代碼
問題:方法返回指示錯誤的特殊值?
解決方案:拋出一個異常。
用測試替換異常
問題:在一個簡單的測試就能完成任務(wù)的地方拋出異常?
解決方案:用條件測試替換異常。
處理泛化
抽象有自己的一組重構(gòu)技術(shù),主要關(guān)于沿著類繼承層次結(jié)構(gòu)移動功能、創(chuàng)建新的類和接口、用委托代替繼承以及相反。
上移字段
問題:兩個類具有相同的字段。
解決方案:從子類中刪除字段,并將其移動到超類。
上移方法
問題:你的子類具有執(zhí)行類似工作的方法。
解決方案:使方法相同,然后將它們移動到相關(guān)的超類。
上移構(gòu)造器主體
問題:你的子類的構(gòu)造器的代碼基本相同。
解決方案:創(chuàng)建一個超類構(gòu)造器,并將子類中相同的代碼移動到它。在子類構(gòu)造器中調(diào)用超類構(gòu)造器。
下移方法
問題:超類中實現(xiàn)的行為是僅由一個(或幾個)子類使用的嗎?
解決方案:將此行為移動到子類。
下移字段
問題:字段是否僅用于少數(shù)子類?
解決方案:將字段移動到這些子類。
提取子類
問題:某個類具有僅在某些情況下使用的功能。
解決方案:創(chuàng)建一個子類,并在這些情況下使用它。
提取超類
問題:有兩個類具有相同的字段和方法。
解決方案:為它們創(chuàng)建一個共享超類,并將所有相同的字段和方法移動到其中。
提取接口
問題:多個客戶端使用類接口的同一部分。另一種情況:兩個類中的部分接口是相同的。
解決方案:將這個相同的部分移動到它自己的接口。
折疊層次結(jié)構(gòu)
問題:你有一個類層次結(jié)構(gòu),其中一個子類實際上與其超類相同。
解決方案:合并子類和超類。
形成模板方法
問題:你的子類實現(xiàn)的算法包含順序相同的類似步驟。
解決方案:將算法結(jié)構(gòu)和相同的步驟移動到一個超類,并將不同步驟的實現(xiàn)留在子類中。
用委托替換繼承
問題:有一個子類只使用其超類的一部分方法(或者不可能繼承超類數(shù)據(jù))。
解決方案:創(chuàng)建一個字段并在其中放置一個超類對象,將方法委托給超類對象,并擺脫繼承。
用繼承替換委托
問題:一個類包含許多簡單的方法,這些方法將委托給另一個類的所有方法。
解決方案:使該類繼承另一個類,這樣就不需要委托方法。