重構 改善既有代碼的設計




1.     重構是在不改變軟件可觀察行爲的前提下改善其內部結構.

 

2.     不改變軟件行爲只是重構的最基本要求。要想真正讓重構技術發揮威力,就必須做到 不需要了解軟件行爲 聽起來很荒謬,但事實如此。如果一段代碼能讓你很容易瞭解其行爲,說明它還不是那麼迫切需要被重構。哪些最需要重構的代碼,你只能看到其中的壞味道,接着選擇對應的重構手法來消除這些壞味道,然後纔可能理解它的行爲。而這整個過程之所以可行,全賴你在腦子裏記錄着一份壞味道與重構手法對應表.

 

3.     記住所有的壞味道,記住它們對應的重構手法,記住常見的重構方法,然後你纔可能有信心面對各種複雜情況—學會所有招式,纔可能無招勝有招。我知道這聽起來很難,但我也知道這並不是你想象的那麼難,你所需要的只是耐心,毅力和不斷重讀這本書

 

4.     實際上,儘管我如此喜歡這本《重構》,但自從完成翻譯之後,就再也沒有讀過它,不,不是因爲我如此對它爛熟於心,而是因爲重構已經變成我的另外一種生活方式,變成我每天的麪包和黃油,變成我們整個團隊的空氣和水,以至於無需再到書中尋找任何神諭。而《設計模式》我倒是放在手邊時常翻閱,因爲總是記得不那麼真切.

 

5.     所謂重構是這樣一個過程:在不改變代碼外在行爲的前提下,對代碼做出修改,以改進程序內部結構。重構是一種經千錘百煉形成的有條不許的程序整理方法,可以最大限度減少整理過程中引入的錯誤機率。本質上說,重構就是在代碼寫好之後改進它的設計.

 

6.     你的態度也許傾向於儘量少修改程序:不管怎麼說,它還運行的很好。你心裏牢牢記住那句古老的工程諺語:如果它沒有壞,就不要動它。這個程序也許還沒有壞掉。但他造成了傷害。它讓你的生活如此難過,因爲你發現很難完成客戶所需要修改。這時候,重構技術就該登場。

 

7.     如果你發現自己需要爲程序添加一個特性,而代碼結構使你無法很方便達成目的,那就先重構那個程序,是特性的添加比較容易進行,然後再添加特性。

 

8.     重構的第一步:第一步永遠相同,我得爲即將修改的代碼建立一組可靠的測試環境。

 

9.     要知道,代碼塊越小,代碼的功能就更容易管理,代碼處理和移動也就更輕鬆.

 

10. 重構技術就是以微小的步伐修改程序。如果你犯下錯誤,就很容易便可以發現它.

 

11. 更改變量名稱是值得做的行爲嗎?絕對值得。好的代碼應該清楚的表達出自己的功能,變量名稱是代碼清晰的關鍵。如果爲了提高代碼的清晰度,需要修改某些東西的名字,那麼久大膽去做吧。

 

12. 任何一個傻瓜都能寫出計算機可以理解的程序,唯有寫出人類可以理解的代碼,纔是優秀的程序員.

 

13. 所有這些重構行爲都使責任的分配更加合理,代碼的維護更加輕鬆。重構後的程序風格,將迥異於過程化風格。

 

14. 這些例子給我們最大的啓發就是重構的節奏:測試,小修改,測試,小修改,測試,小修改。。。。。。正是這種節奏讓重構得以快速而安全地前進.

 

15. 何謂重構

<1>名詞:對軟件內部結構的一種調整。目的是在不改變軟件可觀察行爲的前提下,提高其可理解性,降低其修改成本。

<2>動詞:使用一系列重構手法,在不改變軟件軟件可觀察前提下,調整其結構。

 

16. 重構的目的是使軟件更容易的理解和修改.

 

17. 強調的第二點是:重構不會改變軟件可觀察行爲,重構之後軟件功能一如既往。任何用戶,不論最終用戶或其他程序員,都不知道已經有東西發生變化.

 

18. 兩頂帽子 :重構和添加新功能.

添加新功能時,你不應該修改既有代碼,只管添加功能。通過測試,你可以衡量自己的工作進度。重構時你就不能再添加功能,只管改進程序結構,你不應該添加任何測試(除非發現有先前遺漏的東西)。只在絕對必要時,在修改測試.

 

19 爲何重構

   <1>重構改進軟件設計

      如果消除重複代碼,你就可以確定所有事件和行爲在代碼中只表述一次,這正是優秀設計的根本.

   <2>重構使軟件更容易理解

   <3>重構幫助找到bug

   <4>重構提高編程速度

 

20 何時重構

   在我看來重構本來就不是一件應該特別撥出事件做的事情,重構應該隨時隨地進行

   <1>三次法則

      第一次做某件事情時只管去做;第二次做類似的事會產生反感,但是無論如何還是可以去做;第三次做類似的事,你就應該重構。

      事不過三,三則重構 

  <2>添加功能時重構

 <3>修改錯誤時重構.

 <4>複審代碼時重構

開始重構前我可以先閱讀代碼,得到一定程度的理解,並提出一些建議。一旦想到一些點子,我就會考慮是否可以通過重構立即輕鬆地實現它們,如果可以,我就會動手。這樣做了幾次以後,我就可以把代碼看的更清楚,提出更多恰當的建議。我不必想象代碼應該是什麼樣,我可以看見它是什麼樣。於是我可以獲得更高層次的認識。如果不進行重構,我永遠無法得到這樣的認識 。

 

21 系統當下的行爲,只是整個故事的一部分,如果沒有認清這一點,你無法長期從事編程工作。如果你爲完成今天的任務而不擇手段,導致不可能在明天完成的任務,那麼最終還是會失敗.

 

22 是什麼讓程序如此難相與

   <1>難以閱讀的程序,難以修改

   <2>邏輯重複的程序,難以修改

   <3>添加新行爲時需要修改已有代碼的程序,難以修改

   <4>帶複雜條件邏輯的程序,難以修改

   因此,我們希望程序:<1>容易閱讀<2>所有邏輯都只在唯一地點指定

   <2>新的修改不會危及現有行爲<4>儘可能簡單表達條件邏輯

 

23我的經驗告訴我,對於快速創造軟件,重構可帶來巨大幫助。如果需要添加新功能,而原本設計卻又使我無法方便地修改,我發現先重構在添加新功能會更快些。如果要修補錯誤,就得先理解軟件的工作方式,而我發現重構是理解軟件的最快方式。受進度驅動的經理要我儘可能快速完事,至於怎麼完成,那就是我的事。我認爲最快的方式就是重構,所以我就重構。

 

 

24間接層和重構

<1>計算機科學是這樣一門科學:它相信所有問題都可以通過增加一個間接層來解決.

<2>間接層的價值

   允許邏輯共享

   分開解釋意圖和實現

   隔離變化

   封裝條件邏輯

25重構的難題

<1>數據庫

<2>修改接口

   接口被修改,任何事情都可能發生

   接口一旦發佈,你就再也無法僅僅修改調用者而能夠安全地修改接口,你需要一個更復雜的流程

   簡言之,如果重構手法改變已發佈的接口,你必須同時維護新舊兩個接口,直到所有用戶都有時間對這個變化做出反應。幸運是,這不太困難。你通常都有辦法把事情組織好,讓舊接口繼續工作。請儘量這麼做:讓舊接口調用新接口。當你修改某個函數名稱時,請留下就函數,讓它調用新函數。千萬不要複製函數實現,那會讓你陷入重複代碼的泥潭中難以自拔。你應該使用java提供的deprecation設施,將舊接口標記爲它。

   還好我們有一個選擇:不要發佈接口

   所以除非必要,不要發佈接口。這可能意味需要改變你代碼所有權觀念,讓每個人都可以修改別人的代碼,以適應接口的改動。以結對編程的方式完成這一切通常是好主意。

 

26 何時不該重構

   記住,重構之前,代碼必須起碼能夠在大部分情況下正常運行

   另外,如果項目已經接近最後期限,你也應該避免重構。如果項目已經非常接近最後期限,你不應該在分心於重構,因爲已經沒有時間重構。不過多個項目經驗顯示:重構的確能夠提高生產力。如果最後你沒有足夠的時間重構,通常就表示你其實早就該重構

 

27重構與設計

  如果你選擇重構,問題的重點就轉變。你依然做預先設計,但是不必一定找到正確接近方案。此刻的你只需要得到一個足夠合理的解決方案就夠啦。你很肯定地知道,在實現這個初始解決方案的時候,你對問題的理解會逐漸加深,你可能會察覺最佳解決方案和你當初設想有些不同。只要有重構這把利器在手,就不成問題。因爲重構讓日後的修改成本不在高昂

 

28 哪怕你完全瞭解系統,也請實際度量它的性能,不要猜測,猜測會讓你學到一些東西,但十有八九你是錯的。

 

29 重構和性能

   除了對性能要求實時的系統,其他任何情況編寫快速軟件的祕密就是:首先寫出可調軟件,然後調整以求得足夠速度

 

30重構起源

   優秀程序員肯定至少會花一些實際清理自己的代碼。這麼做事因爲,它們知道簡潔的代碼比雜亂無章的代碼更容易修改,而且它們知道自己幾乎無法一開始就寫出簡潔的代碼

 

31你必須培養出自己的判斷力,學會判斷一個類內有多少實例變量算是太多,一個函數內有多少行代碼纔算太長。

 

32 重複代碼 Duplicated Code

<1>同一個類的兩個函數含有相同的表達式

<2>兩個互爲兄弟的子類內含有相同的表達式

<3>如果有些函數以不同的算法做相同的事,你可以選擇其中較清晰的一個.

<4>兩個毫不相干的類出現重複代碼

 

33過長函數 Long Method

<1>擁有短函數的對象會活得比較好,比較長

<2>間接層所能帶來的好處---解釋能力,共享能力,選擇能力—都是有小函數支持的

<3>如果你能給函數起個好名字,讀者就可以通過名字瞭解函數的作用,根本不必去看其中寫了什麼

<4>我們遵守這樣一條原則:每當感覺需要以註釋來說明點什麼的時候,我們就把需要說明的東西寫進一個獨立的函數中,並以其用途(而非實現手法)命名

<5>關鍵不在於函數的長度,而在於函數做什麼和如何做之間的語義距離

<6>如何確定該提煉那一段代碼了?尋找註釋.它們通常能指出代碼用途和實現手法之間的語義距離。如果代碼前方有一行註釋,就是提醒他你:可以將這段代碼替換成一個函數,而且可以在註釋的基礎上給這個函數命名。就算只是一行代碼,如果它需要以註釋來說明,那也值得將它提煉到獨立的函數

 

34 過大的類 Large Class

 

35 過長參數列 Long Parameter List

 

36 發散式變化 Divergent Change

<1>如果某個類經常因爲不同的原因在不同的方向上發生變化,爲此,你應該找出某特定原因而造成的所有變化,然後運用Extract Class將它們提煉到另一個類

 

37 Shotgun Surgery

<1>如果每遇到某種變化,都必須在許多不同的類做出許多小的修改

<2>發散式變化是指 一個類受多種變化影響,ShotgunSurgery 一種變化引發多個類相應的修改

 

38  依戀情結 Feature Envy

<1>函數對某個類興趣高於對自己所處類的興趣,這種通常是焦點便是數據

<2>我們原則:判斷哪個類擁有最多被此函數使用的數據,然後就把這個函數和那些數擺在一起。

 

39 數據泥團 Data Clumps

 

40 基本類型偏執 Primitive Obsession

 

41 Switch驚悚現身 Switch Statements

 

42 平行繼承體系 Parallel Inheritance hieraichies

 

43 冗餘類 Lazy Class

 

44 誇誇其淡未來性 Speculative Generality

<1>如果所有裝置都會被用到,那就值得那麼做;如果用不到,就不值得做。用不上的裝置只會擋你路,所以,把它搬開吧

45 令人迷惑的暫時字段 Temporary Field

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

 

46 過度耦合的消息鏈 Message Chains

 

47 中間人 Middle Man

 

48 InappropriateIntimacy

 

49 異曲同工得類

 

50 不完美的庫存

 

51 純稚的數據類 Data Class

 

52 被拒絕的遺贈 Refused Bequest

 

53 過多的註釋 Comments

 

54 構築測試體系

<1>如果你想進行重構,首要前提就是擁有一個可靠的測試環境

 

 

55 重新組織函數

   <1>對過長函數,一項重要的重構手法就是Extract Method,它把一段代碼從原先函數中提取出來,放進一個單獨的函數.Inline Method正好相反:將一個函數調用動作替換爲該函數體。

   <2>提煉函數最大的困難就是處理局部變量,而臨時變量則是其中一個主要的困難

   <3>參數帶來的問題比臨時變量稍微少一點,前提是你不在函數內賦值給它們.

 

56 Extract Method 提煉函數

   你有一段代碼可以被組織在一起並獨立出來

   將這段代碼放進一個獨立函數中,並讓函數名稱解釋該函數的用途.

   <1>當我看見一個過長的函數或者一段需要註釋才能讓人理解用途的代碼,我就會將這段代碼放進一個獨立的函數中。

   <2>首先,如果每個函數的粒度都很小,那麼函數被複用的機會就更大,其次,這會使高層函數讀起來就像一系列註釋;再次如果函數都是細粒度,那麼函數的覆蓋也會跟容易。

  <3>只有當你能給小型函數很好地命名時,它們才能真正起作用,所以你需要在函數名稱上下點功夫。

 <4>在我看來,長度不是問題關鍵,關鍵在於函數名稱和函數本體之間的語義距離.

<5>如果需要返回的變量不止一個,又該怎麼處理。

   有幾種選擇。最好的選擇通常是:挑選另外一塊代碼來提煉。我比較喜歡讓每個函數返回一個值,所以會安排多個函數,用以返回多個值。如果你使用的語言支持出餐數,可以使用它們帶回多個回傳值。但我還是儘可能選擇單一返回值。

 

57 Inline Method 內聯函數

  一個函數的本地與名稱同樣清楚易懂

  在函數調用點插入函數本體,然後移除該函數.

  <1>另外一種需要使用Inline Method的情況是:你手中有一羣組織不甚合理的函數。你可以將它們都內聯到一個大函數中,再從中提煉出組織合理的小函數。

 

58 Inline Temp 內聯臨時變量

  你有一個臨時變量,只被一個簡單表達式賦值一次,而它妨礙了其他重構手法.

  將所有對該變量的引用動作,替換爲對它賦值的那個表達式自身.

 

59 Replace Tempwith Query 以查詢取代臨時變量

   你的程序以一個臨時變量保存某一個表達式的運算結果.

   將這個表達式提煉到一個獨立函數中。將這個臨時變量的所有引用點替換爲對新函數的調用。以後,新函數就可以被其他函數使用.

 

60 Introduce ExplainingVariable 引入解釋性變量

  你有一個複雜的表達式

   將該複雜表達式(或其中一部分)的結果放到一個臨時變量,以此變量名稱來解釋表達式用途.

  <1>我比較喜歡使用Extract method,因爲同一個對象中的任何部分,都可以根據自己的需要取用這些提煉出來的函數。一開始我會把這些新函數聲明爲private;如果其他對象也需要他們,我可以輕易釋放這些函數的訪問權限。

  <2>那麼應該在什麼時候使用Introduce Explaining Variable 答案是:在Extract Method需要花費更大工作量時。如果我要處理的是一個擁有大量局部變量的算法,那麼使用Extract Method絕非易事。這種情況下就會使用使用引入解釋變量來清理代碼,然後再考慮下一步該怎麼辦

 

61Split TemporaryVariable 分解臨時變量

  你的程序有某個臨時變量被賦值超過一次,它既不是循環變量,也不被用於收集計算結果。

  針對每次賦值,創建一個獨立對應的臨時變量

 

62 RemoveAssignments to Parameters 移除對參數的賦值

   代碼對一個參數進行賦值。

   以一個臨時變量取代該參數的位置

   <1>在java重,不要對參數賦值;如果你看到手上的代碼已經這樣做了,請使用本重構手法

63Replace Methodwith Method Obect 以函數對象取代函數

  你有一個大型函數,其中對局部變量的使用使你無法採用Extract Method

  將這個函數放進一個獨立對象中,如此一來局部變量就成爲對象內的字段。然後你可以在同一個對象中將這個大型函數分解爲多個小型函數

 

64SubstitueAlgorithm 替換算法

  你想把某個算法替換爲另外一個更清晰的算法

  將函數本體替換爲另外一個算法

 

65在對象之間搬移特性

<1>在對象設計過程中,決定把責任放到哪兒即使不是最重要的事,也是最重要的事之一。我使用面向對象技術已經十幾年,但還是不能一開始就保證作對。這曾讓我很煩惱,但現在我知道,在這種情況下,可以運用重構,改變原先的設計.

<2>類往往會因爲承擔過多責任而變得臃腫,這樣情況下,我會使用Extract class將一個部分責任分離出去。如果一個類變得不太負責任,我就會使用Inline class將它融入另外一個類。如果一個類使用另外一個類。運用hide Delegate將這種關係隱藏起來。

 

 

66Move Method 搬移函數

  你的程序中,有個函數與其所住類之外的另外一個類進行更多交流;調用後再,或者被後者調用.

  在該函數最常引用的類中建立一個有着類似行爲的新函數,將舊函數變成一個單獨的委託函數,或是將舊函數完全移除.

 

67Move Field 搬移字段

  你的程序中,某個字段被其所駐類之外的另外一個類更多地用到.

  在目標類新建一個字段,修改源字段的所有用戶,令他們改用新字段.

 

 

68Extract Class 提煉類

  某個類做了應該有兩個類做的事。

  新建一個新類,將相關的字段和函數從舊類搬移到新類

  <1>你也許聽過類似這樣的教誨:一個類應該是一個清楚的抽象,處理一些明確的責任.

  <2>另一個往往在開發後期出現的信號是類的子類化方式.如果你發現子類化隻影響類的部分特性,或如果你發現某些特性需要以一種方式子類化,某些特性則需要以另外一種方式子類化,這就意味你需要分解原來類.

  <3>使用Move Method將必要函數搬移到新類。先搬移較底層函數(也就是被其他函數調用 多於 調用其他函數 者),在搬移較高層函數.

 

 

69Inline Class 將類內聯化

  某個類沒有做太多事情

 將這個類的所有特性搬移到另外一個類中,然後移除原類

 

 

70Hide Delegate 隱藏委託關係

  客戶通過一個委託類來調用另外一個對象

  在服務類上建立客戶所需要的所有函數,用以隱藏委託關係.

  如果某個客戶先通過服務對象的這段得到另一個對象,然後調用後者的函數,那麼客戶就必須知曉這一層委託關係。萬一委託關係發生變化,客戶也曉得相應變化。你可以在服務對象上放置一個簡單的委託函數,將委託關係隱藏起來,從而去除這種依賴。這麼一來,即便將來發生委託關係的變化,變化也將被限制在服務對象中,不會涉及客戶.

 

 

71Remove MiddleMan 移除中間人

  某個類做了過多簡單委託動作.

  讓客戶直接調用受拖類

  <1>我談到了 封裝受託對象 的好處,但是這層封裝也是要付出代價的,它的代價就是:每當客戶要使用受託類的心特性時,你就必須在服務端添加一個簡單委託函數。隨着受託類的特性越來越多,這一過程就會讓你痛苦不已.

  <2>重構的意思就在於:你永遠不必說對不起------只要把出問題的地方修補好就行啦.

 

 

72IntroduceForeign Method 引入外加函數

  你需要爲提供服務的類增加一個函數,但你無法修改這個類

  在客戶類中建立一個函數,並以第一參數形式傳入一個服務類實例

  <1>如果你以外加函數實現一項功能,那就是一個明確信號:這個函數原本應該提供服務類中實現

  <2>但是不要忘記:外加函數終歸是權益之計。如果有可能,你仍然應該將這些函數搬移到它們的理想家園。如果由於代碼所有權的原因使你無法做這樣的搬移,就把外加函數交給服務類擁有者,請他幫你在服務類中實現這個函數.

 

 

73Introduct LocalExtension 引入本地擴展

  你需要爲服務類提供一些額外函數,但你無法修改這個類.

  建立一個類,使它包含這些額外函數。讓這個擴展成爲源類的子類或包裝類.

  <1>如果只需要一兩個函數,你可以使用Introduce Foreign Method。但如果你需要額外函數超過兩個,外加函數就很那控制它們了.所以,你需要將這些函數組織在一起,放到一個恰當地方去。要達到這一目的,兩種標準對象技術----子類化和包裝

  <2>所謂本地擴展是一個獨立類,但也是被擴展類的子類型:它提供源類的一切特性,同時額外添加新特性。在任何使用源類的地方,你都可以使用本地擴展取而代之。

  <3>使用本地擴展使你得以堅持 函數和數據應該被統一封裝 的原則。如果你一直把本該放到擴展類中的代碼零散地放置於其他類中,最終只會讓其他這些類變得過分複雜,並使得其中函數難以被複用.

 <4>在子類和包裝類之間做出選擇時,我通常首選子類.

 

74.重新組織數據

有時你確實需要訪問函數,此時就可以使用Self Encapsulate Field得到他們,通常我會選擇直接訪問方式,因爲我發現,只要我想做,任何時候進行這項重構都很簡單

 

74 SelfEncapsulate Field 自封裝字段

   你直接訪問一個字段,但與字段之間的耦合關係逐漸變得笨拙.

   爲這個字段建立取值設置函數,並且只以這些函數來訪問字段.

   歸根結底,間接訪問變量的好處是,子類可以通過覆寫一個函數而改變獲取數據的途徑;它還支持更靈活的數據管理方式,例如延遲加載

   面臨選擇時,我會總是做兩手準備。通常情況我會很樂意團隊中的其他人意願來做。我比較喜歡先使用直接訪問方式,直到這種方式給我帶來麻煩爲止,此時我就會轉而使用間接模式。

   使用本項重構時,你必須小心對待在構造函數中使用設置函數的情況。一般說來,設置函數被認爲應該在對象創建後再使用,所以初始化過程中的行爲有可能與設置函數的行爲不同。這種情況下,我也許在構造函數中直接誒方爲字段,要不就是單獨另建一個初始化函數.

 

75 Replace DataValue with Object 以對象取代數據值

  你有一個數據項,需要與其他數據和行爲一起使用纔有意思

  將數據項變成對象

 

76 Change Value toReference 將值對象改爲引用對象

   你從一個類衍生出許多彼此相等的實例,希望將它們替換爲同一個對象

   將這個值對象變成引用對象

  

   在許多系統中,你都可以對對象做一個有用的分類:引用對象和值對象。前者就像客戶,賬戶這樣的東西,每個對象都代表真實世界中的一個實例。

 

77 ChangeReference To Value 將引用對象改爲值對象

  你有一個引用對象,很小且不可變,而且不易管理

  將它變成一個值對象

 

78 Replace Arraywith Object

  你有一個數組,其中的元素各自代表不同的東西.

  以對象替換數組。對於數組重的每個元素,以一個字段表示

 

79DuplicateObserved Data 複製被監聽數據

 

80ChangeUnidirectional Association to BiDirectional

將單向關聯改爲雙向關聯

  兩個類都需要使用對方特性,但其間只有一條單向連接

添加一個反向指針,並使修改函數能夠同時更新兩條連接.

 

81 ChangeBidirectional Association to Unidirectional

將雙向關聯改爲單向關聯

兩個類之間有雙向關聯,但其中一個類如今不在需要另外一個類的特性

 

82 Replace MagicNumber with Symbolic Constant

 以字面常量取代魔法數

  你有一個字面數值,帶有特殊含義

 創建一個產量,根據其意義爲它命名,並將上述的字面數值替換爲這個常量

 

83 EncapsulateField 封裝字段

  你的類中存在一個public字段

  將它聲明爲private,並提供相應的訪問函數

 

84EncapsulateCollection 封裝集合

  有個函數返回一個集合

讓這個函數返回該集合的一個只讀副本,並在這個類中提供添加/移除集合元素的函數

  集合的處理方式應該和其他種類的數據有些不同。取值函數不該返回結合自身,因爲這會讓用戶得以修改集合內容而集合擁有者卻一無所知.

  另外,不應該爲這整個集合提供一個設置函數,但應該提供用以爲集合添加、刪除元素的函數。這樣,集合擁有者就可以控制集合元素的添加和移除.

 

85 Replace Recordwith data class 以數據類取代記錄

  你需要面對傳統變成環境中的記錄結構.

  爲該記錄創建一個啞數據對象`

 

86 Replace TypeCode with Class 以類取代類型碼

  類之中有個數值類型碼,但它並不影響類的行爲

  以一個新的類替換該數值類型碼

 

  如果把那樣的數值換成一個類,編譯器就可以對這個類進行類型檢驗。只要爲這個類提供工程函數,你就可以始終保證只有合法的實例纔會被創建出來,而且它們都會被傳遞給正確的宿主對象

 

  但是,在使用Repalce Type Code with Class之前,你應該先考慮類型碼得其他替換方式,只有當類型碼是純粹數據時,也就是類型嗎不會再switch語句中引其行爲變化。你才能以類取代它.

 

87 Repalce TypeCode with subclasses 以子類取代類型碼

  你有一個不可變的類型碼,它會影響類的行爲

 以子類取代這個類型碼

 

 如果你面對的類型碼不會影響宿主類的行爲,可以使用Repalce Type Code with Class來處理它們。但如果類型嗎不影響宿主類的行爲,那麼最好的辦法就是藉助多態來處理變化行爲

 

 一般來說,這種情況的標誌就是像Switch這樣的表達式。這種條件表達式可能有兩種表現形式:switch語句或者if-then-else結構。不論哪種形式,它們都是檢查類型碼值,並根據不同的值執行不同的動作。

 

  爲建立這樣的繼承體系,最簡單的辦法就是Repalce Type Code With Subclasses,以類型碼得宿主類爲基類,針對每種類型碼建立相應的子類.

 

  但是以下兩種情況你不能那麼做:<1>類型碼在對象創建之後發生了改變<2>由於某些原因,類型碼主類已經有子類。如果你恰好面臨着兩種情況之一,就不要使用Repalce Type Code with State/Strategy

 

  Replace Type Code with subclasses的好處在於:它把對不同行爲的瞭解從類用戶哪兒轉移到類自身。如果需要加入新的行爲變化,只需要添加子類就行.

 

88Replace TypeCode with State/Strategy

你有一個類型碼,它會影響類的行爲,但你無法通過繼承手法消去它

 

如果類型碼得值在對象生命期中發生變化或其他原因使得宿主類不能被繼承.

 

89 RepalceSubclass with Fields 以字段取代子類

  你的各個子類的唯一差別只在返回常量數據的函數身上

  修改這個函數,使它們返回超類中的某個新增字段,然後銷燬子類.

 

90 簡化條件表達式

  

   較之於過程化程序而言,面向對象程序的條件表達式通常比較少,這是因爲很多條件行爲都被多態機制處理掉了。多態之所以更好,是因爲調用者無需瞭解條件行爲的細節,因此條件的擴張更爲容易.

 

91 分解條件表達式 Decompose Conditional

   你有一個複雜的條件 if-then-else語句

   從if,then,else三個段落中分別提煉出獨立函數

 

   像這樣的情況下,許多程序員都不會去提煉分支條件。因爲這些分支條件往往非常短,看上去似乎沒有提煉必要。但是,儘管這些條件往往很短,在代碼意圖和代碼自身之間往往存在不小的差距。哪怕在上面這樣一個小小的列子中,notSummar(date)這個語句也能夠比原本的代碼更好表達自身的用途。對於原來的代碼。我必須看着它,想一想,才能說出其作用。當然,在這個簡單的例子中,這並不是很難。不過,即使如此,提煉出來的函數可讀性也更高一些—它看上去就像一段註釋那樣清楚而明白

 

92 ConsolidateConditional Expression 合併條件表達式

  你有一系列條件測試,都得到相同結果

  將這些測試合併爲一個條件表達式,並將這些條件表達式提煉成爲一個獨立函數.

  有時候你會發現這樣一串條件檢查:檢查條件各不相同,最終行爲卻一致。如果發現這樣情況,就應該使用 邏輯或 和 邏輯與將它們合併爲一個條件表達式.

  首先,合併後的條件代碼會告訴你 實際上只有一次條件檢查,只不過多個並列條件需要檢查自己,從而使這一次檢查用意更加清晰

 

  條件語句的合併理由也同時指出不要合併的理由:如果你認爲這些檢查的確彼此獨立,的確不應該被視爲同一次檢查,那麼就不要使用本項重構.

 

93 ConsolidateDuplicate Conditional Fragments

  和並重復的條件片段

  在條件表達式的每個分支上有着相同的一段代碼。

  將這段重複代碼搬移到條件表達式之外

 

94Remove ControlFlag 移除控制標記

 在一系列布爾表達式中,某個變量帶有控制標記的作用

 以Break語句或者return語句取代控制標記

 

這就是編程語言提供Break語句和continue語句的原因:用它們跳出複雜的條件語句。去掉控制標記所產生的效果往往讓你大喫一驚:條件語句真正的用途會清晰得多.

 

95 Replace NestedConditional with Guard Clauses

  以衛語句取代嵌套的條件表達式

  函數中的條件邏輯使人難以看清楚正常的執行路徑.

  使用衛語句表現所有特殊情況.

 

  根據我的經驗,條件表達式通常有兩種表現形式。第一種形式是:所有分支都屬於正常行爲。第二種則是:條件表達式提供的答案中只有一種正常行爲,其他都是不正常行爲

 

  如果兩條分支都是正常行爲,就應該使用if…else…的條件表達式:如果某個條件極其罕見,就應該單獨檢查條件,並在該條件爲真時立刻從函數中返回。這樣的單獨檢查常常稱爲衛語句.

 

  Replace Nested Conditional with Guard Clauses的精髓就是:給某一條分支以特別的重視。如果使用if—then—else的結構,你對if分支和else分支的重視是同等的。這樣的代碼結構傳遞給閱讀者的消息就是:各個分支有同樣的重要性。衛語句就不同了,它告訴閱讀者:這種情況很罕見,如果它真地發生了,請做一些必要的整理工作,然後退出.

 

在我看來,保持代碼清晰纔是最關鍵的:如果單一出口能使這個函數更加清楚易讀,那麼就使用單一出口:否則就不必這麼做。

 

 

衛語句要不就從函數中返回,要不就拋出一個異常.

 

96 RepalceConditional with polymorphism

  以多態取代條件表達式

  你手上有個條件表達式,它根據對象類型的不同而選擇不同的行爲.

  將這個條件表達式的每個分支放進一個子類內的覆寫函數中,然後將原始函數聲明爲抽象函數.

 

  多態最更本的好處就是:如果你需要根據對象的不同類型而採取不同的行爲,多態使你不必編寫明顯得條件表達式

 

97 引入Null對象Introduce Null Object

   你需要再三檢查某對象是否爲null

   將null值替換爲null對象

 

   多態的最根本好處在於:你不必要向對象詢問 你是什麼類型 而後根據得到的答案調用對象的某個行爲------你只管調用該行爲就是了,其他的一切多態機制會爲你安排妥當.

 

  請記住:空對象一定是常量,它們的任何成分都不會發生變化,因此women你可以使用Singleton模式來實現它,例如不管任何時候,只要你索求一個空對象,得到的一定是空對象的唯一實例

 

請記住:只有當大多數客戶代碼都要求空對象做出相同響應時,這樣的行爲搬移纔有意義。注意,我說的是大多數而不是所有

 

你經常可以在表示數量的類中看到這樣的特殊類。例如java浮點數有正無窮大和負無窮大和非數量等特列。特列類的價值是:它們可以降低你的錯誤處理開銷,例如浮點運算決不會拋出異常。如果你對NAN做浮點運算,結果也會是NAN。這和空對象的訪問函數通常返回另外一個空對象是一樣的道理.

 

 

98 introduceAssertion

 

98 introduceAssertion引入斷言

   某一段代碼需要對程序狀態做出某種假設.

   以斷言明確表現這種假設.

 

  常常會有這樣一段代碼:只有當某個條件爲真時,該段代碼才能正常運行。

  這樣的假設通常並沒有在代碼中明確表現出來,你必須閱讀整個算法纔看出。有時程序員會以註釋寫出這樣的假設。而臥要介紹的是一種更好的技術:使用斷言明確這些假設.

 

  注意,不要亂用斷言,請不要使用它來檢查 你認爲應該爲真 的條件,請只使用它檢查 一定必須爲真 的條件.

 

 但是,更多時間,斷言的價值在於:幫助程序員理解代碼正確運行的必要條件.

 

 

 

99 簡化函數調用

   多年來,我一直堅守一個很有價值的習慣:明確地將”修改對象狀態”的函數和查詢對象狀態的函數分開設計

 

   良好的接口只向用戶展示不要展示的東西。如果一個接口暴露過多細節,你可以將不必要暴露的東西隱藏起來,從而改進接口的質量。毫無疑問,所有數據都應該隱藏起來(希望你不需要我來告訴你這一點),同時,所有可以隱藏的函數都應該被隱藏起來.

 

100 函數改名 Rename Method

函數的名稱未能揭示函數的用途.

修改函數的名稱

 

 我極力提倡一種編程風格就是:將複雜的處理過程分解成小函數。但是,如果做得不好,這會使你費盡周折卻弄不清楚這些小函數各自的用途。

 

 給函數命名有一個好辦法:首先考慮應該給這個函數寫上一句怎樣的註釋,然後想辦法將註釋變成函數名稱

 

 生活就是如此。你常常無法第一次就給函數起一個好名稱。這時候你可能會想:就這樣將就吧,畢竟只是一個名字而已。當心!這是惡魔的召喚,是通向混亂之路,千萬不要被它誘惑!如果你看到一個函數名稱不能很好的表達它的用途,應該馬上加以修改。記住,你的代碼首先是爲人寫的,其次纔是爲計算機寫的。而人需要良好的名稱函數.

 

  要想成爲一個真正的變成高手,起名的水平至關重要.

 

 

101 Add Parameter 添加參數

   將某個函數添加一個對象參數,讓該對象帶進函數所需要信息

 

  實際上我比較需要說明的是:不使用本項重構的時機。除了添加參數外,你常常還有其他選擇。只要可能,其他選擇都比添加參數要好。因爲他們不會添加參數列表的長度.

 

102 Remove Parameter 移除參數

   函數本體不在需要某個參數

   將參數去除

   程序員可能進程添加參數,卻往往不願意去掉他們。他們打地如意算盤是,無論怎樣,多餘的參數不會引起任何問題,而且以後還可能用上它

 

  參數代表函數所需要的信息,不同的參數數值有不同的意義。函數調用者必須爲每一個參數操心該傳什麼東西進去。如果你不去掉多餘參數,就是讓你的每一位用戶多費一份心

 

103Separate Query from Modifier

   將查詢函數和修改函數分離

   某個函數既返回對象狀態值,又修改對象狀態.

   建立兩個不同的函數,其中一個負責查詢,另外一個負責修改

 

   任何有返回值的函數,都不應該看得到副作用。有些程序員甚至將此作爲一條必須遵守的規則。就像對任何東西一樣,我並不是絕對遵守它,不過我總是儘量遵守,而它也回報我很好的效果.

 

  如果你遇到一個 既返回值有又副作用的函數,就應該試着將查詢動作從修改動作重分割出來.

 

104Parameterize Method 令函數攜帶參數

  若干函數做了類似工作,但在函數本體重卻包含了不同的值.

  建立單一函數,以參數表達哪些不同的值.

 

  你可能會發現這樣的兩個函數:它們做着類似的工作,但因爲少數幾個值導致行爲略有不同。這種情況下,你可以將各自分離的函數統一起來,並通過參數來處理哪些變化情況,用以簡化問題。這樣的修改可以去除重複代碼,並提高靈活性,因爲你可以用這個參數處理更多的變化情況.

 

105 Replace Parameter with explicit Methods

   以明確函數取代參數

   你有一個函數,其中完全取決於參數值而採取不同行爲.

  

   計算不考慮編譯期檢查的好處,只是誒了獲得一個清晰的藉口,也值得你執行本項重構。哪怕只是給一個內部的布爾變量值,相比之下,Switch.beOn也比Switch.setState(true)要清楚的多.

 

106 Preserve Whole Object 保持對象的完整

  你從某個對象中取出若干值,將它們作爲某一個詞函數調用時的參數.

  改爲傳遞整個對象

 

  如果被調用函數使用了來自另外一個對象的很多數據,這可能意味該函數實際上應該被定義在那些數據所屬的對象中。所以,考慮Preserve Whole Obejct的同時,你應該考慮Move Method

 

107 Repalce Parameter with methods 以函數取代參數

   對象調用某個函數,並將所得結果作爲參數,傳遞給另外一個函數。而接受該參數的函數本身也能夠調用前一個函數

   讓參數接受者去除該項參數,並直接調用函數.

 

  如果函數可以通過其他途徑獲得參數值,那麼它就不應該通過參數獲得該值。過長的參數列表會增加程序閱讀者理解難度,因此我們應該儘可能縮短參數長度.

 

108 Introduce Parameter Object 引入參數對象

   某些參數總是很自然同時出現

   以一個對象取代這些參數

 

   我已經記不清楚多少次看到代碼用某一個值表示一個範圍,例如表示日期範圍的start和end,表示數值範圍的upper和lower,等等。我知道爲什麼會發生這樣情況。畢竟我自己也經常這樣做。不過,自從血洗Range模式之後,我就儘量以範圍對象取而代之.

 

109 Remove SettingMethod 移除設置函數

   類中的某個字段應該在對象創建的時候被設置,然後就不再改變

   去掉該字段的所有設置函數.

 

110 Hide Method 隱藏函數

   有一個函數,從來沒有被其他任何類用到

   將這個函數修改爲private

 

   重構玩玩促使你修改函數的可見度。提高函數可見度的情況很容易想象;另一個類需要用到某個函數,因此你必須提高函數的可見度。

 

111Replace Constructorwith Factory Method

   以工廠函數取代構造函數

   你希望在創建對象時不僅僅是做簡單的構建動作.

   將構成函數替換爲工廠函數.

 

112 EncapsulateDowncast 封裝向下轉型

   某個函數返回的對象,需要由函數調用者執行向下轉型

   將向下轉型動作移到函數中.

113Replace ErrorCode with Exception

   Code with Exception

   以異常取代錯誤碼

   某函數返回一個特定的代碼,用以表示某種錯誤情況

   改用異常 

 

   如果程序崩潰代價很小,用戶又足以寬容,那麼就放心終止程序的運行就好啦。但如果你的程序比較重要,就需要以更認真的方式處理.

   問題在於:程序中發現的錯誤的地方,並不一定知道如何處理錯誤。當一段子程序發現錯誤時,它需要讓它的調用者知道這個錯誤,而調用者也可能將這個錯誤繼續沿着鏈傳遞上去。許多程序都是用特殊輸出表示錯誤。

   Java有一種更好的錯誤處理方式:異常。這種方式之所以更好。因爲它清楚地將普通程序和錯誤處理分開了,這使得程序更容易理解===我希望如今已經堅信:代碼的可理解性應該是我們虔誠追求的目標.

  決定是拋出受控異常還是非受控異常

  <1>如果調用者有責任在調用前檢查必要狀態,就拋出非受控異常

  <2>如果想拋出受控異常,你就可以新建一個異常類,也可以使用現有的異常類。

 

  爲了使這段代碼使用異常,我首先需要決定使用是受控異常還是非受控異常。決定的關鍵在於:調用者是否有責任在取款之前檢查餘額,還是應該有withdraw()函數負責檢查。如果檢查餘額是調用者得責任,那麼取款金額大於存款金額就是一個編程錯誤。由於這是一個編程錯誤,所以我們應該使用非受控異常。另一方面,如果檢查餘額是withdraw()函數的責任,我就必須在函數接口中聲明它可能拋出這個異常,那麼也就提醒調用者注意這個異常,並採取措施.

 

114 ReplaceExeption with Test

   以測試取代異常

   修改調用者,使它在調用函數之前先做檢查

   異常只應該被用於異常的,罕見的行爲,也就是哪些意料之外的錯誤行爲,而不應該成爲條件檢查的替代品.

 

 

115處理概括關係

   概括關係既繼承關係

 

116 Pull Up Field 字段上移

   連個子類擁有相同的字段

   將該字段移動到超類

 

   判斷若干字段是否重複,唯一的辦法就是觀察函數如何使用它們,如果它們被使用的方式很相似,你就可以將它們歸納到超類去。

 

  本項重構從兩方面減少重複:首先它除去了重複的數據聲明;其次它使你可以將使用該字段的行爲從子類移動到超類,從而去除重複行爲.

 

117 Pull Up Method函數上移

   有些函數,在各個子類中產生完全相同的結果.

   將該函數移動到父類

 

   如果某個函數在各個子類中的函數體都相同。

   有一種特殊情況也需要使用Pull Up Method:子類的函數覆蓋了超類的函數,但卻仍然做相同的工作。

 

118 Pull upConstructor Body

   構造函數本體上移

   你在各個子類中擁有一些構造函數,它們的本地幾乎完全一樣.

   在超類中新建一個構造函數,並在子類的構造函數中調用它

 

119 Push DownMethod 函數下移

   超類中的某個函數只與部分(而非全部)子類有關.

   將這個函數移動到相關的那些子類去.

 

120Push Down Field字段下移

   超類中的某個字段只被部分(而非全部)子類用到.

   將這個字段移動到需要它的那些子類去.

 

121ExtractSubclass 提煉子類

  類中的某些特性只被某些(而非全部)實例用到。

  新建一個子類,將上面所說的那一部分特性移動到子類中。

122ExtractSuperclass 提煉超類

   兩個類有相似特性.

   爲這兩個類建立一個超類,將相同特性移動到超類

 

   重複代碼時系統中最糟糕的東西之一。如果你在不同地方做同樣一件事情,一旦需要修改那些動作,你就得平白做更多的修改.

 

123ExtractInterface 提煉接口

   若干客戶使用類接口中的同一子集,或者兩個類的接口有部分相同.

   將相同的子集提煉到一個獨立接口中.

   類之間彼此互相作用的方式有若干種。使用一個類通常意味用到該類中的所有責任。另一種情況,某一組客戶只使用類責任區中的一個特點子集。在一種情況則是,這個類需要與所有協助處理某特定請求的類合作.

   對於後兩種情況,將真正用到這部分責任分離出來通常很有意義.因爲這樣可以使系統的用法更清晰,同時也更容易看清系統的責任劃分.

   如果某個類在不同環境下扮演截然不同的角色,使用接口就是一個好主意。你可以針對每個角色以Extract Interface出相應的接口.另一種可以用上Extract Interface的情況是:你想要描述一個類的外部依賴接口(既這個類要求服務提供方提供的操作)。如果你打算將來加入其它種類的服務對象,只需要要求他們實現這個接口即可.

 

124CollapseHierarchy 摺疊繼承體系

   超類和子類之間無太大區別.

   將它們合爲一體.

 

125Form TemPlateMethod 塑造模板函數

 

126Repalceinheritance with Delegation

   以委託取代繼承

   某個子類只使用超類接口中的一部分,或是根本不需要繼承而來的數據

   在子類重新建一個字段用以保存超類;調整子類函數,令它改而委託超類;然後去掉兩者之間的繼承關係.

 

  你常常會遇到這樣的情況:一開始繼承了一個類,隨後發現超類中的許多操作並不真正適用於這個子類。這種情況下,你所擁有的接口並未真正反映出子類的功能。或者,你可能發現你從超類繼承了一大堆子類並不需要的數據,或你可能發現超類中的某些protected函數對子類並沒有什麼意義.

 

  你可以容忍,並接受傳統說法:子類可以只使用超類功能的一部分。

 

  如果以委託取代繼承,你可以更清楚的表示:你只需要受託類的一部分功能。接口中的那一部分應該被使用,那一部分應該被忽略,完全由你主導控制.

 

127Replacedelegation with inheritance

   以繼承取代委託

   你在兩個類之間使用委託關係,並經常爲整個接口編寫許多極簡單的委託關係.

   讓委託類繼承受託類.

   兩條告誡要記在心中。首先,如果你並沒有使用受託類的所有函數,那麼就不應該使用Repalce Delegation with inheritance,因爲子類應該總是遵守超類的接口。如果過多的委託函數讓你煩心,你有別的選擇:你可以通過remove Middle Man讓客戶端調用受託函數,也可以使用Extract Superclass將兩個類接口相同的部分提煉到超類中,然後讓兩個子類都繼承這個新的超類,你還可以用類似的手法Extract Interface

 

128Tease ApartInheritance 梳理並分解繼承體系

   某個繼承體系同時承擔兩項責任.

   建立連個繼承體系,並通過委託關係讓其中一個可以調用另一個.

 

129ConvertProcedural Design to Objects

   你手上有一些傳統過程化風格的代碼

   將數據記錄變成對象,將大塊的行爲分成小塊,並將行爲移到相關對象中.

130 ExtractHierarchy 提煉繼承體系

   你有某個類做了太多工作,其中一部分工作是以大量條件表達式完成的。

   建立繼承體系,以一個子類表示一種特殊情況.

 

   當你遇到這種瑞士軍刀般得類—不但能夠開瓶開罐,砍小樹枝,還能在演示會上打出激光強調重點=你就需要一個好策略。將它們各個功能梳理並分開.

 

總結:

這些技術如此精彩,可它們卻僅僅是個開始,這是爲什麼?答案很簡單:因爲你還不知道何時使用它們,何時不應該使用;何時開始,何時停止;何時前進,何時等待。使重構能夠成功,不是前面各自獨立的技術,而是這種節奏。

 

你又是如何得知什麼時候才真正懂得這一切?正是當你開始冷靜下來的時候,對自己的重構技藝感到絕對自信—不論別人留下的代碼多麼雜亂無章,你都可以將它處理變好,好到足以進行後續開發—那是你就知道,自己已經得到。

 

  不過,大多時候,得道德標誌是:你可以自信地停止重構。在重構者得整個表演中,停止正是壓軸大戲。一開始爲自己選擇一個大目標,例如:去掉一堆不必要的子類。然後你開始向着這個目標前進,每一步都走得小而堅定,每一步都有備份,保證能夠回頭,好的,你離目錄愈來愈進,現在只剩下兩個函數合併,然後就大功告成。

 

  就在此時,意想不到的事情發現啦:你再也無法前進一步。也許是因爲時間太短,你太疲倦;也許是因爲一開始你的判斷就出錯,實際上不可能去掉所有子類;也許是因爲沒有足夠的測試來支持你。總而言之,你的自信灰飛因滅,你無法再自信滿滿地胯下下一步。你認爲自己應該沒把任何東西搞亂,但也無法確定。

 

 這是該停下來的時候了。如果代碼以及比重構之前好,那麼就把它集成到系統中,發佈你的成果。如果代碼並沒有變好,就果斷放棄這些無用的工作,回到起始點。

 

 當你真正理解重構之後,系統的這個設計對你來說,就像源代碼中的字符那樣隨心所欲操控。你可以直接感受整個設計,可以清楚看到如何將設計變得更靈活,也可以看到如何修改它;這裏修改一點,於是這樣表現.

 

  應該如何學習?

<1>隨時挑一個目標

   某個地方的代碼開始發臭了,你就應該將問題解決掉。你應該朝目標前進,達成目標後就停止。你之所以重構,不是爲了探索真善美(至少不全是)而是爲了讓你的系統更容易被人理解,爲了防止程序變亂.

<2>沒有把握就停下來
<3>學習原路返回

<4>二重奏.

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