1 Duplicated Code重復代碼
不同的地方出現相同的程序結構:
如果你在一個以上的地點看到相同的程序結構,那么可以肯定:設法將它們和而為一,程序會變得更好。最常見的“重復代碼”就是一個類內的兩個函數含有相同的表達式。另一種常見情況就是兩個互為兄弟的子類內含有相同的表達式。
1)同一個類的2個函數含有相同的表達式,這時可以采用Extract Method(提煉函數)提煉出重復的代碼,然后讓這2個地點都調用被提煉出來的那段代碼。
2)兩個互為兄弟的子類內含相同表達式,只需對2個類都是用Extract Method(提煉函數),然后對被提煉出來的函數是用Pull Up Method (方法上移) ,將它推入超類。如果代碼之間只是類似, 并非完全相同,那么就得運用Extract Method(提煉函數 將相似部分和差異部分隔開,構成單獨一個的函數。然后你可能發現可以運用Form Template Method (塑造模板函數)獲得一個 Template Method設計模式。如果有些函數以不同的算法做相同的事,你可以選擇其中較清晰地一個,并是用 Substitute Algorithm (替換算法)將其他函數的算法替換掉。
如果2個毫不相關的類出現 重復代碼,你應該考慮對其中一個運用 Extract Class (提煉類),將重復代碼提煉到一個獨立類中,然后在另一個類內使用這個新類。但是,重復代碼所在的函數可能只應該屬于某個類,另一個類只能調用它,抑或這個函數可能屬于第三個類,而另2個類應該引用這第三個類。你必須決定這個函數放在哪兒最合適,并確保它被安置后就不會再在其他任何地方出現。
2 Long Method 過長函數
函數中的代碼行數原則上不要多于100行:
我們遵循這樣一條原則:每當感覺需要以注釋開說明點什么的時候,我們就需要把說明的東西寫進一個獨立的函數中,并以其用途(而非實現手法)命名。
90%的場合里,要把函數變小,只需使用Extract Method(提煉函數) ,找到函數中適合集中在一起的部分,將它們提煉出來形成新函數。
如果函數內有大量的參數和臨時變量,它們會對你的函數提煉形成障礙。你可以經常運用Replace Temp with Query (以查詢取代臨時變量),來消除這些臨時元素。Introduce Parameter Object (引入參數對象),Preserve Whole Object (保持對象完整)則可以將過長的參數列變得簡潔一些。
如果已經這么做了,仍然有太多的臨時變量和參數,就應該使用 Replace Method with Method Object (以函數對象取代函數)。如何確定提煉哪一段代碼呢?一個很好的技巧是:尋找注釋。它們通常指出代碼用途和實現手法之間的語義距離。如果代碼前方有一行注釋,就是在提醒你:可以將這段代碼替換成一個函數,而且可以在注釋的基礎上給這個函數命名。就算只有一行代碼,如果它需要以注釋來說明,那也值得將它提煉到獨立函數中。
條件表達式和循環常常也是提煉的信號??梢允褂?nbsp;Decompose Conditional (分解條件表達式)處理條件表達式。至于循環,應該將循環和其內的代碼提煉到獨立函數中。(間接層所帶來的全部利益—解釋能力,共享能力,選擇能力–都是由小型函數支持)
3 Large Class 過大的類
類不要負責超越本類的職責,即前面提到的單一原則:
如果想利用單個類做太多的事,其內往往就會出現太多實例變量。這樣 重復代碼(Duplicated Code)就接踵而至了。
運用Extract Class (提煉類)將幾個變量一起提煉到新類里。提煉類時應該選擇類內彼此相關的變量,將它們放在一起。通常類內的數個變量有相同的前綴或字尾,這就意味有機會把它們提煉到某個組件內。如果這個組件適合作為一個子類,就可以運用 Extract Subclass (提煉子類)。
有時類并非在所有時刻都使用所有實例變量??啥啻问褂?a target="_blank">Extract Class (提煉類)和Extract Subclass (提煉子類)
一個類如果擁有太多代碼,往往也適合使用Extract Class (提煉類)和Extract Subclass (提煉子類)。技巧:先確定客戶端如何使用它們,然后運用 Extract Interface(提煉接口) 為每個使用方式提煉出一個接口??蓭椭憧辞宄绾畏纸膺@個類。
如果是個GUI類,可能需要把數據和行為移到一個獨立的領域對象去??赡苄枰?邊各保留一些重復數據,并保持2邊同步。Duplicate Observed Data (復制"被監視數據")告訴你該這么做。
4. Long Parameter List 過長參數列
過長參數不易理解和維護:
太長的參宿列難以理解,而且會造成前后不一致,不易使用。而且一旦你需要更多數據,就不得不修改它。如果將對象傳遞給函數,大多數修改都沒有必要。例外:不希望造成被調用對象與較大對象間的某種依賴關系。
如果向已有的對象發出1條請求就可以取代1個參數,那么就運用 Replace Parameter with Methods (以函數取代參數)。在這里,"已有的對象"可能是函數所屬類里的1個字段,也可能是另一個參數。還可以運用 Preserve Whole Object (保持對象完整)將來自同一對象的一堆數據收集起來,并以該對象替換它們。如果某些數據缺乏合理的對象歸屬,可使用 Introduce Parameter Object (引入參數對象)為它們制造出一個參數對象。
重要的例外:如果你明顯不想造成"被調用對象"與"較大對象"間的某種依賴關系。這時候將數據從對象中拆解出來單獨作為參數,也很合情理。但是請注意其所引發的代價。如果參數列太長或變化太頻繁,就需要重新考慮自己的依賴結構了。
5. Divergent Change 發散式變化(相對聚焦式)
修改一個地方需要需要很多處代碼.因為修改的代碼散布四處:
如果在系統需要修改的時候不能做到只在某一點出做修改,應該意識到代碼是否過于緊密耦合的味道了:
例如男人是 “聚焦式認知” 女人是“發散式認知”.簡單點說,聚焦式就是把大面凝聚成一個點,發散式相反它是把一個點擴展成大面
男人可以把幾個問題的共性找出來,再再想辦法加以解決。男人以解決問題為目的;女人可以把一個問題分拆出好幾個方向去訴說,永遠不想解決的操作辦法。女人以訴說問題為目的。
如果某個類經常因為不同的原因在不同的方向上發生變化,發散式變化(Divergent Change)就出現了。那么此時也許將這個對象分成2個會更好,這么一來每個對象就可以只因1種變化而需要修改。針對某以外界變化的所有相應修改,都只應該發生在單一類中,而這個新類內的所有內容都應該反應此變化。為此應該找出某特定原因而造成的所有變化,然后運用Extract Class (提煉類)將它們提煉到另一個類中。
6. Shotgun Surgery 霰彈式修改
僅做一個簡單的修改卻要求改變多個類。即我們需要修改的代碼散布四處:
Shotgun Surgery 霰彈式修改 (僅做一個簡單的修改卻要求改變多個類。即我們需要修改的代碼散布四處,不但很難找到它們,也很容易忘掉某個重要的修改。 )將多個類相分離是代碼的一大職責。有可能缺少一個通曉全盤職責的類 (而大量修改本應針對這個類完成)。另外,也有可能因過度去除發散式改變而招致這個壞味道。
找出一個應對這些修改負責的類。這可能是一個現有的類,也可能需要通過應用抽取類來創建一個新的類。使用搬移字段(Move Field)和搬移方法(Move Method),將功能置于所選的類中。如果未選中類足夠簡單,則可以使用內聯類(Inline Class)將該類去除。
7. Feature Envy 依戀情節
類的方法應該去該去的地方:
函數對某個類的興趣高過對自己所處的類,通常的焦點就是數據,某個函數為了計算某個值,從另一個對象那兒調用幾乎半打的取值函數。這時一個運用 Move Method (搬移函數)把它移到自己該去的地方。有時候函數中只有一部分受這種依戀之苦,這時候使用Extract Method (提煉函數)把這部分提煉到獨立函數中,再使用Move Method (搬移函數)帶它去它的夢中家園。
一個函數往往會用到幾個類的功能,那么它究竟該被置于何處呢?我們的原則是:判斷哪個類擁有最多被此函數使用的數據,然后就把這個函數和那些數據擺在一起。
8. Data Clumps 數據泥團
眾多數據項待在一塊:
常??梢栽诤芏嗟胤娇吹较嗤?、4項數據:2個類中相同的字段、許多函數簽名中相同的參數。這些總是綁在一起出現的數據真應該擁有屬于它們自己的對象。首先找出這些數據以字段形式出現的地方,運用Extract Class (提煉類)將它們提煉到一個獨立對象中。然后將注意力轉移到函數簽名上,運用Introduce Parameter Object (引入參數對象)或Preserve Whole Object (保持對象完整)為它減肥。這么做的直接好處是可以將很多參數列縮短,簡化函數調用。不必在意數據泥團(Data Clumps)只用上新對象的一部分字段,只要以新對象取代2(或更多)個字段,就值得了。
一個好的評判方法是:刪除眾多數據中的一項。這么做,其他數據有沒有因而失去意義?如果它們不再有意義,這就是個明確信號:你應該為它們產生一個新對象。
9. Primitive Obsession 基本類型偏執
喜歡使用基本類型,而不愿運用小對象:
對象的一個極具價值的東西是:它們模糊(甚至打破)了橫亙于基本數據和體積較大的類之間的界限。你可以輕松的編寫出一些與語言內置(基本)類型無異的小型類。
對象技術的新手通常不愿意在小任務上運用小對象—像是結合數值和幣種的money類,由一個起始值和結束值組成的range類等。你可以使用 Replace Data Value withObject (以對象取代數據值)將原本單獨存在的數據值替換為對象。如果想要替換的數據值是類型碼,而它并不影響行為,則可以運用 Replace Type Code with Class (以類取代類型碼)將它替換掉。如果你有與類型碼相關的條件表達式,可運用 Replace Type Code with Subclass (以子類取代類型碼)或 Replace Type Code with State/Strategy
(以狀態/策略取代類型碼)加以處理。
如果你有一組應該總是被放在一起的字段,可運用Extract Class (提煉類)。如果你在參數列中看到基本數據類型,不妨試試 Introduce Parameter Object (引入參數對象)。如果你發現自己正從數組中挑選數據,可運用 Replace Array with Object(以對象取代數組).
10. Switch Statement (switch 驚悚現身)
switch語句的問題在于重復:
面向對象程序的一個最明顯特征就是:少用switch或(case)語句。從本質上說,switch語句的問題在于重復。你常會發現switch語句散布于不同地點。如果要為它添加一個新的case子句,就必須找到所有switch語句并修改它們。面向對象中的多態概念可為此帶來優雅的解決辦法。
大多數時候,一看到switch語句,就應該考慮以多態來替換它。問題是多態該出現在哪?switch語句常常根據類型碼進行選擇,你要的是“與該類型碼相關的函數或類”,所以應該使用 Extract Method (提煉函數)將switch語句提煉到一個獨立函數中,再以 Move Method (搬移函數)將它搬移到需要多態性的那個類里。此時你必須決定是否使用Replace Type Code with Subclass (以子類取代類型碼)或 Replace Type Code with State/Strategy (以狀態/策略取代類型碼)。一旦完成這樣繼承結構后,就可以運用
Replace Conditional with Polymorphism (以多態取代條件表達式)了。
如果你只是在單一函數中有些選擇事例,且并不想改動它們,那么多態就不必要了。這時可運用 Replace Parameter with Explicit Methods (以明確函數取代參數)。如果你的選擇條件之一是null,可以試試 Introduce Null Object (引入Null 對象).
11. Parallel Inheritance Hierarchies 平衡繼承體系
平行繼承體系其實是散彈式修改(Shotgun Surgery)的特殊情況:
在這種情況下,每當你為某個類增加1個子類,必須也為另一個類相應增加1個子類。如果你發現某個繼承體系的類名前綴和另一個繼承體系的類名前綴完全相同,便是聞到了這種壞味道。
消除這種重復性的一般策略是:讓一個繼承體系的實例引用另一個繼承體系的實例。如果再接再厲運用 Move Method (搬移函數)和Move Field (搬移字段),,就可以將引用端的繼承體系取消。
12. Lazy Class(冗贅類)
如果一個類的所得不值其身價,它就應該消失:
即如果某些子類沒有足夠的工作,試試Collapse Hierarch (折疊繼承體系)。對應幾乎沒用的組件,應該以Inline Class (將類內聯化)對付它們。
13. Speculative Generality(夸夸其談未來性)
如果用不到的類就不要用:
如果你的某個抽象類其實沒有太大作用,請運用 Collapse Hierarch (折疊繼承體系)。不必要的委托可運用 Inline Class (將類內聯化)除掉。如果函數的某些參數未被用上,可對它實施 Remove Parameter (移除參數)。如果函數名稱帶有多余的抽象意味,應該對它實施Rename Method (函數改名)
如果函數或類的唯一用戶是測試用例,這就飄出了壞味道 夸夸其談未來性(Speculative Generality)。 如果有這樣的函數或類,請把它們連同其測試用例一并刪除。但如果它們的用途是幫助測試用例檢測正當功能,則不能刪除。
14. Temporary Field(令人迷惑的暫時值域)
對象的暫時性屬性經常讓人迷惑:
有時你會看到這樣的對象:其內某個實例變量僅為某種特定情況而設。這樣的代碼讓人不易理解,因為你通常認為對象在所有時候都需要它的所有變量。在變量未被使用的情況下猜測當初設置目的,會讓你發瘋。
請使用Extract Class (提煉類)給這些變量創造一個家,然后把所有和這些變量相關的代碼都放進這個新家,也許你還可以使用 Introduce Null Object (引入Null 對象)在變量不合法的情況下創建一個null對象,從而避免寫出條件式代碼。
如果類中有一個復雜算法,需要好幾個變量,往往就可能導致壞味道令人迷惑的臨時字段(Temporary Field)出現。由于實現者不希望傳遞一長串參數,所以他把這些參數都放進字段。但是這些字段只在使用該算法時才有效,其他情況下只會讓人迷惑。這時可以利用 Extract Class (提煉類)把這些變量和其相關函數提煉到一個獨立的類中。提煉后的新對象將是一個函數對象。
15. Message Chains(過度耦合的消息鏈)
A對象請求B對象,B對象請求C對象…:
如果你看到用戶向一個對象請求另一個對象,然后再向后者請求另一個對象,然后再請求另一個對象……這就是消息鏈。實際代碼中你看到的可能是一長串getThis()或一長串臨時變量。采取這種方式,意味客戶代碼將與查找過程中的導航緊密耦合。一旦對象間關系發生任何變化,客戶端就不得不做出相應的修改。
這時候應該使用 Hide Delegate (隱藏委托關系)。你可以在消息鏈的不同位置進行這種重構。理論上可以重構消息鏈上任何對象,但這么做往往會把一系列對象都變成Middle Man(中間人)。通常更好的選擇是:先觀察消息鏈最終得到的對象是用來干什么的,看看能否以 Extract Method (提煉函數)把使用該對象的代碼提煉到一個獨立函數中,再運用 Move Method (搬移函數)把這個函數推入消息鏈。
16. Middle Man(中間轉手人)
過度運用委托:
對象的基本特征之一就是封裝:對外部世界隱藏其內部細節。封裝往往伴隨委托。比如說你問你主管是否有時間參加一個會議,他就把這個消息“委托”給他的記事簿,然后才能回答你。你沒必要知道這位主管到底使用傳統記事簿或電子記事簿或秘書來記錄自己的約會。
但是人們可能過度運用委托。你也許會看到某個類有一半的函數都委托給其他類,這樣就是過度運用。這時應該使用Remove Middle Man (移除中間人),直接和真正負責的對象打交道。如果不干實事的函數只有少數幾個,可以運用 Inline Method (內聯函數)把它們放進調用端。如果這些中間人還有其他行為,可以運用 Replace Delegation with Inheritance (以繼承取代委托)把它們變成實責對象的子類,這樣你即可以擴展原對象的行為,又不必負擔那么多的委托動作。
17. Inappropriate Intimacy(狎昵關系)
2個類過于親密:
有時候你會看到2個類過于親密,花費太多時間起探究彼此的private成分。你可以采用Move Method (搬移函數)和 Move Field (搬移字段)幫他們劃清界限。你也可以看看是否可以運用 Change Bidirectional Association to Unidirectional (將雙向關聯改為單向關聯)讓其中一個類對另一個斬斷情絲。如果2個類實在是情投意合,可以運用Extract Class (提煉類)把2者共同點提煉到一個安全地點,讓它們坦蕩的使用這個新類?;蛘呖梢試L試運用 Hide Delegate (隱藏委托關系)讓另一個類來為它們傳遞相思情。
繼承往往造成過度親密,因為子類對超類的了解總是超過后者的主觀愿望,如果你覺得該讓這個孩子獨自生活了,請運用Replace Inheritance with Delegation (以委托取代繼承)讓它離開繼承關系。
18. Alternative Classes with Different Interfaces(異曲同工的類)
兩個函數做同一件事:
如果兩個函數做同一件事,卻有著不同的簽名,請運用 Rename Method (函數改名)根據它們的用途重新命名。但這往往不夠,請反復運用 Move Method (搬移函數)將某些行為移入類,直到2者的協議一致為止。如果你必須反復而贅余的移入代碼才能完成這些,或許可運用Extract Superclass (提煉超類)。
19. Incomplete Library Class(不完善的庫類)
復用常被視為對象的終極目的。不過復用的意義常被高估:大多數對象只要夠用就好。但是無可否認,許多編程技術都建立在程序庫的基礎上。庫類的構筑者沒用未卜先知的能力,不能因此責怪他們。麻煩的是庫往往構造的不夠好,而且往往不可能讓我們修改其中的類使它們完成我們希望完成的工作。這是否意味著那些經過實踐檢驗的技術,如今都派不上用場了?
幸好我們有2個專門應付這種情況的工具。如果你只想修改類庫的一兩個函數,可以運用Introduce Foreign Method (引入外加函數);如果想要添加一大堆額外行為,就得運用 Introduce Local Extension (引入本地擴展) 。
20. Data Class(純稚的數據類)
所謂的Data Class是指:它們擁有一些字段,以及用于訪問這些字段的函數,除此之外一無長物。這樣的類只是不會說話的數據容器,它們幾乎一定被其他類過分細瑣的操控著。這些類早期可能擁有public字段,果真如此你應該在別人注意到它們之前,立刻運用 Encapsulated Field (封裝字段)將它們封裝起來。如果這些類含容器類的字段,你應該檢查它們是不是得到了恰當的封裝;如果沒有,就運用 Encapsulated Collection (封裝集合)把它們封裝起來。對于那些不該被其他類修改的字段,請運用 Remove Setting Method (移除設置函數)。
然后,找出這些取值/設值函數被其他類運用的地點。嘗試以 Move Method (搬移函數)把那些調用行為搬移到Data Class來。如果無法搬移這個函數,就運用 Extract Method (提煉函數)產生一個可搬移的函數。不久之后就可以運用 Hide Method (隱藏函數)把這些取值/設值函數隱藏起來了。
21 Refused Bequest(被拒絕的遺贈)
子類應該繼承超類的函數和數據。但如果它們不想或不需要繼承,又該這么辦呢?它們得到所有禮物,卻只從中挑選幾樣來玩。
按傳統說法,這就意味著繼承體系設計誤。你需要為這個子類新建一個兄弟類,再次運用 push down Method (函數下移)和 push down field (字段下移)把所有用不到的函數下推給那個兄弟。這樣一來,超類就只持有所有子類共享的東西。你常常會聽到這樣的建議:所有超類都應該是抽象的。既然使用“傳統說法”這個略帶貶義的詞,你就可以猜到,我們不建議你這么做,起碼不建議你每次都這么做。我們繼承利用繼承來復用一些行為,并發現可以很好的應用于日常工作。這也是一種壞味道,我們不否認,但氣味通常并不強烈。所以我們說:如果Refused Bequest引起困惑和問題,請遵循傳統忠告。
但不必認為你每次都得那么做。十有八九這種壞味道很淡,不值得理睬。如果子類復用了超類的行為,卻有不愿支持超類的接口,Refused Bequest的壞味道就會變得濃烈。拒絕繼承超類的實現, 這點我們不介意;但如果拒絕繼承超類的接口,我們不以為然。不過即使你不愿意繼承接口,也不要胡亂修改繼承體系,應用運用 Replace Inheritance with Delegation (以委托取代繼承)來達到目的。
22 Comments(過多的注釋)
我們之所以在這里提到注釋,是因為人們常把它當做除臭劑來使用。常常會有這樣的情況:你看到一段代碼有著長長的注釋,然后發現,這些注釋之所以存在是因為代碼很糟糕。注釋可以帶我們找到代碼中的壞味道。找到壞味道后,我們首先應該以各種重構手法把壞味道去除。完成之后我們常常會發現:注釋已經變得多余了,因為代碼已經清楚說明了一切。
如果你需要注釋來解釋一塊代碼做了說明,試試Extract Method (提煉函數);如果函數已經提煉出來,但還是需要注釋來解釋其行為,試試 Rename Method (函數改名);如果你需要注釋說明某些系統的需求規格,試試 Introduce Assertion (引入斷言)。
轉自:http://blog.csdn.net/hguisu/article/details/7591280
原創文章,作者:s19930811,如若轉載,請注明出處:http://www.www58058.com/2819