《重構--改善既有程式碼的設計》閱讀筆記
摘要:
如果你想知道重構是什麼,請閱讀第1章,其中示例會讓你清楚重構過程。
如果你想知道為什麼應該重構,請閱讀前兩章。它們告訴你「重構是什麼」以及「為什麼應該重構」。
如果你想知道該在什麼地方重構,請閱讀第3章。它會告訴你一些程式碼特徵,這些特徵指出「這裡需要重構」。
...
- 如果你想知道重構是什麼,請閱讀第1章,其中示例會讓你清楚重構過程。
- 如果你想知道為什麼應該重構,請閱讀前兩章。它們告訴你「重構是什麼」以及「為什麼應該重構」。
- 如果你想知道該在什麼地方重構,請閱讀第3章。它會告訴你一些程式碼特徵,這些特徵指出「這裡需要重構」。
- 如果你想真正(實際〉進行重構,請完整閱讀前四章,然後選擇性地閱讀重構名錄(refactoring catalog)。一開始只需概略瀏覽名錄,看看其中有些什麼,不必理解所有細節。一旦真正需要實施某個準則,再詳細閱讀它,讓它來幫助你。名錄是一種具備查詢價值的章節,你也許並不想一次把它全部讀完。此外你還應該讀一讀名錄之後的「客串章節」,特別是第15章。
2 重構原則
2.1 為何重構
- 重構改進軟體設計
- 重構使軟體更容易理解
- 重構幫助找到bug
- 重構提高程式設計速度
2.2 何時重構
2.2.1 三次法則
- Don Roberts給了我一條準則:第一次做某件事時只管去做;第二次做類似的事會產生反感,但無論如何還是可以去做;第三次再做類似的事,你就應該重構。
2.2.2 新增功能時重構
- 當原程式碼的設計無法幫助我輕鬆新增我所需的特性時,重構是一個很好的選擇。一旦完成重構,新特性的新增就會更快速、更流暢。
2.2.3 修補錯誤時重構
- 如果收到一份錯誤報告,這就是需要重構的訊號,因為顯然程式碼還不夠清晰——沒有清晰到讓你能一眼看出bug。
2.2.4 複審程式碼時重構
2.3 為什麼重構有用
是什麼讓程式如此難以相與?眼下我能想起下述四個原因,它們是:
- 難以閱讀的程式,難以修改;
- 邏輯重複的程式,難以修改;
- 新增新行為時需要修改已有程式碼的程式,難以修改;
- 帶複雜條件邏輯的程式,難以修改。
因此,我們希望程式:
- 容易閱讀;
- 所有邏輯都只在唯一地點指定;
- 新的改動不會危及現有行為;
- 儘可能簡單表達條件邏輯.
重構是這樣一個過程:它在一個目前可執行的程式上進行,在不改變程式行為的前提下使其具備。上述美好性質,使我們能夠繼續保持高速開發,從而增加程式的價值。
2.4 重構的難題
2.4.1 資料庫
- 重構經常出問題的一個領域就是資料庫。絕大多數商用程式都與它們背後的資料庫結構緊密耦合在一起,這也是資料庫結構如此難以修改的原因之一。
2.4.2 修改介面
- 簡言之,如果重構手法改變了已釋出介面,你必須同時維護新舊兩個介面,直到所有使用者都有時間對這個變化做出反應。幸運的是,這不太困難。你通常都有辦法把事情組織好,讓舊介面繼續工作。請儘量這麼做:讓舊介面呼叫新介面。當你要修改某個函式名稱時,請留下舊函式,讓它呼叫新函式。千萬不要複製函式實現,那會讓你陷入重複程式碼的泥淖中難以自拔。
- 釋出介面很有用,但也有代價。所以除非真有必要,不要釋出介面。這可能意味需要改變你的程式碼所有權觀念,讓每個人都可以修改別人的程式碼,以適應介面的改動。以結對程式設計的方式完成這一切通常是個好主意。
- tips:不要過早釋出介面。請修改你的程式碼所有權政策,使重構更順暢。
2.4.3 難以通過重構手法完成的設計改動
- 遇到難以重構的情況下我的辦法就是:先想象重構的情況。考慮候選設計方案時,我會問自己:將某個設計重構為另一個設計的難度有多大?如果看上去很簡單,我就不必太擔心選擇是否得當,於是我就會選最簡單的設計,哪怕它不能覆蓋所有潛在需求也沒關係。但如果預先看不到簡單的重構辦法,我就會在設計上投入更多力氣。不過我發現,後一種情況很少出現。
2.4.4 何時不該重構
- 有時候你根本不應該重構,例如當你應該重新編寫所有程式碼的時候。有時候既有程式碼實在太混亂,重構它還不如重新寫一個來得簡單。作出這種決定很困難,我承認我也沒有什麼好準則可以判斷何時應該放棄重構。
- 重寫(而非重構)的一個清楚訊號就是:現有程式碼根本不能正常運作。你可能只是試著做點測試,然後就發現程式碼中滿是錯誤,根本無法穩定運作。記住,重構之前,程式碼必須起碼能夠在大部分情況下正常運作。
- 一個折中辦法就是:將“大塊頭軟體”重構為封裝良好的小型元件。然後你就可以逐一對元件做出“重構或重建”的決定。這是一個頗有希望的辦法,但我還沒有足夠資料,所以也無法寫出好的指導原則。對於一個重要的遺留系統,這肯定會是一個很好的方向。
- 另外,如果專案已近最後期限,你也應該避免重構。在此時機,從重構過程贏得的生產力只有在最後期限過後才能體現出來,而那個時候已經為時晚矣。如果最後你沒有足夠時間,通常就表示你其實早該進行重構。
2.5 程式碼的壞味道
2.5.1 Duplicated Code (重複程式碼)
- 壞味道行列中首當其衝的就是DuplicatedCode。如果你在一個以上的地點看到相同的程式結構,那麼可以肯定:設法將它們合而為一,程式會變得更好。
2.5.2 Long Method (過長函式)
- 擁有短函式的物件會活得比較好、比較長。
- 最終的效果是:你應該更積極地分解函式。我們遵循這樣一條 原則:每當感覺需要以註釋來說明點什麼的時候,我們就把需要說明的東西寫進一個獨立函式中,並以其用途(而非實現手法)命名。
- 如何確定該提煉哪段程式碼呢?一個很好的技巧是:尋找註釋。它們通常能指出程式碼用途和實現手法之間的語義距離。如果程式碼前方有一行註釋,就是在提醒你:可以將這段程式碼替換成一個函式,而且可以在註釋的基礎上給這個函式命名。就算只有一行程式碼,如果它需要以註釋來說明,那也值得將它提煉到獨立函式去。
- 條件表示式和迴圈常常也是提煉的訊號。你可以使用Decompose Conditional (238)處理條件表示式。至於迴圈,你應該將迴圈和其內的程式碼提煉到一個獨立函式中。
2.5.3 Large Class (過大的類)
- 如果想利用單個類做太多事情,其內往往就會出現太多例項變數。一旦如此,Duplicated Code也就接踵而至了。
2.5.4 Long Parameter List (過長引數列)
- 太長的引數列難以理解,太多引數會造成前後不一致、不易使用,而且一旦你需要更多資料,就不得不修改它。如果將物件傳遞給函式,大多數修改都將沒有必要,因為你很可能只需(在函式內)增加一兩條請求,就能得到更多資料。
2.5.5 Divergent Change (發散式變化)
2.5.6 Shotgun Surgery(霰彈式修改)
- Divergent Change是指“一個類受多種變化的影響”, Shotgun Surgery則是指“一種變化引發多個類相應修改”。這兩種情況下你都會希望整理程式碼,使“外界變化”與“需要修改的類”趨於一一對應。
2.5.7 Feature Envy (依戀情結)
- 將總是一起變化的東西放在一塊兒。資料和引用這些資料的行為總是一起變化的,但也有例外。如果例外出現,我們就搬移那些行為,保持變化只在一地發生。Strategy和Visitor使你得以輕鬆修改函式行為,因為它們將少量需被覆寫的行為隔離開來一當然也付出了“多一層間接性”的代價。
2.5.8 Data Clumps (資料泥團)
- 一個好的評判辦法是:刪掉眾多資料中的一項。這麼做,其他資料有沒有因而失去意義?如果它們不再有意義,這就是個明確訊號:你應該為它們產生一個新物件。
2.5.9 Comments (過多的註釋)
- 你看到一段程式碼有著長長的註釋,然後發現,這些註釋之所以存在乃是因為程式碼很糟糕。這種情況的發生次數之多,實在令人吃驚。
- 當你感覺需要撰寫註釋時,請先嚐試重構,試著讓所有註釋都變得多餘。
- 如果你不知道該做什麼,這才是註釋的良好運用時機。除了用來記述將來的打算之外,註釋還可以用來標記你並無十足把握的區域。你可以在註釋裡寫下自己“為什麼做某某事”。這類資訊可以幫助將來的修改者,尤其是那些健忘的傢伙。
2.6 重構手法
2.6.1 Extract Method (提煉函式)
- 創造一個新函式,根據這個函式的意圖來對它命名(以它“做什麼”來命名,而不是以它“怎樣做”命名)。即使你想要提煉的程式碼非常簡單,例如只是一條訊息或一個函式呼叫,只要新函式的名稱能夠以更好方式昭示程式碼意圖,你也應該提煉它。但如果你想不出一個更有意義的名稱,就別動。
- 將提煉出的程式碼從源函式複製到新建的目標函式中。
- 仔細檢查提煉出的程式碼,看看其中是否引用了“作用域限於源函式”的變數(包括區域性變數和源函式引數)。
- 檢查是否有“僅用於被提煉程式碼段”的臨時變數。如果有,在目標函式中將它們宣告為臨時變數。
- 檢查被提煉程式碼段,看看是否有任何區域性變數的值被它改變。如果一個臨時變數值被修改了,看看是否可以將被提煉程式碼段處理為一個查詢,並將結果賦值給相關變數。如果很難這樣做,或如果被修改的變數不止一個,你就不能僅僅將這段程式碼原封不動地提煉出來。你可能需要先使用Split Temporary Variable (128),然後再嘗試煉。也可以使用Replace Temp with Query (120)將臨時變數消滅掉(請看“範例”中的討論)。
- 將被提煉程式碼段中需要讀取的區域性變數,當作引數傳給目標函式。
- 處理完所有區域性變數之後,進行編譯。
- 在源函式中,將被提煉程式碼段替換為對目標函式的呼叫。如果你將任何臨時變數移到目標函式中,請檢查它們原本的宣告式是否在被提煉程式碼段的外圍。如果是,現在你可以刪除這些宣告式了。
- 編譯,測試。
2.6.2 Inline Method (行內函數)
- 檢查函式,確定它不具多型性。如果子類繼承了這個函式,就不要將此函* 數內聯,因為子類無法覆寫一個,根本不存在的函式。
- 找出這個函式的所有被呼叫點。
- 將這個函式的所有被呼叫點都替換為函式本體。
- 編譯,測試。
- 刪除該函式的定義。
- 被我這樣一寫, Inline Method (117)似乎很簡單。但情況往往並非如此。對於遞迴呼叫、多返回點、內聯至另一個物件中而該物件並無提供訪問函式....每一種情況我都可以寫上好幾頁。我之所以不寫這些特殊情況,原因很簡單:如果你遇到了這樣的複雜情況,那麼就不應該使用這個重構手法。