代碼重構新手教程:如何將爛代碼變成好代碼?
作者 | 王莉敏
作爲有幾年工作經驗的程序員,都會對 bad code 不滿意。如何將爛代碼變成好代碼,本文將由淺入深、一步步帶你理解重構的奧祕,讓你對重構有個基本的瞭解。本文基於文章《The Simple Ways to Refactor Terrible Code》編譯整理而成。
-
擔心破壞已有代碼。這種情況在覈心業務系統尤爲普遍,比如電商平臺,企業的 ERP 平臺等,系統需要 7*24 運行,你的一個修改可能導致 10000 元的商品被 1 元錢買走了。作爲普通人的我們,自然會抱着多一事不如少一事的心理,畢竟出了問題,會吃不了兜着走。
-
不能立即看到產出。在每日的 stand-up 會議中,當研發經理問我,你今天干什麼了,我說重構代碼,如果連續三天都是這個答案,估計研發經理就要發飆了。
-
沒有時間去做。面對繁重的研發任務和日益逼近的 deadline,重構,真的不是一場說走就走的旅行。
說了這麼多,讀者朋友可能會有一個想法,是否有一些方法,能讓我享受重構的好處,又能避免上面提到的風險。
幸運的是還真有!
下面我將從最簡單、基本不會破壞已有代碼、花費很少時間的重構方法入手,逐步深入,讓大家對重構有一個基本瞭解,在對方法的介紹中,我將按照《InfoQ 編程語言 2 月排行榜結果出爐》中的調研情況,選取用戶掌握最多的編程語言 Java,以及該語言使用最多的 IDE 環境 Eclipse,進行舉例。
爲了消除恐懼,讓我們從最簡單的重構方法入手
當你發現代碼縮進層次不齊,代碼塊中缺少 {} 等問題時,就需要考慮代碼格式化了,現在的 IDE 工具已經對格式化提供了很好的支持,以 eclipse 爲例,選中要格式化的代碼,點擊以下菜單項就能完成代碼格式化。
此外很多源代碼管理網站,也提供了格式化工具,如圖所示:
在團隊開發中,爲了保證開發代碼樣式統一,需要建立編碼規範。我們並不需要重頭建立編碼規範,可以在大廠的編碼規範基礎上進行定製,比如在 Java 領域可採用阿里、華爲、Google Java Style Guide 等編碼規範。該編碼規範可以與 IDE 進行結合,如在 eclipse 中,打開 Window->Preferences->Java->Code Style 導入編碼規範:
重要的是,不管選擇何種規範,要堅持下去,並讓每個團隊成員都用起來。
在代碼開發中,好的註釋可以提高程序的可讀性,壞的註釋可能會畫蛇添足,甚至起反作用。作者提到好的註釋要做到和代碼相關、及時更新。很多時候,代碼剛開始編寫時,註釋和代碼是一致,後期因爲間隔時間過長或其他人接手修改代碼,沒有對註釋及時修改,就會造成註釋和代碼漸行漸遠。
儘量減少不必要的註釋。如很多函數或者類,如果設計架構清晰,通過命名就能知道他們做什麼,註釋不是必須的。還有一種情況是暫時不用的代碼,很多人會覺得以後會用到,會加個註釋,作者給出的建議是刪掉它,如果你將來真的用到了,可以到 git(一種代碼管理工具)的歷史記錄中查找。
對於邏輯混亂的代碼,如在循環中隨意使用 break,複雜的 if 語句嵌套等,你要做的是理清邏輯,重構代碼,而不是讓註釋替你補鍋。正如《重構》中提到的 “當你感覺需要撰寫註釋,請先嚐試重構,試着讓所有註釋都變得多餘。”
隨着系統版本的不斷迭代,有一些函數或類不再使用後,我們應該將它及時刪除,否則隨着時間流逝,會造成代碼庫臃腫,程序可讀性變差。而且如果還發生人員的變動,慢慢會成爲誰也不敢動的代碼,因爲都不知道有啥用和在哪用到。
多先進的 IDE 工具都對查找代碼的調用提供了支持,以 eclipse 爲例,查找函數是否被調用,可以使用調用層次圖功能,或者直接使用高級搜索功能,如圖所示:
在調用層次圖(Call Hierarchy)中可以看到 getInstance() 函數被什麼地方調用。
如果使用類似於 spring 的自動裝配功能,在 xml 中定義了調用關係,可以使用高級搜索功能查看 xml 或 properties 文件中定義的同名函數進行篩選。
- 變量命名
就像我們人一樣,一個好名字對變量、常量、函數和類都很重要,一個好的名字會讓其他開發人員很容易明白其功能是什麼。以下是命名的一些注意事項:
-
類和文件名使用名詞,但這個名詞要有意義,比如 Data、Information 就意義不明顯,不是好名字。
-
函數使用動詞或短語命名,比如 isReady hasName。
-
長名字 vs 無意義名字:在長名字和無意義名字中選擇時,請選擇長且有意義的名字,比如 java 語言中使用最廣的類庫 spring 中的一個命名是:SimpleBeanFactoryAwareAspectInstanceFactory。
-
命名法則:常見的有駝峯命名法(camelCase)和蛇形命名法(snake_case), 比如文件名使用蛇形是 file_name,駝峯式 fileName。選擇一種,所有的命名都按照這個規則,並將其作爲編碼規範的一部分,讓團隊成員都要遵守。
如果你要對已有代碼中錯誤的命名方式進行修改,eclipse 提供了很好地支持:選擇要修改的類、函數或變量,選擇 Refactor——》Rename 可以同時修改該變量在聲明和使用處的名稱,如下圖所示:
- 常量命名
常量的命名除了要遵守上一小節提到的通用方法外,還有一類魔法數字(magical numbers)的情況,如使用 0,1 來代表男女。更恰當的做法是定義常量名來代替魔法數字,如在 Java 中:
final int static FEMALE=0,MALE=1;
- 負值條件的重構
在條件或循環語句中,使用負值條件,會讓代碼難以理解、容易出錯,比如判斷是否爲男性,條件寫成了 "! isNotFemale(gender)"
重構方法是將條件改成正值,並調換 if/else 語句代碼塊的順序。
- {} 作爲單獨的一行
正確的 {} 格式已經在 1 部分中提到,這裏再強調下,如果你沒有將括號作爲單獨的一行,如下所示:
catch (IOException e) { e.printstackTrace(); }
你得到的好處只是減少了一行代碼,但是當你設置斷點調試時,斷點將不能精確定位到你想調試的部分。
- 變量定義和使用距離太遠
變量的定義和使用不要離得太遠,一般不要超過 20 行,函數也類似。如果你意識到這個問題,但並不能縮短定義和使用的距離,那代表這是個大函數(big function),你需要對函數做拆分。
以上是對入門級重構方法的介紹,在進行重構時,最重要的規則是:每次只做微小修改,並保證測試能正確運行(小步快跑)。
重構進階
現在我們對重構已經有了基本的瞭解,並建立了初步的信心。讓我們下面關注一些稍微複雜的重構內容。
- 重複代碼
當你發現相同的代碼塊在三個地方都出現時,你就需要考慮重構代碼了。對於同一個類中重複的代碼塊,可使用提取方法(extract method:將重複代碼提取出單獨的函數)來完成;對於一組相關類如父類、子類 A、子類 B 中的重複函數,通過上移方法(pull method:將子類中的方法移入父類中)和模板方法(template method:父類方法定義模板,子類編寫不同實現)來完成。
Eclipse 提供了相關功能,如圖所示:
- 函數參數
-
開關參數的濫用(boolean parameters):函數的形參中有一個是 boolean 類型,函數體根據該參數爲 true 或者 false 執行不同的代碼塊。這種方式會導致重構的另一個壞味道——大函數(big function)的形成,從而增加代碼的複雜性。重構方法是:去掉這個開關參數,將函數拆分成兩個函數。
-
在函數內修改了參數:參數用於外界向函數體傳遞信息,好的做法是參數對於函數是隻讀的。如果在函數內修改參數,會造成函數功能難以理解,如果函數內多次修改參數,這個函數會變成一座迷宮,重構方法是:將參數賦值給局部變量,對局部變量修改,如下代碼所示:
原始的:
int fun(int val) { val=32; }
修改後:
int fun(int val) { int temp=val; temp=32; }
- 參數太多:函數的參數最多有三個是合理的,超過三個就需要提高警惕了。重構方法是:根據邏輯拆分函數;引入參數對象(parameter object:構造參數類,將原來傳遞的參數作爲類的屬性,調用方傳入該類的一個對象)
- 變量多餘
當定義的變量沒太多含義,而且沒有賦值操作,如下代碼:
double basePrice = anOrder.basePrice(); return (basePrice > 1000);
其中的 basePrice 完全是多餘的變量,完全可以用函數本身來替代它,如下代碼:
return (anOrder.basePrice() > 1000);
- 缺少變量
某個臨時變量被賦值超過一次,它既不是循環變量,也不被用於收集計算結果。如果它們被賦值超過一次,就意味着它們在函數中承擔了一個以上的職責。如果臨時變量承擔多個責任,它就應該被替換爲多個臨時變量,每個變量只承擔一個責任。
重構方法:針對每次賦值,創造一個獨立、對應的臨時變量
- 複雜條件
我們都見過由 && || 構成的複雜的多行條件。複雜條件可讀性很差,調試和修改也很麻煩。
一個簡單的重構方式是:將這塊代碼抽取出來,變成一個單獨的判斷函數,如下代碼:
double fetchSalary(double money, int day) { if(money>10000 && day>30 ) { return money
day/365
0.2; }else { return money
30.0/365
0.1; } }
其中的條件 money>10000 && day>30 可重構爲:
boolean isHigherSalary(double money,int day) { return (money>10000 && day>30 ); }
老舊代碼的重構
在進行代碼重構時,需要考慮測試代碼是否能覆蓋重構的功能,如果沒有,需要增加測試用例覆蓋所做的修改,否則重構可能會破壞已有的功能。在前面的章節,作者假設已有足夠的測試用例,並且重構完成後測試可以正確運行。
但是如何重構測試用例沒有完全覆蓋的代碼呢,如老舊代碼?作者的建議是隻做必要的重構,如當需要修正 bug 或者增加新的功能,這種情況下,先爲遺留代碼編寫測試用例,在理解的基礎上重構代碼,爲代碼修改做好準備,然後進行代碼修改。
從這點上來說,你可以進行任何類型代碼的重構:一次只做一步重構,從小的容易的重構做起,並頻繁測試。
利用工具
重構代碼需要花費時間,當項目工期很緊時,很難下定決心去做重構。爲了讓重構變得更容易,市面上提供了大量相關工具,如 pylint( Python 代碼分析工具)、Checkstyle(代碼規範工具)、Sonarqube(代碼質量管理的開源工具)
此外,你要保證你的測試用例跑的足夠快,否則你會沒有耐心等待測試運行結果,或者直接就不運行了。
理想情況下,程序在構建後部署到測試環境前,可以藉助 CI/CD(持續集成 / 持續部署)工具實現代碼質量檢查、代碼樣式檢查、潛在 bug 監測等模塊的自動化運行。
總 結
這篇文章並沒有窮盡重構的所有內容,更多的重構清單和實例,請參考鮑勃大叔編寫的《代碼整潔之道》(Clean Code),尤其是第 17 章的味道與啓發(Smells and Heuristics)和馬丁 · 福勒(Martin Fowler)編寫的重構(Refactoring)一書 。不管你打算以哪本書爲主,在實踐過程中,都會殊途同歸——沉澱出幾條簡單的規則。
不要專門花費大量的時間去進行重構,利用小塊時間,每次只做一部分,只要保證代碼質量比之前有進步就可以了。不要想着以後再做,這個以後很可能是永遠不,最終你將面對一系列可怕的遺留代碼,然後你就深刻理解了 “出來混遲早是要還的” 這句話的涵義。
參考文章:
The Simple Ways to Refactor Terrible Code
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/UHSqj2eguS_VkL0Ia6VXzA