《重構》代碼壞味道

Divergent Change(發散式變化)

1、官方解釋:

我們希望軟件能夠更容易被修改——畢竟軟件再怎麼說本來就該是「軟」的。一旦需要修改,我們希望能夠跳到系統的某一點,只在該處做修改。如果不能做到這點,你就嗅出兩種緊密相關的刺鼻味道中的一種了。

如果某個class經常因爲不同的原因在不同的方向上發生變化,Divergent Change就出現了。當你看着一個class說:『呃,如果新加入一個數據庫,我必須修改這三個函數;如果新出現一種金融工具,我必須修改這四個函數』,那麼此時也許將這個對象分成兩個會更好,這麼一來每個對象就可以只因一種變化而需要修改。當然,往往只有在加入新數據庫或新金融工具後,你才能發現這一點。針對某一外界 變化的所有相應修改,都只應該發生在單一class中,而這個新class內的所有內容都應該反應該外界變化。爲此,你應該找出因着某特定原因而造成的所有變化,然後運用Extract Class 將它們提煉到另一個class中。

2、通俗來講:

  由於代碼的各種修改或擴展,每次都要修改某個類

3、壞處:
  可擴展性差,某個類乾的事過多。
4、目標:
  針對某一外界變化的所有相應修改,都只發生在單一類中。這個類內的所有內容都應該反應此變化。
5、實現方法:
  在代碼的不斷更迭中,多注意看哪些類經常會因爲更迭而修改 。
  把這些經常變化的類獨立出來,提取成單一類,專門負責此種類型的修改,考慮使用 Extract Class(提取類),降低不同的代碼修改和擴展時造成同一個類不斷的被修改。

Shotgun Surgery(散彈式修改)

1、官方解釋:

Shotgun Surgery類似Divergent Change,但恰恰相反。如果每遇到某種變化,你都必須在許多不同的classes內做出許多小修改以響應之,你所面臨的壞味道就是Shotgun Surgery。如果需要修改的代碼散佈四處,你不但很難找到它們,也很容易忘記某個重要的修改。

這種情況下你應該使用Move Method 和 Move Field 把所有需要修改的代碼放進同一個class。如果眼下沒有合適的可以安置這些代碼,就創造一 個。通常你可以運用Inline Class 把一系列相關行爲放進同一個class。這可能會造成少量Divergent Change,但你可以輕易處理它。

Divergent Change是指「一個class受多種變化的影響」,Shotgun Surgery則是指「一種變化引發多個classes相應修改」。這兩種情況下你都會希望整理代碼,取得「外界變化」與「待改類」呈現一對一關係的理想境地。

2、通俗來講:

  牽一髮而動全身,每次遇到某種變化,都必須在不同的類中做出小修改
3、壞處:
  代碼散步各處,不利於擴展和閱讀,增加代碼修改難度及工作量
4、目標:
  儘量使某類變化通過某個特定類來處理,避免修改過多類。提升代碼可擴展性,減少不必要的工作量。
5、實現方法:
  考慮使用Move Method(搬移函數) 和Move Field(搬移字段) 把所有需要修改的代碼放進同一個類。如果沒有合適的類,就新建一個,考慮使用Inline Class(將類內聯) 把一系列相關行爲放進同一個類,這有可能造成Divergent Change(上篇講到過)。
  此種情況與上篇所述的Divergent Change情況正好相反。
6、本文涉及的重構方法
  Inline Class (將類內聯化)
    作用:
    把某個做很少事且未來擴展可能性較爲低的類合併到另一個類中。清除不必要的類,提升代碼質量。
    步驟:
    利用Move Method(搬移函數) 和 Move Field(搬移字段) 把源類搬到需要搬到的目標類中
    修改原來類內的引用,調整函數及變量的作用域
    編譯,測試。
    重構思路:
    把不必要的,做的事過少,並且未來擴展的可能性也較小的類合併到另一個經常引用,或者抽象概念上,可以分爲一類的類中。
    建議:
    使用前考慮此類未來是否可能進行擴展,諮詢同事等確認,以免無效勞動。
  Move Field (搬移字段) 與Move Method(搬移函數)
    重構思路:
    搬移字段:
    類中的某個字段比起自己類裏,在另一個類中的使用頻率更高,把此字段搬移到那個類中。使用此方法也需注意各個類的職責,並評估未來變化。
    搬移函數
    類中的某個函數比起自己類裏,在另一個類中的使用頻率更高,把此函數搬移到那個類中。使用此方法也需注意各個類的職責,並評估未來變化。

 

Feature Envy(依戀情結)

官方解釋:

對象技術的全部要點在於:這是一種「將數據和加諸其上的操作行爲包裝在一起」 的技術。有一種經典氣味是:函數對某個class的興趣高過對自己所處之host class的興趣。這種孺慕之情最通常的焦點便是數據。無數次經驗裏,我們看到某個函數 爲了計算某值,從另一個對象那兒調用幾乎半打的取值函數(getting method)。療法顯而易見:把這個函數移至另一個地點。你應該使用Move Method 把它 移到它該去的地方。有時候函數中只有一部分受這種依戀之苦,這時候你應該使用 Extract Method 把這一部分提煉到獨立函數中,再使用Move Method 帶它去它的夢中家園。

當然,並非所有情況都這麼簡單。一個函數往往會用上數個特性,那麼它究竟該被置於何處呢?我們的原則是:判斷哪個class擁有最多「被此函數使用」的數據,然後就把這個函數和那些數據擺在一起。如果先以Extract Method 將這個函數分解爲數個較小函數並分別置放於不同地點,上述步驟也就比較容易完成了。

有數個複雜精巧的模式(patterns)破壞了這個規則。說起這個話題,「四巨頭」[Gang of Four]的Strategy 和Visitor立刻跳入我的腦海,Kent Beck 的 Self Delegation [Beck]也在此列。使用這些模式是爲了對抗壞味道Divergent Change。最根本的原則是:將總是一起變化的東西放在一塊兒。「數據」和「引用這些數據」的行爲總是一起變化的,但也有例外。如果例外出現,我們就搬移那些行爲,保持「變化只在一地發生」。Strategy 和Visitor『使你得以輕鬆修改函數行爲,因爲它們將少量需被覆寫〔overridden)的行爲隔離開來——當然也付出了「多一層間接性」的 代價。

通俗來講:

  某個函數爲了實現其功能,經常從另一個類中獲取大量數據。比起自身所在的類來說,更加依賴於另一個類
壞處:
  代碼結構混亂,類分功不明確,可能造成其他壞味道。
目標:
  確定類的具體作用,併合理擺置每個函數所在的類。
實現方法:
  考慮使用Move Method(搬移函數),如果只有函數內的一部分特別依賴其他類,先使用Extract Method(提煉函數), 再考慮使用Move Method(搬移函數)。
  如果一個函數用到了多個類的功能,那需要判斷哪個類擁有最多被此函數使用的數據,把這個函數和數據擺在一起,使用Extract Method(提煉函數) 將函數分解爲數個較小的函數,並分別放置在不同的位置。
  如果某些設計模式(比如策略模式,委託模式)看起來破壞了此規矩,考慮將總是變化的部分和不怎麼變化的部分分開,保持變化只在一地發生。

Data Clumps(數據泥團)

官方解釋:

數據項(data items)就像小孩子:喜歡成羣結隊地待在一塊兒。你常常可以在很多地方看到相同的三或四筆數據項:兩個classes內的相同值域(field)、許多函數簽名式(signature)中的相同參數。這些「總是綁在一起出現的數據」真應該放進屬於它們自己的對象中。首先請找出這些數據的值域形式(field)出現點,運用Extract Class 將它們提煉到一個獨立對象中。然後將注意力轉移到函數簽名式(signature)上頭,運用Introduce Parameter Object 或Preserve Whole Object 爲它減肥。這麼做的直接好處是可以將很多參數列縮短,簡化函數調用動作。是的,不必因爲Data Clumps只用上新對象的一部分值域而在意,只要你以新對象取代兩個(或更多)值域,你就值回票價了。

一個好的評斷辦法是:刪掉衆多數據中的一筆。其他數據有沒有因而失去意義?如果它們不再有意義,這就是個明確信號:你應該爲它們產生一個新對象。

縮短值域個數和參數個數,當然可以去除一些壞味道,但更重要的是:一旦擁有新對象,你就有機會讓程序散發出一種芳香。得到新對象後,你就可以着手尋找Feature Envy,這可以幫你指出「可移至新class」中的種種程序行爲。不必太久, 所有classes都將在它們的小小社會中充分發揮自己的生產力。

通俗來講:

  多個類中重複出現的字段,或多個函數(方法)中相同的入參。

壞處:
  重複參數多,影響閱讀和理解。
目標:
  減少相同的字段及入參,縮短入參列,簡化函數調用
實現方法:
  找出這些重複的入參,考慮使用Extract Class(提煉類), 把這些入參放到同一個類中,然後函數的入參替換爲此類,入參問題可以考慮使用 Introduce Parameter object (引入參數對象) 和 **Preserve Whole Object (保持對象完整)**來處理。
  處理完以後,就可以尋找Feature Envy(依戀情結)(見上篇),來進一步重構代碼。

Primitive Obsession(基本型別偏執)

官方解釋:

大多數編程環境都有兩種數據:結構型別(record types)允許你將數據組織成有意義的形式;基本型別(Primitive type)則是構成結構型別的積木塊。結構總是會帶 來一定的額外開銷。它們有點像數據庫中的表格,或是那些得不償失(只爲做一兩件事而創建,卻付出太大額外開銷〕的東西。

對象的一個極具價值的東西是:它們模糊(甚至打破)了橫亙於基本數據和體積較大的classes之間的界限。你可以輕鬆編寫出一些與語言內置(基本〕型別無異的小型classes。例如Java就以基本型別表示數值,而以class表示字符串和日期——這 兩個型別在其他許多編程環境中都以基本型別表現。

對象技術的新手通常不願意在小任務上運用小對象——像是結合數值和幣別的 money classes 、含一個起始值和一個結束值的range classes、電話號碼或郵政編碼(ZIP) 等等的特殊strings。你可以運用Replace Data Value with Object 將原本單獨存在的數據值替換爲對象,從而走出傳統的洞窟,進入炙手可熱的對象世界。如果欲替換之數據值是 type code(型別碼),而它並不影響行爲,你可以運用 Replace Type Code with Class 將它換掉。如果你有相依於此 type code的條件式,可運用 Replace Type Code with Subclasses 或 Replace Type Code with State/Strategy 加以處理。

如果你有一組應該總是被放在一起的值域(fields),可運用Extract Class。 如果你在參數列中看到基本型數據,不妨試試Introduce Parameter Object。 如果你發現自己正從array中挑選數據,可運用Replace Array with Object。

通俗來講:

  以類代替原本單獨存在的數值

壞處:
  單獨存在的數值不易於理解,也不符合面向對象的思想。
目標:
  使數值儘量用類代替,就像java中的基本類型那樣。
實現方法:
  單獨存在的數據值,考慮使用Replace Data Value with Object (以對象取代數據值)
  如果想要替換的數據值是類型碼,而這些類型碼並不影響行爲,則可以運用Replace Type Code with Class (以類取代類型碼)
  如果有與類型碼相關的條件表達式,可運用Replace Type Code with Subclass (以子類替代類型碼) 或 Replace Type Code with State/Strategy(以狀態/策略取代類型碼)。
  如果有一組總是放在一起的字段,可運用Extract Class(提取類)
  如果你在參數列中看到基本型數據,試試Introduce Parameter Object

 

Switch Statements(switch驚悚現身)

官方解釋:

面向對象程序的一個最明顯特徵就是:少用switch (或case)語句。從本質上說, switch語句的問題在於重複(duplication)。你常會發現同樣的switch語句散佈 於不同地點。如果要爲它添加一個新的子句,你必須找到所有switch語句 並修改它們。面向對象中的多態(polymorphism )概念可爲此帶來優雅的解決辦法。

大多數時候,一看到switch語句你就應該考慮以「多態」來替換它。問題是態 該出現在哪兒?switch語句常常根據 type code(型別碼)進行選擇,你要的是「與 該 type code相關的函數或class」。所以你應該使用Extract Method 將switch語句提煉到一個獨立函數中,再以Move Method 將它搬移到需要多態性的那個class裏頭。此時你必須決定是否使用 Replace Type Code with Subclasses 或 Replace Type Code with State/Strategy。一旦這樣完成繼承結構之後, 你就可以運用Replace Conditional with Polymorphism了。

如果你只是在單一函數中有些選擇事例,而你並不想改動它們,那麼「多態」就有 點殺雞用牛刀了。這種情況下Replace Parameter with Explicit Methods是個不錯的選擇。如果你的選擇條件之一是null,可以試試Introduce Null Object。

通俗來講:

  少用switch語句,儘量用多態取代
壞處:
  switch帶來代碼重複的問題
目標:
  使用多態代替switch
實現方法:
  如果需要根據類型碼進行選擇,使用Extract Method(提煉函數),在使用**Move Method(搬移函數)**搬到需要改爲多態性的類中。
  之後,可以考慮使用Replace Type Code with Subclasses(以子類代替類型碼) 或 Replace Type Code with State/Strategy(用狀態或策略模式代替類型碼)
  完成了前面的步驟,就可運用Replace Conditional with Polymorphism (使用多態代替條件語句) 來消除switch了。
  如果想要替換的數據值是類型碼,而這些類型碼並不影響行爲,則可以運用Replace Type Code with Class (以類取代類型碼)
  如果有與類型碼相關的條件表達式,可運用Replace Type Code with Subclass (以子類替代類型碼) 或 Replace Type Code with State/Strategy(以狀態/策略取代類型碼)。
  如果switch語句並不多,就不宜使用多態方式了,這時候可以考慮使用Replace Parameter with Explicit Methods(以明確的函數取代參數)。
  如果選擇的條件中,有null的,可以試試Introduce Null Object(引入Null對象)

Parallel Inheritance Hierarchies(平行繼承體系)

官方解釋:

Parallel Inheritance Hierarchies其實是shotgun surgery的特殊情況。在這種情況下,每當你爲某個class增加一個subclass,必須也爲另一個class相應增加一個subclass。如果你發現某個繼承體系的名稱前綴和另一個繼承體系的名稱前綴完全相同,便是聞到了這種壞味道。

消除這種重複性的一般策略是:讓一個繼承體系的實體(instance)指涉(參考、引用、refer to)另一個繼承體系的實體(instances)。如果再接再厲運用Move Method 和 Move Field,就可以將指涉端( referring class )的繼承體系消弭於無形。

通俗來講: 

  每當爲一個類增加子類時,必須也爲另一個類相應增加子類。

壞處:
  重複的類
目標:
  讓其中一個繼承體系的實例引用另一個繼承體系的實例,減少平行繼承的類。
實現方法:
  讓其中一個繼承體系的實例引用另一個繼承體系的實例
  使用Move Method (搬移函數) 和**Move Field(搬移字段)**消除引用,最終這些平行繼承的類。

Lazy Class(冗贅類)

官方解釋:

你所創建的每一個class,都得有人去理解它、維護它,這些工作都是要花錢的。如 果一個class的所得不值其身價,它就應該消失。項目中經常會出現這樣的情況: 某個class原本對得起自己的身價,但重構使它身形縮水,不再做那麼多工作;或開發者事前規劃了某些變化,並添加一個class來應付這些變化,但變化實際上沒 有發生。不論上述哪一種原因,請讓這個class莊嚴赴義吧。如果某些subclass沒有做滿足夠工作,試試 Collapse Hierarchy。對於幾乎沒用的組件,你應該以Inline Class對付它們。

通俗來講:
  一個類用處少或者已經無用了

壞處:
  多餘的類不利於程序維護

目標:
  消除這些多餘的類

實現方法:

  如果是某些子類沒有做足夠的工作,使用Collapse Hierarchy (摺疊繼承體系)

  其他沒用的類,使用Inline Class (將類合併) 。

 

Speculative Generality(誇誇其談未來性)

官方解釋:

這個令我們十分敏感的壞味道,命名者是Brian Foote。當有人說『噢,我想我們總有一天需要做這事』並因而企圖以各式各樣的掛勾(hooks)和特殊情況來處理一 些非必要的事情,這種壞味道就出現了。那麼做的結果往往造成系統更難理解和維護。如果所有裝置都會被用到,那就值得那麼做;如果用不到,就不值得。用不上的裝置只會擋你的路,所以,把它搬開吧。

如果你的某個abstract class其實沒有太大作用,請運用Collapse Hierarchy。非必要之delegation (委託)可運用Inline Class 除掉。如果函數的某些參數未被用上,可對它實施 Remove Parameter。如果函數名稱帶有多餘的抽象意味,應該對它實施Rename Method 讓它現實一些。

如果函數或class的惟一用戶是test cases (測試用例),這就飄出了壞味道Speculative Generality。如果你發現這樣的函數或class,請把它們連同其test cases都刪掉。但如果它們的用途是幫助test cases檢測正當功能,當然必須刀下留人。

通俗來講:
  高估未來的擴展性,添加過多不必要的類,方法或繼承體系
壞處:
  不利於程序維護,可讀性差
目標:
  依現實來重新評估,去除多餘的代碼部分。
實現方法:
  使用Collapse Hierarchy (摺疊繼承體系) 和 Inline Class (將類合併) 。
  如果函數中某些參數沒有用到,考慮使用Remove Parameter (移除參數)
  如果函數名稱過於抽象,可以使用Rename Method (重命名函數)

Temporary Field(令人迷惑的暫時值域)

官方解釋:

有時你會看到這樣的對象:其內某個instance變量僅爲某種特定情勢而設。這樣的代碼讓人不易理解,因爲你通常認爲對象在所有時候都需要它的所有變量。在變量未被使用的情況下猜測當初其設置目的,會讓你發瘋。

請使用 Extract Class 給這個可憐的孤兒創造一個家,然後把所有和這個變 量相關的代碼都放進這個新家。也許你還可以使用 Introduce Null Object 在「變量不合法』的情況下創建一個Null對象,從而避免寫出『條件式代碼」。

如果class中有一個複雜算法,需要好幾個變量,往往就可能導致壞味道Temporary Field的出現。由於實現者不希望傳遞一長串參數(想想爲什麼),所以他把這些 參數都放進值域(field)中。但是這些值域只在使用該算法時纔有效,其他情況下只會讓人迷惑。這時候你可以利用 Extract Class 把這些變量和其相關函數提煉到一個獨立class中。提煉後的新對象將是一個method object[Beck](譯註:其存在只是爲了提供調用函數的途徑,class本身並無抽象意味)。

通俗來講:
  某個實例變量僅爲代碼中一小部分功能臨時所用而創建

壞處:
  可讀性差

目標:
  減少代碼中的臨時變量

實現方法:

  把這些變量和相關的函數使用Extract Class(提煉類), 放到新的類中,把原來引用這些臨時變量的地方改爲調用此新類中的函數。

 

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

官方解釋:

如果你看到用戶向一個對象索求(request)另一個對象,然後再向後者索求另一個對象,然後再索求另一個對象……這就是Message Chains。實際代碼中你看到的可 能是一長串getThis()或一長串臨時變量。採取這種方式,意味客戶將與查找過程中的航行結構(structure of the navigation)緊密耦合。一旦對象間的關係發生任何變化,客戶端就不得不做出相應修改。

這時候你應該使用Hide Delegate。你可以在Message Chains的不同位置進行這種重構手法。理論上你可以重構Message Chains上的任何一個對象,但這麼做往往會把所有中介對象(intermediate object )都變成Middle Man。通常更好的選擇是:先觀察Message Chains最終得到的對象是用來幹什麼的,看看能否以 Extract Method 把使用該對象的代碼提煉到一個獨立函數中,再運用Move Method 把這個函數推入Message Chains。如果這條鏈上的某個對象有多位客戶打算航行此航線的剩餘部分,就加一個函數來做這件事。

有些人把任何函數鏈(method chain。譯註:就是Message Chains;面向對象領域中所謂「發送消息」就是「調用函數」)都視爲壞東西,我們不這樣想。呵呵,我們的冷靜鎮定是出了名的,起碼在這件事情上是這樣。

通俗來講:
  代碼中調用鏈過長
壞處:
  代碼耦合度高,造成代碼擴展或修改困難
目標:
  減少代碼中調用鏈過長過複雜造成的耦合度高問題
實現方法:
  使用 Hide Delegate(隱藏委託關係) 來減少耦合。
  在重構前,觀察調用鏈最終得到的對象是用來幹什麼的,看是否能用Extract Method(提煉函數),把使用該對象的代碼提煉到獨立的函數中,再運用Move Method(搬移函數),在原調用鏈中使用這個函數。

Middle Man(中間轉手人)

官方解釋:

對象的基本特徵之一就是封裝(encapsulation)——對外部世界隱藏其內部細節。封裝往往伴隨delegation (委託)。比如說你問主管是否有時間參加一個會議,他就把這個消息委託給他的記事簿,然後才能回答你。很好,你沒必要知道這位主管到底使用傳統記事簿或電子記事簿抑或祕書來記錄自己的約會。

但是人們可能過度運用delegation。你也許會看到某個class接口有一半的函數都委託給其他class,這樣就是過度運用。這時你應該使用Remove Middle Man,直接和實責對象打交道。如果這樣「不幹實事」的函數只有少數幾個,可以運用 Inline Method 把它們" Inlining",放進調用端。如果這些Middle Man還有其他行 爲,你可以運用 Replace Delegation with Inheritance 把它變成實責對象的subclass,這樣你既可以擴展原對象的行爲,又不必負擔那麼多的委託動作。

通俗來講:
  類中的函數存在過度委託給其他對象的情況
壞處:
  多餘的代碼,中間人作用小
目標:
  委託函數過多時,減少委託,讓調用者直接訪問目標類進行操作
實現方法:
  與上一篇Message Chains相反,減少委託函數
  使用**Remove Middle Man(移除中間人)**來減少無用的委託對象,
  如果委託函數只有少數幾個,可以運用**Inline Method(合併函數)**來合併方法使代碼更精煉。
  如果這些委託類裏同時還有其他行爲,可以運用Replace Delegation with Inheritance (以繼承代替委託) 來重構

Inappropriate Intimacy(狎暱關係)

官方解釋:

有時你會看到兩個classes過於親密,花費太多時間去探究彼此的private成分。如果這發生在兩個「人」之間,我們不必做衛道之士;但對於classes,我們希望它們嚴守清規。

就像古代戀人一樣,過份狎暱的classes必須拆散。你可以採用 Move Method 和 Move Field 幫它們劃清界線,從而減少狎暱行徑。你也可以看看是否運用 Change Bidirectional Association to Unidirectional 讓其中一個class對另一個斬斷情絲。如果兩個實在是情投意合,可以運用Extract Class 把兩者共同點提煉到一個安全地點,讓它們坦蕩地使用這個新class。或者也可以嘗試運用 Hide Delegate 讓另一個class來爲它們傳遞相思情。

繼承(inheritance)往往造成過度親密,因爲subclass對superclass的瞭解總是超過superclass的主觀願望。如果你覺得該讓這個孩子獨自生活了,請運用Replace Delegation with Inheritance 讓它離開繼承體系。

通俗來講:
  兩個類間互相依賴,總是調用對方的過多屬性。
壞處:
  代碼耦合度高,破壞類的獨立性
目標:
  把聯繫過分緊密部分搬移或者建立新類放在其中。
實現方法:
  使用Move Method(搬移函數) 和 Move Field(搬移字段) 來使字段或函數放在正確的位置,避免2個類之間過多的交互。
  也可以嘗試使用 Change Bidirectional Association to Unidirectional (將雙向關聯改爲單向關聯) 來減少耦合。
  如果兩個類實在分離不開,使用 Extract Class(提煉類) 把兩者合併到一塊。也可以嘗試 Hide Delegate(隱藏委託關係) 把互相之間的訪問委託給第三方來斬斷直接的訪問。
  如果是繼承關係,子類不需要父類中過多的內容,運用 Replace Inheritance with Delegation(用委託代替繼承) 來處理不恰當的繼承關係。

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

官方解釋:

  如果兩個函數做同一件事,卻有着不同的簽名式(signatures),請運用Rename Method 根據它們的用途重新命名。但這往往不夠,請反覆運用Move Method 將某些行爲移入classes,直到兩者的協議(protocols )一致爲止。如果你必須重複而贅餘地移入代碼才能完成這些,或許可運用Extract Superclass 爲自己贖 點罪。

通俗來講:
  多個做幾乎相同工作的函數
壞處:
  代碼重複性過高
目標:
  把聯繫過分緊密部分搬移或者建立新類放在其中。
實現方法:
  使用 Rename Method(重命名函數) 重新命名這些相同功能的函數,反覆運用 Move Method(搬移函數) 將這些函數移入類,如果需要移入類的函數過多,可以考慮**Extract Superclass(提煉父類)**來減少過多贅餘的屬性。

 

Incomplete Library Class(不完美的程序庫類)

官方解釋:

複用(reuse)常被視爲對象的終極目的。我們認爲這實在是過度估計了(我們只是使用而己)。但是無可否認,許多編程技術都建立在library classes (程序庫類)的基礎上,沒人敢說是不是我們都把排序算法忘得一乾二淨了。

library classes構築者沒有未卜先知的能力,我們不能因此責怪他們。畢竟我們自己也幾乎總是在系統快要構築完成的時候才能弄清楚它的設計,所以library 構築者的任務真的很艱鉅。麻煩的是library的形式(form)往往不夠好,往往不可能讓我們修改其中的classes使它完成我們希望完成的工作。這是否意味那些經過實踐檢驗的戰術如 Move Method 等等,如今都派不上用場了?

幸好我們有兩個專門應付這種情況的工具。如果你只想修改library classes內的一兩 個函數,可以運用 Introduce Foreign Method;如果想要添加一大堆額外行爲,就得運用Introduce Local Extension。

通俗來講:
  封裝好的類庫中功能不能滿足實際需求
缺憾:
  庫中沒有某些需求能夠使用的方法函數等,封裝好的庫不能更改
目標:
  解決代碼不能更改造成的不便
實現方法:
  使用 Introduce Foreign Method(引入外加函數)
  或使用 Introduce Local Extension(引入本地擴展)

Data Class(純稚的數據類)

官方解釋:

所謂Data Class是指:它們擁有一些值域(fields),以及用於訪問(讀寫〕這些值域的函數,除此之外一無長物。這樣的classes只是一種「不會說話的數據容器」,它們幾乎一定被其他classes過份細瑣地操控着。這些classes早期可能擁有public值域,果真如此你應該在別人注意到它們之前,立刻運用Encapsulate Field 將它們封裝起來。如果這些classes內含容器類的值域(collection fields),你應該 檢査它們是不是得到了恰當的封裝;如果沒有,就運用 Encapsulate Collection 把它們封裝起來。對於那些不該被其他classes修改的值域,請運用 Remove Setting Method。

然後,找出這些「取值/設值」函數(getting and setting methods)被其他classes運用的地點。嘗試以Move Method 把那些調用行爲搬移到Data Class來。如果無法搬移整個函數,就運用 Extract Method 產生一個可被搬移的函數。不久之後你就可以運用Hide Method 把這些「取值/設值」函數隱藏起來了。

Data Class就像小孩子。作爲一個起點很好,但若要讓它們像「成年(成熟)」的對象那樣參與整個系統的工作,它們就必須承擔一定責任。

通俗來講:
  只有一些字段和set/get方法,單純用作數據存儲的類。也就是POJO類。
特點:
  這些POJO類應儘量保持純淨,解耦。
目標:
  消除類中容易耦合的部分,使類中的字段訪問根據需求決定。
實現方法:
  如果包含public字段,使用 **Encapsulate Field(封裝字段)**來私有化
  如果某些字段應該不能被修改,使用 Remove Setting Method(移除set方法)
  如果類裏有Collection集合類型的字段,使用 Encapsulate Collection(封裝集合類)

Refused Bequest(被拒絕的遺贈)

官方解釋:

Subclasses 應該繼承superclasses的函數和數據。但如果它們不想或不需要繼承,又該怎麼辦呢?它們得到所有禮物,卻只從中挑選幾樣來玩!

按傳統說法,這就意味繼承體系設計錯誤。你需要爲這個subclass 新建一個兄弟(sibling class),再運用Push Down Method 和 Push Down Field 把所有用不到的函數下推給那兄弟。這樣一來superclass就只持有所有subclasses共享的東西。常常你會聽到這樣的建議:所有superclasses都應該是抽象的(abstract)。

既然使用「傳統說法」這個略帶貶義的詞,你就可以猜到,我們不建議你這麼做,起碼不建議你每次都這麼做。我們經常利用subclassing手法來複用一些行爲,並發現這可以很好地應用於日常工作。這也是一種壞味道,我們不否認,但氣味通常並不強烈。所以我們說:如果Refused Bequest引起困惑和問題,請遵循傳統忠告。但不必認爲你每次都得那麼做。十有八九這種壞味道很淡,不值得理睬。

如果subclass複用了superclass的行爲(實現),卻又不願意支持superclass的接口,Refused Bequest的壞味道就會變得濃烈。拒絕繼承superclass的實現,這一點我們不介意;但如果拒絕繼承superclass的接口,我們不以爲然。不過即使你不願意繼承接口,也不要胡亂修改繼承體系,你應該運用Replace Inheritance with Delegation 來達到目的。

通俗來講:
  繼承某個類的子類,並不需要父類的某些方法,屬性或不需要實現父類實現的接口
缺點:
  不必要的繼承關係
目標:
  消除不必要的繼承關係,使子類掌握父類需要的方法或字段。
實現方法:
  使用Push Down Method(下移方法) 和 Push Down Field(下移字段) 來讓子類擁有隻有子類需要的方法或字段。
  如果子類服用了父類的方法,卻不願意支持父類的接口,考慮運用Replace Inheritance with Delegation (以委託代替繼承)。

Comments(過多的註釋)

官方解釋:

別擔心,我們並不是說你不該寫註釋。從嗅覺上說,Comments不是一種壞味道;事實上它們還是一種香味呢。我們之所以要在這裏提到Comments,因爲人們常把它當作除臭劑來使用。常常會有這樣的情況:你看到一段代碼有着長長的註釋,然後發現,這些註釋之所以存在乃是因爲代碼很糟糕。這種情況的發生次數之多,實 在令人喫驚。

Comments可以帶我們找到本章先前提到的各種壞味道。找到壞味道後,我們首先應該以各種重構手法把壞味道去除。完成之後我們常常會發現:註釋已經變得多餘了,因爲代碼已經清楚說明了一切。

如果你需要註釋來解釋一塊代碼做了什麼,試試 Extract Method;如果method已經提煉出來,但還是需要註釋來解釋其行爲,試試Rename Method;如果你需要註釋說明某些系統的需求規格,試試 Introduce Assertion。

TIP:當你感覺需要撰寫註釋,請先嚐試重構,試着讓所有註釋都變得多餘。

如果你不知道該做什麼,這纔是註釋的良好運用時機。除了用來記述將來的打算之外,註釋還可以用來標記你並無十足把握的區域。你可以在註釋裏寫下自己「爲什 麼做某某事」。這類信息可以幫助將來的修改者,尤其是那些健忘的傢伙。

通俗來講:
  避免寫不必要的註釋,與其大段的寫註釋,不如使代碼變得更加易讀易懂。
  在寫註釋前,先考慮能否通過重構的方式讓代碼不需要加不必要的註釋。
  註釋主要起到提醒和引導的作用,不可濫用。
缺點:
  多餘且不必要的註釋
目標:
  註釋前,先考慮通過重構消除不必要的註釋。
實現方法:
  Extract Method(方法提煉)
  Rename Method(方法重命名)
  Introduce Assertion (斷言引入) :可以參考別人的好文章

 

參考:

https://blog.csdn.net/HermaeuxMora/article/details/83063598

https://www.kancloud.cn/sstd521/refactor/194210

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章