一. Duplicated Code(重復代碼)
- 如果你在一個以上的地點看到相同的程序結構,設法將他們合而為一,程序會變得更好。
- 同一個類的兩個函數含有相同的表達式,采用
Extract Method
(提煉函數)提煉出重復的代碼。 - 兩個互為兄弟的子類含有相同的表達式,首先對兩個類都使用
Extract Method
(提煉函數),然后再對提煉出來的代碼使用Pull Up Method
(函數上移),將它推入超類。 - 如果代碼之間只是類似,并非完全相同,運用
Extract Method
(提煉函數)將相似部分和差異部分分割開,然后可以運用Form Template Method
(塑造模板函數)獲取一個莫模板方法。 - 如果有些函數以不同算法做相同的事情,你可與選擇其中較清晰的一個,使用
Substitute Algorithm
(替換算法)將其他函數的算法替換掉。 - 如果兩個毫不相關的類出現重復代碼,應考慮對其中一個使用
Extract Class
(提煉類),將重復代碼提煉到一個獨立的類中。 - 重復代碼所在的函數應該只屬于某一個類,另一個類調用它;或者應該屬于第三個類,另兩個類引用這第三個類。決定重復函數的最合適位置,確保只有一份。
二. Long Method(過長函數)
- 擁有短函數的對象會活的比較好、比較長。
- 絕大部分情況下,要把函數變小,只需要使用
Extract Method
(提煉函數)。 - 使用
Extract Method
(提煉函數)時,如果函數中有個別參數和臨時變量,可以把他們當做參數,傳遞給被提煉出來的新函數。 - 如果被提煉函數內有大量的參數和臨時變量,可以運用
Replace Temp with Query
(以查詢取代臨時變量)來消除這些臨時元素。 - 使用
Introduce Parameter Object
(引入參數對象)可以將過長的參數列變得更簡潔一些。 - 如果被提煉函數仍然有太多臨時變量和參數,可以使用
Replace Method with Method Object
(以函數對象取代函數)。 - 注釋通常能夠指出應該被提煉的代碼。就算只有一行代碼,如果它需要以注釋來說明,那也值得將它提煉到獨立函數去。
- 你可以使用
Decompose Conditional
(分解條件表達式)處理條件表達式。 - 你應該將循環和其內的代碼提煉到一個獨立的函數中。
三. Large Class(過大的類)
- 如果想利用單個類做太多事情,其內往往就會出現太多實例變量。一旦如此,重復代碼也就接踵而至了。
- 可以運用
Extract Class
(提煉類)將幾個彼此相關的實例變量一起提煉至新類內。如果被提煉出的類適合作為一個子類,使用Extract Subclass
(提煉子類)往往比較簡單。 - 有時候類并非所有時刻都使用所有實例變量。你可以多次使用
Extract Class
(提煉類)或Extract Subclass
(提煉子類)。 - 和“太多實例變量”一樣,類內如果有太多代碼,也是代碼重復、混亂并最終走向死亡的源頭。
- 如果有五個“百行函數”,他們之中有很多代碼相同,那么你也許可以把他們變成五個“十行函數”和十個提煉出的“雙行函數”。
- 和“太多實例變量”一樣,類內如果有太多代碼,往往也適合使用
Extract Class
(提煉類)或Extract Subclass
(提煉子類)。 - 如果你的過大的類是一個
GUI
類,你可能需要把數據和行為移到一個獨立的類中。
四. Long Parameter List(過長參數列)
- 太長的參數列難以理解,太多參數會造成前后不一致、不易使用。
- 剛開始學習編程時,老師教我們:把函數所需的所有東西都以參數傳遞進去。這可以理解,因為除此之外只能選擇全局數據,而全局數據是邪惡的東西。
- 對象技術改變了這一情況:如果你手上沒有所需的東西,總可以叫另一個對象給你。有了對象,函數需要的東西多半可以在函數的宿主類中找到。面向對象程序中的函數,其參數列通常比在傳統程序中短的多。
- 如果向已有的對象發出一條請求就可以取代一個參數,那么你應該激活重構手法
Replace Parameter with Method
(以函數取代參數)。 - 你還可以運用
Preserve Whole Object
(保持對象完整)將來自同一對象的一堆數據收集起來,并以該對象替換他們。 - 如果某些數據缺乏合理的對象歸屬,可使用
Introduce Parameter Object
(引入參數對象)為他們制造出一個“參數對象”。 - 這里有一個例外:有時候你明顯不希望造成“被調用對象”與“較大對象”間的某種依賴關系。這時候將數據從對象拆解出來單獨作為參數,也很合情合理。但是請權衡其所引發的代價。
五. Divergent Change(發散式變化)
- 我們希望軟件能夠更容易被修改。一旦需要修改,我們希望能夠跳到系統的某一點,只在該處做修改。
- 如果某個類經常因為不同的原因在不同的方向上發生變化,
Divergent Change
(發散式變化)就出現了。 - 針對某一外界變化的所有相應修改,都只應該發生在單一類中。為此,你應該找出某特定原因而造成的所有變化,運用
Extract Class
(提煉類)將他們提煉到另一個類中。
六. Shotgun Surgery(散彈式修改)
-
Shotgun Surgery
(散彈式修改)類似Divergent Change
(發散式變化),但恰恰相反。 - 如果每遇到某種變化,你都必須在許多不同的類內做出許多小修改,你所面臨的壞味道就是
Shotgun Surgery
(散彈式修改)。 - 如果需要修改的代碼散布四處,你不但很難找到他們,也很容易忘記某個重要的修改。
- 你應該使用
Move Method
(搬移函數)和Move Field
(搬移字段)把所有需要修改的代碼放進同一個類。 - 如果眼下沒有合適的類可以安置這些代碼,就創造一個。通??梢赃\用
Inline Class
(將類內聯化)把一系列相關行為放進同一個類。 -
Divergent Change
(發散式變化) 是指“一個類受多種變化的影響”。Shotgun Surgery
(散彈式修改)則是指“一種變化引起多個類響應修改”。這兩種情況你都希望整理代碼,使“外界變化”與“需要修改的類”趨于一一對應。
七. Feature Envy(依戀情節)
- 對象技術即是一種“將數據和對數據的操作行為包裝在一起”的技術。
-
Feature Envy
(依戀情節)指的是:函數對某個類的興趣高過對自己所處類的興趣。 - 我們常??吹侥硞€函數為了計算某個值,從另一個對象那兒調用了幾乎半打的取值函數。此時,你應該使用
Move Method
(搬移函數)把它移到它該去的地方。 - 先使用
Extract Method
(提煉函數),將這個函數分解為數個較小函數并分別置于不同地點,有助于Move Method
(搬移函數)重構手法的實施。 - 如果一個函數用到幾個類的功能,那么需要判斷哪個類擁有最多被此函數使用的數據,然后就把這個函數和那些數據擺在一起。
八. Data Clumps(數據泥團)
- 數據項就像小孩子,喜歡成群結隊地待在一塊兒。這些總是綁在一起出現的數據真應該擁有屬于它們自己的對象。
- 首先運用
Extract Class
(提煉類)將他們提煉到一個獨立對象中,然后將注意力轉移到函數簽名上,運用Introduce Parameter Object
(引入參數對象)或Preserve Whole Object
(保持對象完整性)為它減肥。這樣做可以縮短參數列,簡化函數調用。 - 如果刪掉眾多數據中的一項,其他數據不再有意義,那么他們應該以一個對象的形式存在。
- 一旦擁有新對象,你就有機會讓程序散發出一種芬芳。可以將適當的程序行為移至新類。不必太久,所有的類都將在他們的小小社會中發揮價值。
九. Primitive Obsession(基類類型偏執)
- 大多數編程環境都有兩種數據:結構類型允許你將數據組織成有意義的形式;基本類型則是構成結構類型的積木塊。
- 對象技術的新手通常不愿意在小任務上運用小對象——像是結合數值和幣種
money
類、由一個起始值和一個結束值組成的range
類、電話號碼或郵政編碼等的特殊字符串。 - 你可以運用
Replace Data Value with Object
(以對象取代數據值)將原本單獨存在的數據值替換為對象,從而走出洞窟,進入炙手可熱的對象世界。 - 如果想要替換的數據值是類型碼,而它并不影響行為,則可以運用
Replace Type Code with Class
(以類取代類型碼)將它換掉。 - 如果你有與類型碼相關的條件表達式,可運用
Replace Type Code with Subclass
(以子類取代類型碼)或Replace Type Code with State/Strategy
(以State/Strategy
取代類型碼)加以處理。 - 如果你有一組應該總是被放在一起的字段,可運用
Extract Class
(提煉類)。 - 如果你在參數列中看到基本類型數據,不妨試試
Introduce Parameter Object
(引入參數對象)。 - 如果你發現自己正從數組中挑選數據(數組中的元素各自代表不同的東西),可運用
Replace Array with Object
(以對象取代數組)。
十. Switch Statements(Switch驚悚現身)
- 面向對象程序的一個最明顯特征就是:少用
switch
語句。從本質上說,switch
語句的問題在于重復。面向對象中的多態概念可為此帶來優雅的解決辦法。 - 使用
Extract Method
(提煉函數)將switch
語句提煉到一個獨立函數中,再以Move Method
(搬移函數)將它搬移到需要多態性的那個類里。 - 你必須決定是否使用
Replace Type Code with Subclass
(以子類取代類型碼)或Replace Type Code with State/Strategy
(以State/Strategy
取代類型碼)。一旦這樣完成繼結構之后,你就可以運用Replace Conditional with Polymorphism
(以多態取代條件表達式)了。 - 如果你只是在單一函數中使用
switch
語句,多態就有點殺雞用牛刀了。這種情況下Replace Parameter with Explicit Methods
(以明確函數取代參數)是個不錯的選擇。如果你的選擇條件之一是null
,可以試試Introduce Null Object
(引入null
對象)。
十一. Parallel Inheritance Hierarchies(平行繼承體系)
-
Parallel Inheritance Hierarchies
(平行繼承體系)其實是Shotgun Surgery
(散彈式修改)的特殊情況。在這種情況下,每當你為某個類添加一個子類,必須也為另一個類相應增加一個子類。 - 讓一個繼承體系的實例引用另一個繼承體系的實例。如果再接再厲運用
Move Method
(搬移函數)和Move Field
(搬移字段),就可以將引用端的繼承體系消弭于無形。
十二. Lazy Class(冗贅類)
- 你創建的每一個類,都得有人去維護它。如果一個類的所得不值其身價,就應該消失。
- 如果某些子類沒有做足夠的工作,試試
Collapse Hierarchy
(折疊繼承體系)。 - 對于幾乎沒用的組件,你應該以
Inline Class
(將類內聯化)對付他們。
十三. Speculative Generality(夸夸其談未來性)
- 當有人說“噢,我想我們總有一天需要做這事”,并企圖以各式各樣的鉤子和特殊情況來處理一些非必要的事情,這種壞味道就出現了。
- 如果所有裝置都會被用到,那就值得那么做;如果用不到,就不值得。用不上的裝置只會擋你的路,所以,把它搬開吧。
- 如果你的某個抽象類其實沒有太大作用,請運用
Collapse Hierarchy
(折疊繼承體系)。 - 不必要的委托可運用
Inline Class
(將類內聯化)除掉。 - 如果函數的某些參數未被用上,可對它實施
Remove Parameter
(移除參數)。 - 如果函數名稱帶有多余的抽象意味,應該對它實施
Rename Method
(函數改名),讓它更現實一些。
十四. Temporary Field(令人迷惑的暫時字段)
- 有時你會看到這樣的對象:其內某個實例變量僅為某種特定情況而設。這樣的代碼讓人不宜理解,因為你通常認為對象在所有時候都需要它的所有變量。
- 請使用
Extract Class
(提煉類)給這個可憐的孤兒創造一個家,然后把所有和這個變量相關的代碼都放進這個新家。 - 或許你還可以使用
Introduce Null Object
(引入Null
對象)在變量不合法的情況下創建一個Null
對象,從而避免寫出條件式代碼。 - 如果類中有一個復雜算法,需要好幾個變量,實現者不希望傳遞一長串參數,所以他把這些參數放進字段中,導致壞味道。這些字段只在使用該算法時才有效,你可以利用
Extract Class
(提煉類)把這些變量和其相關函數提煉到一個獨立類中。提煉后的新對象將是一個函數對象。
十五. Message Chains(過度耦合的消息鏈)
- 如果你看到用戶向一個對象請求另一個對象,然后再向后者請求另一個對象,然后再請求另一個對象……這就是消息鏈。
- 你應該使用
Hide Delegate
(隱藏“委托關系”)。 - 先觀察消息鏈最終得到的對象時用來干什么的,看看能否以
Extract Method
(提煉函數)把使用該對象的代碼提煉到一個獨立函數中,再運用Move Method
(搬移函數)把這個函數推入消息鏈。
十六. Middle Man(中間人)
- 對象的基本特征之一就是封裝——對外部世界隱藏其內部細節。封裝往往伴隨委托。
- 人們可能過度運用委托。你也許會看到某個類接口有一半的函數都委托給其他類,這樣就是過度運用。
- 這時應該使用
Remove Middle Man
(移除中間人),直接和真正負責的對象打交道。 - 如果這樣“不干實事”的函數只有少數幾個,可以運用
Inline Method
(內聯函數)把他們放進調用端。 - 如果這些中間人還要其他行為,可以運用
Replace Delegation with Inheritance
(以繼承取代委托)把它變成實責對象的子類,這樣你既可以擴展原對象的行為,又不必負擔那么多的委托動作。
十七. Inappropriate Intimacy(狎昵關系)
- 有時你會看到兩個類過于親密,花費太多時間去探究彼此的私有成分。如果這發生在兩個“人”之間,我們不必做衛道士;但對于類,我們希望他們嚴守清規。
- 可以采取
Move Method
(搬移函數)和Move Field
(搬移字段)幫他們劃清界限,從而減少狎昵關系。 - 你也可以看看是否可以運用
Change Bidirectional Association to Unidirectional
(將雙向關聯改為單向關聯),讓其中一個類對另一個斬斷情絲。 - 如果兩個類實在是情投意合,可以運用
Extract Class
(提煉類)把兩者共同點提煉到一個安全地點,讓他們坦蕩地使用這個新類。或者也可以嘗試運用Hide Delegate
(隱藏“委托關系”)讓另一個類來為他們傳遞相思情。 - 繼承往往造成過度親密,因為子類對超類的了解總是超過后者的主觀愿望。如果你覺得是該讓這個孩子獨自生活了,請運用
Replace Inheritance with Delegation
(以委托取代繼承)讓它離開繼承體系。
十八. Alternative Classes with Different Interfaces(異曲同工的類)
- 如果兩個函數做同一件事,卻有著不同的簽名,請運用
Rename Method
(函數改名)根據他們的用途重新命名。 - 反復運用
Move Method
(搬移函數)將某些行為移入類,直到兩者的協議一致為止。如果你必須重復而贅余地移入代碼才能完成這些,或許可運用Extract Superclass
(提煉超類)為自己贖點罪。
十九. Incomplete Library Class(不完美的庫類)
- 復用常被視為對象的終極目的。許多編程技術都建立在程序庫的基礎上。
- 庫類構筑者沒有未卜先知的能力,我們不能因此責怪他們。庫往往構造的不夠好,而且往往不可能讓我們修改其中的類使它完成我們希望完成的工作。
- 如果你只想修改庫類的一兩個函數,可以運用
Introduce Foreign Method
(引入外加函數);如果想要添加一大堆額外行為,就得運用Introduce Local Extension
(引入本地擴展)。
二十. Data Class(幼稚的數據類)
- 幼稚的數據類是指:他們擁有一些字段,以及用于訪問(讀寫)這些字段的函數,除此之外一無長物。
- 這樣的類只是一種不會說話的數據容器,他們幾乎一定被其他類過分細鎖地操縱著。
- 你應該運用
Encapsulate Collection
(封裝集合)把他們封裝起來。對于那些不該被其他類修改的字段,請運用Remove Setting Method
(移除設值函數)。 - 找出這些取值/設值函數被其他類運用的地點。嘗試
Move Method
(搬移函數)把那些調用行為搬移到Data Class
(幼稚的數據類)來。如果無法搬移整個函數,就運用Extract Method
(提煉函數)產生一個可被搬移的函數。不久以后你就可以運用Hide Method
(隱藏函數)把這些取值/設置函數隱藏起來了。 -
Data Class
(幼稚的數據類)就像小孩子。作為一個起點很好,但若要讓它們像成熟的對象那樣參與整個系統的工作,它們就必須承擔一定責任。
二十一. Refused Bequest(被拒絕的遺贈)
- 子類應該繼承超類的函數和數據。但如果他們得到所有禮物,卻只從中挑選幾樣來玩!又該怎么辦呢?
- 按傳統做法,你需要為這個子類新建一個兄弟類,再運用
Push Down Method
(函數下移)和Push Down Field
(字段下移)把所有用不到的函數從超類下推給那個兄弟。這樣,超類就只持有所有子類共享的東西。 - 我們不建議你胡亂修改繼承體系,應該運用
Replace Inheritance with Delegation
(以委托取代繼承)來達到目的。
二十二. Comments(過多的注釋)
- 注釋本身不是一種壞味道,事實上他們還是一種香味呢。
- 有時候,注釋之所以存在乃是因為代碼很糟糕。把注釋當做除臭劑是一種壞味道。
- 很多時候,注釋可以幫助我們找到代碼的壞味道。找到壞味道之后,我們首先應該以各種重構手法把壞味道去除。完成之后我們常常會發現:注釋已經變得多余了,因為代碼已經清晰說明了這一切。
- 如果你需要注釋來解釋一塊代碼做了什么,試試
Extract Method
(提煉函數);如果函數已經提煉出來,但還是需要注釋來解釋其行為,試試Rename Method
(函數改名);如果你需要注釋說明某些系統的需求規格,試試Introduce Assertion
(引入斷言)。 - 當你感覺需要撰寫注釋時,請先嘗試重構,試著讓所有注釋都變得多余。
- 注釋應該用來記述將來的打算、標記你并無十足把握的區域。你可以在注釋里寫下自己“為什么做某某事”。這類信息可以幫助將來的修改者,尤其是那些健忘的家伙。