何時重構(gòu)、何時停止重構(gòu)
學(xué)會判斷一個類內(nèi)有多少實例變量算是太大、一個函數(shù)內(nèi)有多少行代碼算太長
下面是對各種壞味道的簡短匯集和解釋
Duplicated Code
Long Method
Large Class
Long Parameter List
Divergent Change(多種變化導(dǎo)致一個類的變化)
Shotgun Surgery(一種變化導(dǎo)致多個類的變化)
Feature Envy(函數(shù)對某個類的興趣高于對自己所處類的興趣。)
Data Clumps(多個經(jīng)常一起出現(xiàn)的字段或參數(shù))
Primitive Obsession(不使用基本類型而使用類)
Switch Statements(同樣的switch語句散布于不同地點。)
Parallel Inheritance Hierarchies(每當(dāng)你為某個類增加一個子類,同時也必須為另一個類相應(yīng)地增加一個子類。)
Lazy Class(冗贅類)
Speculative Generality(夸夸其談未來性)
如果某個抽象類其實沒有太大作用->Collapse Hierarchy。
不必要的委托可運用Inline Class除掉。
函數(shù)的某些參數(shù)未被用上->Remove Parameter。
函數(shù)名稱帶有多余的抽象意味->Rename Method,讓它現(xiàn)實一些。
函數(shù)或類的唯一用戶是測試用例,把它們連同其測試用例一并刪掉。但如果它們的用途是幫助測試用例檢測正當(dāng)功能,則必須刀下留人。
Temporary Field(某個類內(nèi)某個實例變量僅為某種特殊情況而設(shè)。)
Message Chains(用戶向一個對象請求另一個對象,然后再向后者請求另一個對象,然后再請求另一個對象.... )
Middle Man(過度運用委托)
Inappropriate Intimacy(兩個類過于親密,花費太多時間去探究彼此的private成分。 )
Alternative Classes with Different Interfaces(異曲同工的類)
Incomplete Library Class(不完美的類庫)
Data Class(限制屬性的訪問性)
Refused Bequest(被拒絕的遺贈)
子類破壞父類的接口
Comments(過多的注釋)
3.1 Duplicated Code(重復(fù)代碼)
同一個類的兩個函數(shù)含有相同的表達(dá)式(Extract Method)
兩個互為兄弟的子類內(nèi)含有相同表達(dá)式:對兩個類使用Extract Method,然后再對被提煉出來的代碼使用Pull Up Method,將它推入超類內(nèi)。
如果代碼之間只是類似,并非完全相同,那么就得運用Extract Method將相似部分和差異部分分割開,構(gòu)成單獨一個函數(shù)。然后可能發(fā)現(xiàn)可以運用Form Template Method獲得一個Template Method設(shè)計模式。有些函數(shù)以不同的算法做相同的事,你可以選擇其中較清晰的一個,并使用Subsitute Algorithm將其他函數(shù)的算法替換掉。
如果兩個毫不相關(guān)的類出現(xiàn)Duplicated Code,就應(yīng)該考慮對其中一個使用Extract Class,將重復(fù)代碼提煉到一個獨立類中,然后在另一個類內(nèi)使用這個新類。
但是重復(fù)代碼所在的函數(shù)也可能的確只應(yīng)該屬于某個類,另一個類只能調(diào)用它,抑或這個函數(shù)可能屬于第三個類,而另外兩個類應(yīng)該引用某個類。必須決定這個函數(shù)放在哪兒最合適,并確保它被安置后就不會再在其他任何地方出現(xiàn)。
3.2 Long Method(過長函數(shù))
擁有短函數(shù)的對象會活得比較好、比較長。
“間接層”所能帶來的全部利益——解釋能力、共享能力、選擇能力——都是由小型函數(shù)支持的。
程序愈長愈難理解。
早期的編程語言中,子程序調(diào)用需要額外開銷,這使得人不太樂意使用小函數(shù)。
現(xiàn)在的OO語言進(jìn)程內(nèi)的函數(shù)調(diào)用開銷可以忽略不計。
讓小函數(shù)容易理解的真正關(guān)鍵在于一個好名字。
你應(yīng)該更積極地分解函數(shù)。
我們遵循這樣一條原則:如果感覺需要以注解來說明點什么的時候,我們就把需要說明的東西寫進(jìn)一個獨立函數(shù)中,并以其用途(而非實現(xiàn)手法)命名。
我們可以對一組甚至一行代碼做這件事,哪怕替換后的函數(shù)調(diào)用動作比函數(shù)自身還長,只要函數(shù)名稱能夠解釋其用途,我們也該毫不猶豫地這么做。
關(guān)鍵不在于函數(shù)的長度,而在于函數(shù)“做什么”和“如何做”之間的語義距離。(方法調(diào)用告訴我們這個函數(shù)做了什么;直接把代碼寫在函數(shù)內(nèi),是通過代碼的分析我們才能得知這些代碼做了什么;所以如果可以永遠(yuǎn)都只告訴別人我做了什么,而不是我怎么一步一步做的。)
找到函數(shù)中適合集中在一起的部分,將它們提煉出來形成一個新函數(shù)。(Extract Method)
函數(shù)內(nèi)有大量的參數(shù)和臨時變量——>運用Extract Method,最終就會把許多參數(shù)和臨時變量當(dāng)做參數(shù),傳遞給被提煉出來的新函數(shù),導(dǎo)致可讀性幾乎沒有任何提升?!?gt;Replace Temp with Query來消除這些臨時元素。Introduce Parameter Object和Perserve Whole Object則可以將過長的參數(shù)列變得更簡潔一些。->Replace Method with Method Object。
如何確定該提煉哪一段代碼呢?尋找注釋。就算只有一行代碼,如果它需要以注釋來說明,那也值得將它提煉到獨立函數(shù)去。
條件表達(dá)式和循環(huán)常常也是提煉的信號。可以使用Decompose Conditional處理條件表達(dá)式。循環(huán):將循環(huán)和其內(nèi)的代碼提煉到一個獨立函數(shù)中。
3.3 Large Class(過大的類)
Extract Class將幾個變量一起提煉到新類內(nèi)。提煉時應(yīng)該選擇類內(nèi)彼此相關(guān)的變量,將它們放在一起。
通常如果類內(nèi)的數(shù)個變量有著相同的前綴或字尾,這就意味有機會把它們提煉到某個組件內(nèi)。如果這個組件適合作為一個子類->Extract Subclass。
有時候類并非在所有時刻都使用所有實例變量。->Extract Class或Extract Subclass。
一個類如果擁有太多代碼->Extract Class和Extract Subclass.
先確定客戶端如何使用它們,然后運用Extract Interface為每一種使用方式提煉出一個接口??梢詭椭憧辞宄绾畏纸膺@個類。
Large Class是個GUI類->Duplicate Observed Data
3.4 Long Parameter List(過長參數(shù)列表)
太長的參數(shù)列難以理解,太多參數(shù)會造成前后不一致、不易使用,而且一旦你需要更多數(shù)據(jù),就不得不修改它。如果將對象傳遞給函數(shù),大多數(shù)修改都將沒有必要,因為只需(在函數(shù)內(nèi))增加一兩條請求,就能得到更多數(shù)據(jù)。
如果向已有的對象(函數(shù)所屬類內(nèi)的一個字段或另一個參數(shù))發(fā)出一條請求就可以取代一個參數(shù)->Replace Parameter with Method.
Preserve Whole Object將來自同一對象的一堆數(shù)據(jù)收集起來,并以該對象替換它們。
如果某些數(shù)據(jù)缺乏合理的對象歸屬->Introduce Parameter Object為它們制造出一個“參數(shù)對象”。
如果不希望造成“被調(diào)用對象”與“較大對象”間的某種依賴關(guān)系,可以將數(shù)據(jù)從對象中拆解出來單獨作為參數(shù)。
3.5 Divergent Change(發(fā)散式變化)(一個類只能有一個導(dǎo)致它變化的理由,單一職責(zé)) 一個類受多種變化的影響
軟件必須能夠更容易被修改。
一旦需要修改,最好是只需要修改一個地方甚至是不需要修改,只需要添加一些類和做一些配置。
某個類經(jīng)常因為不同的原因在不同的方向上發(fā)生變化。
針對某一外界變化的所有相應(yīng)修改,都只應(yīng)該發(fā)生在單一類中。
應(yīng)該找到某特定原因而造成的所有變化,然后運用Extract Class將它們提煉到另一個類中。
3.6 Shotgun Surgery(霰彈式修改)一種變化引發(fā)多個類相應(yīng)修改
如果每遇到某種變化,就必須在許多不同的類內(nèi)做出許多小修改。
Move Method和Move Field把所有需要修改的代碼放進(jìn)一個類。如果眼下沒有合適的類可以放置這些代碼,就創(chuàng)造一個。
Inline Class把一系列相關(guān)行為放進(jìn)同一個類。
“外界變化”與“需要修改的類”最好一一對應(yīng)。
最根本的原則:將總是一起變化的東西放在一塊兒。數(shù)據(jù)和引用這些數(shù)據(jù)的行為總是一起變化的,但也有例外。如果例外出現(xiàn),我們就搬移哪些行為,保持變化只在一地發(fā)生。
3.7 Feature Envy(依戀情結(jié))
函數(shù)對某個類的興趣高于對自己所處類的興趣。(數(shù)據(jù))
使用Move Method把它移到它該去的地方。
如果函數(shù)中只有一部分有這種情況,先使用Extract Method把這一部分提煉到獨立函數(shù)中,再使用Move Method。
使用到幾個類的功能的函數(shù)該移動到什么位置?判斷哪個類擁有最多被此函數(shù)使用的數(shù)據(jù),然后就把這個函數(shù)和哪些數(shù)據(jù)擺在一起。可以先以Extract Method將這個函數(shù)分解為數(shù)個較小函數(shù)并分別置放于不同地點。
3.8 Data Clumps(數(shù)據(jù)泥團(tuán))
兩個類有相同的三四個字段、許多函數(shù)簽名中有相同的三四個參數(shù)。這些總是綁在一起出現(xiàn)的數(shù)據(jù)真應(yīng)該擁有屬于它們自己的對象。
首先找出這些數(shù)據(jù)以字段形式出現(xiàn)的地方,運用Extract Class將它們提煉到一個獨立對象中。然后將注意力轉(zhuǎn)移到函數(shù)簽名上,運用Introduce Parameter Object或Preserve Whole Object為它減肥。這么做的好處是可以將很多參數(shù)列縮短,簡化函數(shù)調(diào)用。
不必在意Data Clumps只用上新對象的一部分字段,只要以新對象取代兩個以上的字段就可以了。
得到新對象后,可以著手尋找Feature Envy,這可以幫你獲知哪些能夠移至新類中的種種程序行為。
一個好的評判辦法是:刪除眾多數(shù)據(jù)中的一項。這么做,其他數(shù)據(jù)有沒有因而失去意義?如果它們不再有意義,這就是個明確信號:你應(yīng)該為它們產(chǎn)生一個新對象。
3.9 Primitive Obsession(基本類型偏執(zhí))
對象的一個極大的價值在于:它們模糊(甚至打破)了橫亙于基本數(shù)據(jù)和體積較大的類之間的界限??梢暂p松編寫出一些與語言內(nèi)置(基本)類型無異的小型類。
Replace Data Value with Object將原本單獨存在的數(shù)據(jù)值替換為對象。
如果想要替換的數(shù)據(jù)項是類型碼,而它并不影響行為,可以運用Replace Type Code with Class。
如果有與類型碼相關(guān)的條件表達(dá)式,可運用Replace Type Code with Subclass或Replace Type Code with State/Startegy加以處理。
有一組總是被放在一起的字段,運用Extract Class。
在參數(shù)列表中看到基本類型數(shù)據(jù),運用Introduce Parameter Object。
在數(shù)組中挑選數(shù)據(jù),運用Replace Array with Object。
3.10 Switch Statements(switch驚悚現(xiàn)身)
面向?qū)ο蟪绦虻囊粋€最明顯特征就是:少用switch(或case)語句。從本質(zhì)上說,switch語句的問題在于重復(fù)。
你常會發(fā)現(xiàn)同樣的switch語句散布于不同地點。如果要為switch添加一個新的case字句,就必須找到所有switch語句并修改它們。
一看到switch語句,就應(yīng)該考慮以多態(tài)來替換它。
switch語句常常根據(jù)類型碼進(jìn)行選擇,你要的是“與該類型碼相關(guān)的函數(shù)或類”,所以應(yīng)該使用Extract Method將switch語句提煉到一個獨立函數(shù)中,再以Move Method將它搬移到需要多態(tài)性的那個類里。此時你必須決定是否使用Replace Type Code with Subclasses或Replace Type Code with State/Strategy。一旦這樣完成繼承結(jié)構(gòu)之后,你就可以運用Replace Conditional with Polymorhpism了。
如果你只是在單一函數(shù)中有些選擇事例,且并不想改動它們,那么多態(tài)就有點殺雞用牛刀了。這種情況下Replace Parameter with Explicit Methods是個不錯的選擇。如果你的選擇條件之一是null,可以試試Introduce Null Object。
3.11 Parallel Inheritance Hierarchies(平行繼承體系)
可不要認(rèn)為設(shè)計模式的橋梁模式會有這種情況。
Parallel Inheritance Hierarchies是Shotgun Surgery的特殊情況。
每當(dāng)你為某個類增加一個子類,同時也必須為另一個類相應(yīng)地增加一個子類。
如果你發(fā)現(xiàn)某個繼承體系的類名稱前綴和另一個繼承體系的類名稱前綴完全相同,便是聞到這種壞味道。
消除這種重復(fù)性的一般策略是:讓一個繼承體系的實例引用另一個繼承體系的實例。然后運用Move Method和Move Field將引用端的繼承體系去掉。
3.12 Lazy Class(冗贅類)
你所創(chuàng)建的每一個類,都得有人去理解它、維護(hù)它,這些工作都是要花錢的。
開發(fā)者事前規(guī)劃了某些變化,并添加一個類來對付這些變化,但變化實際上沒有發(fā)生。
如果某些子類沒有做足夠的工作->Collapse Hierarchy。
對于幾乎沒用的組件->Inline Class。
3.13 Speculative Generality(夸夸其談未來性)
如果某個抽象類其實沒有太大作用->Collapse Hierarchy。
不必要的委托可運用Inline Class除掉。
函數(shù)的某些參數(shù)未被用上->Remove Parameter。
函數(shù)名稱帶有多余的抽象意味->Rename Method,讓它現(xiàn)實一些。
函數(shù)或類的唯一用戶是測試用例,把它們連同其測試用例一并刪掉。但如果它們的用途是幫助測試用例檢測正當(dāng)功能,則必須刀下留人。
3.14 Temporary Field(令人迷惑的臨時字段)
某個類內(nèi)某個實例變量僅為某種特殊情況而設(shè)。這樣的代碼讓人不易理解,因為通常認(rèn)為類在所有時候都需要它的所有變量。在變量未被使用的情況下猜測當(dāng)初其設(shè)置目的,會讓人發(fā)瘋的。
使用Extract Class把臨時字段移到新類中,然后把所有和這個變量相關(guān)的代碼都放進(jìn)這個新類。
使用Introduce Null Object在“變量不合法”的情況下創(chuàng)建一個Null對象,從而避免寫出條件式代碼。
如果類中有一個復(fù)雜算法,需要好幾個變量,往往就可能導(dǎo)致壞味道Temporary Field的出現(xiàn)。由于實現(xiàn)者不希望傳遞一長串參數(shù),所以他把這些參數(shù)都放進(jìn)字段中。但是這些字段只有在使用該算法時才有效,其他情況下只會讓人迷惑。->Extract Class把這些變量和其相關(guān)函數(shù)提煉到一個獨立類中。提煉后的新對象將是一個函數(shù)對象。
3.15 Message Chains(過度耦合的消息鏈)
消息鏈:用戶向一個對象請求另一個對象,然后再向后者請求另一個對象,然后再請求另一個對象....
采取這種方式,意味客戶代碼將與查找過程中的導(dǎo)航結(jié)構(gòu)緊密耦合。一旦對象間的關(guān)系發(fā)生任何變化,客戶端就不得不做出相應(yīng)修改。
Hide Delegate->可以在消息鏈的不同位置進(jìn)行這種重構(gòu)手法。理論上可以重構(gòu)消息鏈上的任何一個對象,但這么做往往會把一系列對象都變成Middle Man。通常更好的選擇是:先觀察消息鏈最終得到的對象是用來干什么的,看看能夠以Extract Method把使用該對象的代碼提煉到一個獨立函數(shù)中,再運用Move Method把這個函數(shù)推入消息鏈。如果這條鏈上的某個對象有多位客戶打算導(dǎo)航此航線的剩余部分,就加一個函數(shù)來做這件事。
3.16 Middle Man(中間人)
封裝往往伴隨委托。
過度運用委托:某個類接口有一半的函數(shù)都委托給其他類。
Remove Middle Man——>直接和真正負(fù)責(zé)的對象打交道。
如果這樣“不干實事”的函數(shù)只有少數(shù)幾個,可以運用Inline Method把它們放進(jìn)調(diào)用端。
如果這些Middle Man還有其他行為->Replace Delegation with Inheritance把它變成實責(zé)對象的子類,這樣既可以擴展原對象的行為,又不必負(fù)擔(dān)那么多的委托動作。
3.17 Inappropriate Intimacy(狎昵關(guān)系)
兩個類過于親密,花費太多時間去探究彼此的private成分。
Move Method和Move Field幫它們劃清界限,從而減少狎昵行徑。
Change Bidirectional Association to Unidirectional讓其中一個類對另一個類斬斷關(guān)系。
如果兩個類實在是情投意合,運用Extract Class把兩者共同點提煉到一個安全地點,讓它們使用這個新類?;蜻\用Hide Delegate讓另一個類來為它們傳遞關(guān)系。
繼承往往造成過度親密,因為子類對超類的了解總是超過后者的主觀愿望。->Replace Inheritance with Delegation讓子類離開繼承體系。
3.18 Alternative Classes with Different Interfaces(異曲同工的類)
如果兩個函數(shù)做同一件事,卻有著不同的簽名->Rename Method根據(jù)它們的用途重新命名。->反復(fù)運用Move Method將某些行為移入類,直到兩者的協(xié)議一致為止。如果你必須重復(fù)而贅余地移入代碼才能完成這些,或許可以運用Extract Superclass。
3.19 Incomplete Library Class(不完美的類庫)
大多數(shù)對象只要夠用就好。
如果類庫構(gòu)造得不夠好,我們不可能修改其中的類使它完成我們希望完成的工作。
只想修改類庫中的類的一兩個函數(shù)->Introduce Foreign Method
添加一大堆額外行為->Introduce Local Extension
3.20 Data Class(幼稚的數(shù)據(jù)類)
Data Class:擁有一些字段,以及用于訪問(讀寫)這些字段的函數(shù)。
如果擁有public字段->Encapsulate Field
如果這些類內(nèi)含容器類的字段,應(yīng)該檢查它們是不是得到了恰當(dāng)?shù)胤庋b->Encapsulate Collection
對于不該被其他類修改的字段->Remove Setting Method->找出取值/設(shè)置函數(shù)被其他類運用的地點->Move Method把這些調(diào)用行為搬移到Data Class來。如果無法搬移整個函數(shù),就運用Extract Method產(chǎn)生一個可被搬移的函數(shù)->Hide Method把這些取值/設(shè)置函數(shù)隱藏起來。
3.21 Refused Bequest(被拒絕的遺贈)
子類應(yīng)該繼承超類的函數(shù)和數(shù)據(jù)。子類繼承得到所有函數(shù)和數(shù)據(jù),卻只使用了幾個。
繼承體系設(shè)計錯誤。需要為這個子類新建一個兄弟類->Push Down Method和Push Down Field把所有用不到的函數(shù)下推給兄弟類,這樣一來,超類就只持有所有子類共享的東西。
所有超類都應(yīng)該是抽象的。
拒絕繼承超類的實現(xiàn),可以不用介意;拒絕繼承超類的接口,可以不以為然。但是不能胡亂修改繼承體系->Replace Inheritance with Delegation.
3.22 Comments(過多的注釋)
因為代碼很糟糕所以寫了很多注釋。
注釋提示我們代碼有壞味道,重構(gòu)完會發(fā)現(xiàn)注釋變得多余了。
需要注釋來解釋一塊代碼做了什么->Extract Method
函數(shù)已經(jīng)提煉出來,但還是需要注釋解釋其行為->Rename Method
需要注釋說明某些系統(tǒng)的需求規(guī)格->Introduce Assertion
當(dāng)你感覺需要撰寫注釋時,請先嘗試重構(gòu),試著讓所有注釋都變得多余。
如果你不知道該做什么,這才是注釋的良好運用時機。除了用來記述將來的打算之外,注釋還可以用來標(biāo)記你并無十足把握的區(qū)域。你可以在注釋里寫下自己“為什么做某某事”。這類信息可以幫助將來的修改者,尤其是哪些健忘的家伙。