《重構》- 代碼的壞味道

如果尿布臭了,就換掉它。

一. Duplicated Code(重復代碼)

  1. 如果你在一個以上的地點看到相同的程序結構,設法將他們合而為一,程序會變得更好。
  2. 同一個類的兩個函數含有相同的表達式,采用Extract Method(提煉函數)提煉出重復的代碼。
  3. 兩個互為兄弟的子類含有相同的表達式,首先對兩個類都使用Extract Method(提煉函數),然后再對提煉出來的代碼使用Pull Up Method(函數上移),將它推入超類。
  4. 如果代碼之間只是類似,并非完全相同,運用Extract Method(提煉函數)將相似部分和差異部分分割開,然后可以運用Form Template Method(塑造模板函數)獲取一個莫模板方法。
  5. 如果有些函數以不同算法做相同的事情,你可與選擇其中較清晰的一個,使用Substitute Algorithm(替換算法)將其他函數的算法替換掉。
  6. 如果兩個毫不相關的類出現重復代碼,應考慮對其中一個使用Extract Class(提煉類),將重復代碼提煉到一個獨立的類中。
  7. 重復代碼所在的函數應該只屬于某一個類,另一個類調用它;或者應該屬于第三個類,另兩個類引用這第三個類。決定重復函數的最合適位置,確保只有一份。

二. Long Method(過長函數)

  1. 擁有短函數的對象會活的比較好、比較長。
  2. 絕大部分情況下,要把函數變小,只需要使用Extract Method(提煉函數)。
  3. 使用Extract Method(提煉函數)時,如果函數中有個別參數和臨時變量,可以把他們當做參數,傳遞給被提煉出來的新函數。
  4. 如果被提煉函數內有大量的參數和臨時變量,可以運用Replace Temp with Query(以查詢取代臨時變量)來消除這些臨時元素。
  5. 使用Introduce Parameter Object(引入參數對象)可以將過長的參數列變得更簡潔一些。
  6. 如果被提煉函數仍然有太多臨時變量和參數,可以使用Replace Method with Method Object(以函數對象取代函數)。
  7. 注釋通常能夠指出應該被提煉的代碼。就算只有一行代碼,如果它需要以注釋來說明,那也值得將它提煉到獨立函數去。
  8. 你可以使用Decompose Conditional(分解條件表達式)處理條件表達式。
  9. 你應該將循環和其內的代碼提煉到一個獨立的函數中。

三. Large Class(過大的類)

  1. 如果想利用單個類做太多事情,其內往往就會出現太多實例變量。一旦如此,重復代碼也就接踵而至了。
  2. 可以運用Extract Class(提煉類)將幾個彼此相關的實例變量一起提煉至新類內。如果被提煉出的類適合作為一個子類,使用Extract Subclass(提煉子類)往往比較簡單。
  3. 有時候類并非所有時刻都使用所有實例變量。你可以多次使用Extract Class(提煉類)或Extract Subclass(提煉子類)。
  4. 和“太多實例變量”一樣,類內如果有太多代碼,也是代碼重復、混亂并最終走向死亡的源頭。
  5. 如果有五個“百行函數”,他們之中有很多代碼相同,那么你也許可以把他們變成五個“十行函數”和十個提煉出的“雙行函數”。
  6. 和“太多實例變量”一樣,類內如果有太多代碼,往往也適合使用Extract Class(提煉類)或Extract Subclass(提煉子類)。
  7. 如果你的過大的類是一個GUI類,你可能需要把數據和行為移到一個獨立的類中。

四. Long Parameter List(過長參數列)

  1. 太長的參數列難以理解,太多參數會造成前后不一致、不易使用。
  2. 剛開始學習編程時,老師教我們:把函數所需的所有東西都以參數傳遞進去。這可以理解,因為除此之外只能選擇全局數據,而全局數據是邪惡的東西。
  3. 對象技術改變了這一情況:如果你手上沒有所需的東西,總可以叫另一個對象給你。有了對象,函數需要的東西多半可以在函數的宿主類中找到。面向對象程序中的函數,其參數列通常比在傳統程序中短的多。
  4. 如果向已有的對象發出一條請求就可以取代一個參數,那么你應該激活重構手法Replace Parameter with Method(以函數取代參數)。
  5. 你還可以運用Preserve Whole Object(保持對象完整)將來自同一對象的一堆數據收集起來,并以該對象替換他們。
  6. 如果某些數據缺乏合理的對象歸屬,可使用Introduce Parameter Object(引入參數對象)為他們制造出一個“參數對象”。
  7. 這里有一個例外:有時候你明顯不希望造成“被調用對象”與“較大對象”間的某種依賴關系。這時候將數據從對象拆解出來單獨作為參數,也很合情合理。但是請權衡其所引發的代價。

五. Divergent Change(發散式變化)

  1. 我們希望軟件能夠更容易被修改。一旦需要修改,我們希望能夠跳到系統的某一點,只在該處做修改
  2. 如果某個類經常因為不同的原因在不同的方向上發生變化, Divergent Change(發散式變化)就出現了。
  3. 針對某一外界變化的所有相應修改,都只應該發生在單一類中。為此,你應該找出某特定原因而造成的所有變化,運用Extract Class(提煉類)將他們提煉到另一個類中。

六. Shotgun Surgery(散彈式修改)

  1. Shotgun Surgery(散彈式修改)類似Divergent Change(發散式變化),但恰恰相反。
  2. 如果每遇到某種變化,你都必須在許多不同的類內做出許多小修改,你所面臨的壞味道就是Shotgun Surgery(散彈式修改)。
  3. 如果需要修改的代碼散布四處,你不但很難找到他們,也很容易忘記某個重要的修改。
  4. 你應該使用Move Method(搬移函數)和Move Field(搬移字段)把所有需要修改的代碼放進同一個類。
  5. 如果眼下沒有合適的類可以安置這些代碼,就創造一個。通??梢赃\用Inline Class(將類內聯化)把一系列相關行為放進同一個類。
  6. Divergent Change(發散式變化) 是指“一個類受多種變化的影響”。Shotgun Surgery(散彈式修改)則是指“一種變化引起多個類響應修改”。這兩種情況你都希望整理代碼,使“外界變化”與“需要修改的類”趨于一一對應。

七. Feature Envy(依戀情節)

  1. 對象技術即是一種“將數據和對數據的操作行為包裝在一起”的技術。
  2. Feature Envy(依戀情節)指的是:函數對某個類的興趣高過對自己所處類的興趣。
  3. 我們常??吹侥硞€函數為了計算某個值,從另一個對象那兒調用了幾乎半打的取值函數。此時,你應該使用Move Method(搬移函數)把它移到它該去的地方。
  4. 先使用Extract Method(提煉函數),將這個函數分解為數個較小函數并分別置于不同地點,有助于Move Method(搬移函數)重構手法的實施。
  5. 如果一個函數用到幾個類的功能,那么需要判斷哪個類擁有最多被此函數使用的數據,然后就把這個函數和那些數據擺在一起。

八. Data Clumps(數據泥團)

  1. 數據項就像小孩子,喜歡成群結隊地待在一塊兒。這些總是綁在一起出現的數據真應該擁有屬于它們自己的對象。
  2. 首先運用Extract Class(提煉類)將他們提煉到一個獨立對象中,然后將注意力轉移到函數簽名上,運用Introduce Parameter Object(引入參數對象)或Preserve Whole Object(保持對象完整性)為它減肥。這樣做可以縮短參數列,簡化函數調用。
  3. 如果刪掉眾多數據中的一項,其他數據不再有意義,那么他們應該以一個對象的形式存在。
  4. 一旦擁有新對象,你就有機會讓程序散發出一種芬芳。可以將適當的程序行為移至新類。不必太久,所有的類都將在他們的小小社會中發揮價值。

九. Primitive Obsession(基類類型偏執)

  1. 大多數編程環境都有兩種數據:結構類型允許你將數據組織成有意義的形式;基本類型則是構成結構類型的積木塊。
  2. 對象技術的新手通常不愿意在小任務上運用小對象——像是結合數值和幣種money類、由一個起始值和一個結束值組成的range類、電話號碼或郵政編碼等的特殊字符串。
  3. 你可以運用Replace Data Value with Object(以對象取代數據值)將原本單獨存在的數據值替換為對象,從而走出洞窟,進入炙手可熱的對象世界。
  4. 如果想要替換的數據值是類型碼,而它并不影響行為,則可以運用Replace Type Code with Class(以類取代類型碼)將它換掉。
  5. 如果你有與類型碼相關的條件表達式,可運用Replace Type Code with Subclass(以子類取代類型碼)或Replace Type Code with State/Strategy(以State/Strategy取代類型碼)加以處理。
  6. 如果你有一組應該總是被放在一起的字段,可運用Extract Class(提煉類)。
  7. 如果你在參數列中看到基本類型數據,不妨試試Introduce Parameter Object(引入參數對象)。
  8. 如果你發現自己正從數組中挑選數據(數組中的元素各自代表不同的東西),可運用Replace Array with Object(以對象取代數組)。

十. Switch Statements(Switch驚悚現身)

  1. 面向對象程序的一個最明顯特征就是:少用switch語句。從本質上說,switch語句的問題在于重復。面向對象中的多態概念可為此帶來優雅的解決辦法。
  2. 使用Extract Method(提煉函數)將switch語句提煉到一個獨立函數中,再以Move Method(搬移函數)將它搬移到需要多態性的那個類里。
  3. 你必須決定是否使用Replace Type Code with Subclass(以子類取代類型碼)或Replace Type Code with State/Strategy(以State/Strategy取代類型碼)。一旦這樣完成繼結構之后,你就可以運用Replace Conditional with Polymorphism(以多態取代條件表達式)了。
  4. 如果你只是在單一函數中使用switch語句,多態就有點殺雞用牛刀了。這種情況下Replace Parameter with Explicit Methods(以明確函數取代參數)是個不錯的選擇。如果你的選擇條件之一是null,可以試試Introduce Null Object(引入null對象)。

十一. Parallel Inheritance Hierarchies(平行繼承體系)

  1. Parallel Inheritance Hierarchies(平行繼承體系)其實是Shotgun Surgery(散彈式修改)的特殊情況。在這種情況下,每當你為某個類添加一個子類,必須也為另一個類相應增加一個子類。
  2. 讓一個繼承體系的實例引用另一個繼承體系的實例。如果再接再厲運用Move Method(搬移函數)和Move Field(搬移字段),就可以將引用端的繼承體系消弭于無形。

十二. Lazy Class(冗贅類)

  1. 你創建的每一個類,都得有人去維護它。如果一個類的所得不值其身價,就應該消失。
  2. 如果某些子類沒有做足夠的工作,試試Collapse Hierarchy(折疊繼承體系)。
  3. 對于幾乎沒用的組件,你應該以Inline Class(將類內聯化)對付他們。

十三. Speculative Generality(夸夸其談未來性)

  1. 當有人說“噢,我想我們總有一天需要做這事”,并企圖以各式各樣的鉤子和特殊情況來處理一些非必要的事情,這種壞味道就出現了。
  2. 如果所有裝置都會被用到,那就值得那么做;如果用不到,就不值得。用不上的裝置只會擋你的路,所以,把它搬開吧。
  3. 如果你的某個抽象類其實沒有太大作用,請運用Collapse Hierarchy(折疊繼承體系)。
  4. 不必要的委托可運用Inline Class(將類內聯化)除掉。
  5. 如果函數的某些參數未被用上,可對它實施Remove Parameter(移除參數)。
  6. 如果函數名稱帶有多余的抽象意味,應該對它實施Rename Method(函數改名),讓它更現實一些。

十四. Temporary Field(令人迷惑的暫時字段)

  1. 有時你會看到這樣的對象:其內某個實例變量僅為某種特定情況而設。這樣的代碼讓人不宜理解,因為你通常認為對象在所有時候都需要它的所有變量。
  2. 請使用Extract Class(提煉類)給這個可憐的孤兒創造一個家,然后把所有和這個變量相關的代碼都放進這個新家。
  3. 或許你還可以使用Introduce Null Object(引入Null對象)在變量不合法的情況下創建一個Null對象,從而避免寫出條件式代碼。
  4. 如果類中有一個復雜算法,需要好幾個變量,實現者不希望傳遞一長串參數,所以他把這些參數放進字段中,導致壞味道。這些字段只在使用該算法時才有效,你可以利用Extract Class(提煉類)把這些變量和其相關函數提煉到一個獨立類中。提煉后的新對象將是一個函數對象。

十五. Message Chains(過度耦合的消息鏈)

  1. 如果你看到用戶向一個對象請求另一個對象,然后再向后者請求另一個對象,然后再請求另一個對象……這就是消息鏈。
  2. 你應該使用Hide Delegate(隱藏“委托關系”)。
  3. 先觀察消息鏈最終得到的對象時用來干什么的,看看能否以Extract Method(提煉函數)把使用該對象的代碼提煉到一個獨立函數中,再運用Move Method(搬移函數)把這個函數推入消息鏈。

十六. Middle Man(中間人)

  1. 對象的基本特征之一就是封裝——對外部世界隱藏其內部細節。封裝往往伴隨委托。
  2. 人們可能過度運用委托。你也許會看到某個類接口有一半的函數都委托給其他類,這樣就是過度運用。
  3. 這時應該使用Remove Middle Man(移除中間人),直接和真正負責的對象打交道。
  4. 如果這樣“不干實事”的函數只有少數幾個,可以運用Inline Method(內聯函數)把他們放進調用端。
  5. 如果這些中間人還要其他行為,可以運用Replace Delegation with Inheritance(以繼承取代委托)把它變成實責對象的子類,這樣你既可以擴展原對象的行為,又不必負擔那么多的委托動作。

十七. Inappropriate Intimacy(狎昵關系)

  1. 有時你會看到兩個類過于親密,花費太多時間去探究彼此的私有成分。如果這發生在兩個“人”之間,我們不必做衛道士;但對于類,我們希望他們嚴守清規。
  2. 可以采取Move Method(搬移函數)和Move Field(搬移字段)幫他們劃清界限,從而減少狎昵關系。
  3. 你也可以看看是否可以運用Change Bidirectional Association to Unidirectional(將雙向關聯改為單向關聯),讓其中一個類對另一個斬斷情絲。
  4. 如果兩個類實在是情投意合,可以運用Extract Class(提煉類)把兩者共同點提煉到一個安全地點,讓他們坦蕩地使用這個新類。或者也可以嘗試運用Hide Delegate(隱藏“委托關系”)讓另一個類來為他們傳遞相思情。
  5. 繼承往往造成過度親密,因為子類對超類的了解總是超過后者的主觀愿望。如果你覺得是該讓這個孩子獨自生活了,請運用Replace Inheritance with Delegation(以委托取代繼承)讓它離開繼承體系。

十八. Alternative Classes with Different Interfaces(異曲同工的類)

  1. 如果兩個函數做同一件事,卻有著不同的簽名,請運用Rename Method(函數改名)根據他們的用途重新命名。
  2. 反復運用Move Method(搬移函數)將某些行為移入類,直到兩者的協議一致為止。如果你必須重復而贅余地移入代碼才能完成這些,或許可運用Extract Superclass(提煉超類)為自己贖點罪。

十九. Incomplete Library Class(不完美的庫類)

  1. 復用常被視為對象的終極目的。許多編程技術都建立在程序庫的基礎上。
  2. 庫類構筑者沒有未卜先知的能力,我們不能因此責怪他們。庫往往構造的不夠好,而且往往不可能讓我們修改其中的類使它完成我們希望完成的工作。
  3. 如果你只想修改庫類的一兩個函數,可以運用Introduce Foreign Method(引入外加函數);如果想要添加一大堆額外行為,就得運用Introduce Local Extension(引入本地擴展)。

二十. Data Class(幼稚的數據類)

  1. 幼稚的數據類是指:他們擁有一些字段,以及用于訪問(讀寫)這些字段的函數,除此之外一無長物。
  2. 這樣的類只是一種不會說話的數據容器,他們幾乎一定被其他類過分細鎖地操縱著。
  3. 你應該運用Encapsulate Collection(封裝集合)把他們封裝起來。對于那些不該被其他類修改的字段,請運用Remove Setting Method(移除設值函數)。
  4. 找出這些取值/設值函數被其他類運用的地點。嘗試Move Method(搬移函數)把那些調用行為搬移到Data Class(幼稚的數據類)來。如果無法搬移整個函數,就運用Extract Method(提煉函數)產生一個可被搬移的函數。不久以后你就可以運用Hide Method(隱藏函數)把這些取值/設置函數隱藏起來了。
  5. Data Class(幼稚的數據類)就像小孩子。作為一個起點很好,但若要讓它們像成熟的對象那樣參與整個系統的工作,它們就必須承擔一定責任。

二十一. Refused Bequest(被拒絕的遺贈)

  1. 子類應該繼承超類的函數和數據。但如果他們得到所有禮物,卻只從中挑選幾樣來玩!又該怎么辦呢?
  2. 按傳統做法,你需要為這個子類新建一個兄弟類,再運用Push Down Method(函數下移)和Push Down Field(字段下移)把所有用不到的函數從超類下推給那個兄弟。這樣,超類就只持有所有子類共享的東西。
  3. 我們不建議你胡亂修改繼承體系,應該運用Replace Inheritance with Delegation(以委托取代繼承)來達到目的。

二十二. Comments(過多的注釋)

  1. 注釋本身不是一種壞味道,事實上他們還是一種香味呢。
  2. 有時候,注釋之所以存在乃是因為代碼很糟糕。把注釋當做除臭劑是一種壞味道。
  3. 很多時候,注釋可以幫助我們找到代碼的壞味道。找到壞味道之后,我們首先應該以各種重構手法把壞味道去除。完成之后我們常常會發現:注釋已經變得多余了,因為代碼已經清晰說明了這一切。
  4. 如果你需要注釋來解釋一塊代碼做了什么,試試Extract Method(提煉函數);如果函數已經提煉出來,但還是需要注釋來解釋其行為,試試Rename Method(函數改名);如果你需要注釋說明某些系統的需求規格,試試Introduce Assertion(引入斷言)。
  5. 當你感覺需要撰寫注釋時,請先嘗試重構,試著讓所有注釋都變得多余。
  6. 注釋應該用來記述將來的打算、標記你并無十足把握的區域。你可以在注釋里寫下自己“為什么做某某事”。這類信息可以幫助將來的修改者,尤其是那些健忘的家伙。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,441評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,211評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,475評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,834評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,009評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,559評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,306評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,516評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,728評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,249評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,484評論 2 379