天天看點

重構之壞味道

壞味道

  1. 神秘的命名

    命名是程式設計中最難的兩件事之一,整 潔代碼最重要的一環就是好的名字,是以我們會深思熟慮如何給函數、子產品、變量和類命名,使它們能清晰地 表明自己的功能和用法。

    改名不僅僅是修改名字而已。如果你想不出一個好名字,說明背後很可能潛藏着更深的設計問題。為一個惱人的名字所付出的糾結,常常能推動我們對代碼進行精簡。

  2. 重複代碼

    如果你在一個以上的地點看到相同的代碼結構,那麼可以肯定:設法将它們合而為一,程式會變得更好。

    最單純的重複代碼就是“同一個類的兩個函數含有相同的表達式”。這時候你需要做的就是采用提煉函數(106)提煉出重複的代碼,然後讓這兩個地點都調用被提煉出來的那一段代碼。如果重複代碼隻是相似而不 是完全相同,請首先嘗試用移動語句(223)重組代碼順序,把相似的部分放在一起以便提煉。如果重複的代 碼段位于同一個超類的不同子類中,可以使用函數上移(350)來避免在兩個子類之間互相調用。

  3. 過長的函數
  4. 過長的參數清單
  5. 全局資料

    全局資料印證了帕拉塞爾斯的格言:良藥與毒藥的差別在于劑量。有少量的全局資料或許無妨,但數量越多,處理的難度就會指數上升。即便隻是少量的資料,我們也願意将它封裝起來,這是在軟體演進過程中應對變化的關鍵所在。

  6. 可變資料
  7. 發散式變化

    一旦需要修改,我們希望能夠跳到系統的某一點,隻在該處做修改。如果不能做到這一點,你就嗅出兩種緊密相關的刺鼻味道中的一種了。如果某個子產品經常因為不同的原因在不同的方向上發生變化,發散式變化就出現了。

  8. 霰彈式修改

    霰彈式修改類似于發散式變化,但又恰恰相反。如果每遇到某種變化,你都必須在許多不同的類内做出許多小修改,你所面臨的壞味道就是霰彈式修改。如果需要修改的代碼散布四處,你不但很難找到它們,也很容易錯過某個重要的修改。

  9. 依戀情結

    所謂子產品化,就是力求将代碼分出區域,最大化區域内部的互動、最小化跨區域的互動。但有時你會發 現,一個函數跟另一個子產品中的函數或者資料交流格外頻繁,遠勝于在自己所處子產品内部的交流,這就是依戀情結的典型情況。

  10. 資料泥團

    資料項就像小孩子,喜歡成群結隊地待在一塊兒。你常常可以在很多地方看到相同的三四項資料:兩個類中相同的字段、許多函數簽名中相同的參數。這些總是綁在一起出現的資料真應該擁有屬于它們自己的對象。

  11. 基本類型偏執

    大多數程式設計環境都大量使用基本類型,即整數、浮點數和字元串等。一些庫會引入一些小對象,如日期。但我們發現一個很有趣的現象:很多程式員不願意建立對自己的問題域有用的基本類型,如錢、坐标、範圍等。于是,我們看到了把錢當作普通數字來計算的情況、計算實體量時無視機關(如把英寸與毫米相加)的情況以及大量類似if (a < upper && a > lower)這樣 的代碼。

    字元串是這種壞味道的最佳培養皿,比如,電話号碼不隻是一串字元。一個體面的類型,至少能包含一緻的顯示邏輯,在使用者界面上需要顯示時可以使用。“用字元串來代表類似這樣的資料”是如此常見的臭味,以至于人們給這類變量專門起了一個名字,叫它們“類字元串類型”(stringly typed)變量。

  12. 重複的switch

    如果你跟真正的面向對象布道者交談,他們很快就會談到switch語句的邪惡。在他們看來,任何switch語句都應該用以多态取代條件表達式(272)消除掉。我們甚至還聽過這樣的觀點:所有條件邏輯都應該用多态取代,絕大多數if語句都應該被掃進曆史的垃圾桶。

    如今的程式員已經更多地使用多态,switch語句也不再像15年前那樣有害無益,很多語言支援更複雜的 switch語句,而不隻是根據基本類型值來做條件判斷。是以,我們現在更關注重複的switch:在不同的地方反 複使用同樣的switch邏輯(可能是以switch/case語句的形式,也可能是以連續的if/else語句的形式)。重複的switch的問題在于:每當你想增加一個選擇分支 時,必須找到所有的switch,并逐一更新。多态給了我

    們對抗這種黑暗力量的武器,使我們得到更優雅的代碼 庫。

  13. 循環語句

    函數作為一等公民已經得到了廣泛的支援,是以 我們可以使用以管道取代循環(231)來讓這些老古董退休。我們發現,管道操作(如filter和map)可以幫助我們更快地看清被處理的元素以及處理它們的動作。

  14. 冗贅的元素

    程式元素(如類和函數)能給代碼增加結構,進而支援變化、促進複用或者哪怕隻是提供更好的名字也好,但有時我們真的不需要這層額外的結構。可能有這樣一個函數,它的名字就跟實作代碼看起來一模一樣;也可能有這樣一個類,根本就是一個簡單的函數。通常你隻需要使用内聯函數(115)或是内聯類(186)。如果這個類處于一個繼承體系中,可以使用折疊繼承體系(380)。

  15. 誇誇其談的通用性

    當有人說“噢,我想我們總有一天需要做這事”,并因而企圖以各式各樣的鈎子和特殊情況來處理一些非必要的事情,這種壞味道就出現了。這麼做的結果往往造成系統更難了解和維護。如果所有裝置都會被用到,就值得那麼做;如果用不到,就不值得。用不上的裝置隻會擋你的路,是以,把它搬開吧。

    如果你的某個抽象類其實沒有太大作用,請運用折疊繼承體系(380)。不必要的委托可運用内聯函數(115)和内聯類(186)除掉。如果函數的某些參數未被用上,可以用改變函數聲明(124)去掉這些參數。 如果有并非真正需要、隻是為不知遠在何處的将來而塞進去的參數,也應該用改變函數聲明(124)去掉。 如果函數或類的唯一使用者是測試用例,這就飄出了壞味道“誇誇其談通用性”。如果你發現這樣的函數或類,可以先删掉測試用例,然後使用移除死代碼(237)。

  16. 臨時字段

    有時你會看到這樣的類:其内部某個字段僅為某種特定情況而設。這樣的代碼讓人不易了解,因為你通常認為對象在所有時候都需要它的所有字段。在字段未被使用的情況下猜測當初設定它的目的,會讓你發瘋。

    請使用提煉類(182)給這個可憐的孤兒創造一個家,然後用搬移函數(198)把所有和這些字段相關的代碼都放進這個新家。也許你還可以使用引入特例(289)在“變量不合法”的情況下建立一個替代對象,進而避免寫出條件式代碼。

  17. 過長的消息鍊

    如果你看到使用者向一個對象請求另一個對象,然後再向後者請求另一個對象,然後再請求另一個對象…… 這就是消息鍊。在實際代碼中你看到的可能是一長串取值函數或一長串臨時變量。采取這種方式,意味用戶端代碼将與查找過程中的導航結構緊密耦合。一旦對象間的關系發生任何變化,用戶端就不得不做出相應修改。

    這時候應該使用隐藏委托關系(189)。你可以在消息鍊的不同位置采用這種重構手法。理論上,你可以重構消息鍊上的所有對象,但這麼做就會把所有中間對象都變成“中間人”。通常更好的選擇是:先觀察消息鍊最終得到的對象是用來幹什麼的,看看能否以提煉函數 (106)把使用該對象的代碼提煉到一個獨立的函數中,再運用搬移函數(198)把這個函數推入消息鍊。如果還有許多用戶端代碼需要通路鍊上的其他對象,同樣添加一個函數來完成此事。

  18. 中間人

    人們可能過度運用委托。你也許會看到某個類的接口有一半的函數都委托給其他類,這樣就是過度運用。這時應該使用移除中間人(192),直接和真正負責的對象打交道。如果這樣“不幹實事”的函數隻有少數幾個,可以運用内聯函數(115)把它們放進調用端。如果這些中間人還有其他行為,可以運用以委托取代超類(399)或者以委托取代子類(381)把它變成真正的對象,這樣你既可以擴充原對象的行為,又不必負擔那麼多的委托動作。

  19. 内幕交易

    軟體開發者喜歡在子產品之間建起高牆,極其反感在子產品之間大量交換資料,因為這會增加子產品間的耦合。在實際情況裡,一定的資料交換不可避免,但我們必須盡量減少這種情況,并把這種交換都放到明面上來。

    如果兩個子產品總是在咖啡機旁邊竊竊私語,就應該用搬移函數(198)和搬移字段(207)減少它們的私下交流。如果兩個子產品有共同的興趣,可以嘗試再建立一個子產品,把這些共用的資料放在一個管理良好的地方; 或者用隐藏委托關系(189),把另一個子產品變成兩者的中介。

    繼承常會造成密謀,因為子類對超類的了解總是超過後者的主觀願望。如果你覺得該讓這個孩子獨立生活 了,請運用以委托取代子類(381)或以委托取代超類 (399)讓它離開繼承體系。

  20. 過大的類

    如果想利用單個類做太多事情,其内往往就會出現太多字段。一旦如此,重複代碼也就接踵而至了。

    你可以運用提煉類(182)将幾個變量一起提煉至新類内。提煉時應該選擇類内彼此相關的變量,将它們放在一起。例如,depositAmount和depositCurrency可能應該隸屬同一個類。通常,如果類内的數個變量有着 相同的字首或字尾,這就意味着有機會把它們提煉到某個元件内。如果這個元件适合作為一個子類,你會發現 提煉超類(375)或者以子類取代類型碼(362)(其實就是提煉子類)往往比較簡單。

    有時候類并非在所有時刻都使用所有字段。若果真如此,你或許可以進行多次提煉。

    和“太多執行個體變量”一樣,類内如果有太多代碼,也是代碼重複、混亂并最終走向死亡的源頭。最簡單的解決方案是把多餘的東西消弭于類内部。如果有5個“百行函數”,它們之中很多代碼都相同,那麼或許你可以把它們變成5 個“十行函數”和10個提煉出來的“雙行函數”。

    觀察一個大類的使用者,經常能找到如何拆分類的線索。看看使用者是否隻用到了這個類所有功能的一個子集,每個這樣的子集都可能拆分成一個獨立的類。一旦識别出一個合适的功能子集,就試用提煉類 (182)、提煉超類(375)或是以子類取代類型碼 (362)将其拆分出來。

  21. 異曲同工的類

    使用類的好處之一就在于可以替換:今天用這個類,未來可以換成用另一個類。但隻有當兩個類的接口一緻時,才能做這種替換。可以用改變函數聲明 (124)将函數簽名變得一緻。但這往往還不夠,請反 複運用搬移函數(198)将某些行為移入類中,直到兩者的協定一緻為止。如果搬移過程造成了重複代碼,或許可運用提煉超類(375)補償一下。

  22. 純資料類

    所謂純資料類是指:它們擁有一些字段,以及用于通路(讀寫)這些字段的函數,除此之外一無長物。這樣的類隻是一種不會說話的資料容器,它們幾乎一定被其他類過分細瑣地操控着。這些類早期可能擁有public字段,若果真如此,你可以運用封裝記錄(162)将它們封裝起來。對于那些不該被其他類修改的字段,請運用移除設值函數(331)。

    然後,找出這些取值/設值函數被其他類調用的地點。嘗試以搬移函數(198)把那些調用行為搬移到純資料類裡來。如果無法搬移整個函數,就運用提煉函數(106)産生一個可被搬移的函數。

    純資料類常常意味着行為被放在了錯誤的地方。也就是說,隻要把處理資料的行為從用戶端搬移到純資料類裡來,就能使情況大為改觀。但也有例外情況,一個最好的例外情況就是,純資料記錄對象被用作函數調用的傳回結果,比如使用拆分階段(154)之後得到的中轉資料結構就是這種情況。這種結果資料對象有一個關鍵的特征:它是不可修改的(至少在拆分階段(154)的實際操作中是這樣)。不可修改的字段無須封裝,使用者可以直接通過字段取得資料,無須通過取值函數。

  23. 被拒絕的遺贈

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

    按傳統說法,這就意味着繼承體系設計錯誤。你需要為這個子類建立一個兄弟類,再運用函數下移(359)和字段下移(361)把所有用不到的函數下推給那個兄弟。這樣一來,超類就隻持有所有子類共享的東西。你常常會聽到這樣的建議:所有超類都應該是抽象 (abstract)的。

    如果“被拒絕的遺贈”正在引起困惑和問題,請遵循傳統忠告。但不必認為你每次都得那麼做。十有八九這種壞味道很淡,不值得理睬。

    如果子類複用了超類的行為(實作),卻又不願意支援超類的接口,“被拒絕的遺贈”的壞味道就會變得很濃烈。拒絕繼承超類的實作,這一點我們不介意;但如果拒絕支援超類的接口,這就難以接受了。既然不願意 支援超類的接口,就不要虛情假意地糊弄繼承體系,應該運用以委托取代子類(381)或者以委托取代超類(399)徹底劃清界限。

  24. 注釋

    注釋不但不是一種壞味道,事實上它們還是一種香味。之是以要在這裡提到注釋,是因為人們常把它當作“除臭劑”來使用。常常會有這樣的情況:你看到 一段代碼有着長長的注釋,然後發現,這些注釋之是以存在乃是因為代碼很糟糕。

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

    如果你需要注釋來解釋一塊代碼做了什麼,試試提煉函數(106);如果函數已經提煉出來,但還是需要注釋來解釋其行為,試試用改變函數聲明(124)為它改名;如果你需要注釋說明某些系統的需求規格,試試引入斷言(302)。