為什么要重構
你可能正在面對一個遺留系統,增加一個需求要改動好幾個文件,定位 Bug 經常要花掉一整天時間,修復一個 Bug 可能又制造了 3 個新的 Bug。你也可能會為了軟件設計和同事爭得面紅耳赤,討論如何應對未來可能出現的需求變化。
為了開發一個新需求,你打開一份源代碼,完全不知所云嘛,你吐槽著誰能寫出如此不堪入目的代碼,于是決定查看版本記錄,把這個家伙找出來鄙視一下。然后你在提交歷史里看到了自己的名字... 恭喜你,你進步了。如果你是一個積極進取的程序員,通常在幾個月甚至幾個星期之后就認不出自己寫的代碼。你總能發現更好的實現方式,讓代碼更加優雅。
隨著增加新特性或需求變更,代碼會變得越來越難以維護。敏捷軟件開發的十二條原則中有一條是:我們始終擁抱需求變化,哪怕是在軟件開發的后期。為了達到這種狀態,我們就要在開發過程中持續地優化代碼。
而重構這項技術,為我們提供了一種更可控的方式來優化代碼。
重構是什么
重構,通常指的是「代碼重構」,起源于 Smalltalk 圈子。
在日常工作中,我們把重構既作為名詞又作為動詞來使用,作為名詞時,它的意思是:
對軟件內部結構的一種調整,目的是在不改變軟件可觀察行為的前提下,提高其可理解性,降低其修改成本。
所以我們會說,「這里需要做一個重構」,「這個重構有點問題」等。
而在其它時候,我們也會說:「我們來重構一下這段代碼吧」,「我正在重構一個遺留系統」,這時就是把重構當做動詞在用,它的意思是:
使用一系列重構手法,在不改變軟件可觀察行為的前提下,調整其結構。
重構本質上是一種代碼整理技術,這項技術使得代碼整理的效率更高,風險更小。
如何做
接下來從幾個方面來說說如何做重構:
- 什么時候開始
- 什么時候停止
- 前提條件
- 重構的過程
什么時候開始
重構不應該是一個單獨的環節,應該融入到開發軟件編寫代碼的過程中,就像使用版本控制系統提交代碼一樣,是一個必須做的動作。你不會跟項目經理說,我需要申請一段時間來提交代碼,所以也不用說服項目經理給你時間重構。你可以在開發新功能,修復 Bug 的過程中就把重構做了,除了你的程序員同伴,沒有人知道你做了什么。而他們會認為你做了一件了不起的事情,因為你讓代碼結構更清晰了,以后添加新特性就會更容易,而 Bug 也無處藏身。
如果你采用 TDD 的方式(測試驅動開發),那重構已完全融入到了開發過程中。如果沒有采用 TDD,通常有四個時機可以考慮要不要重構:
事不過三
如果有段代碼讓你修改起來很不舒服,前兩次還可以忍耐,第三次就無需再忍了,果斷操起 IDE 重構之。因為出現了三次修改,說明有很大概率以后還會修改,這是一筆劃算的投資。
添加新功能
有時候我們發現要添加一個新功能很難,我們可以對代碼做一些重構,讓添加新功能變得容易。
修復缺陷
在修 Bug 時,我們大部分的時間會花在定位 Bug 上,為什么這么難以找到呢?多半是因為代碼結構不清晰,如果代碼在同一抽象層次上,每個方法都在 10 行以內,每個方法名和變量名都能清晰地表達意圖,Bug 就再無藏身之處。所以,通過重構代碼,可以讓 Bug 自動浮現出來。
代碼評審
Code Review 已是一個廣泛采用的實踐,在 Code Review 時,其他程序員會提出代碼修改的意見,記錄下來,等 Code Review 結束之后就可以開始重構了。
什么時候停止
重構到什么時候,我們就認為可以停止了呢?
有兩個標準可以參考,一個是「簡單設計」的四條原則:
- 通過所有測試
- 沒有重復
- 表達意圖
- 最少化程序元素(類,接口,變量,方法等)
另一個是滿足《Clean Code》(整潔代碼)的要求。
前提條件
現代 IDE,尤其是 JetBrains 公司的一系列產品,支持常用的重構手法,極大地降低了重構的風險。但為了保證不改變軟件的可觀察行為,還是需要完善的測試。我也做過一些沒有測試代碼保護的重構,通常會加一個端到端測試以保證不破壞最重要的功能。實在很難編寫測試代碼,至少也要手工測試來保證重構真的沒有改變軟件行為。
另一個重要前提是,使用版本控制系統,比如 Git。因為我們的重構并不一定總是令人滿意,也有可能出現錯誤,導致軟件變得不可用,所以最好是小步提交,以保證可以隨時放棄變更,回到上一次滿意的狀態。
重構的過程
重構的基本步驟是:
- 測試保護
- 識別味道
- 采用手法
- 運行測試
- 提交代碼
測試保護
如果沒有測試代碼,就要先添加測試代碼。如果有測試代碼,先運行一下,保證在開始重構之前,測試是運行通過的。還要認真審查一下測試代碼,看是否有遺漏一些場景,有遺漏的話要補充遺漏的測試場景。
識別味道
怎么知道哪些代碼需要重構呢?首先,代碼是可以工作的,我們并不能說它有問題,但它又不像我們期望的那樣好。受 Kent Beck 剛出生的女兒的使用的尿布的啟發,Martin Fowler 和 Kent Beck 決定用「味道」這個詞來表示需要重構的代碼。他們在《重構》一書中列舉了 22 中常見的味道,如果你看《Clean Code》的話,會發現還有更多。不過,他們并沒有給出一個具體的標準,而是需要我們的直覺來判斷。比如多大的類算「過大的類」?多少行代碼算「過長的方法」?這些需要自行判斷,而直覺的形成有兩種方法,一是隨著編碼經驗的增多自然形成,另一種更快的方式是大量閱讀優秀的開源代碼,提高自己的代碼審美。
《重構》一書中的味道可以分為五類:
- 膨脹劑
- OO 使用不合理
- 難以修改
- 可有可無
- 耦合
書中都有詳細的解釋,這里不再贅述。
發散式變化和散彈式修改是比較容易混淆的兩個味道。前者指一個類的職責過多,有很多因素會引起它的變化,具體的表現就是,不同的需求都會修改同一個文件,導致經常沖突,不能順利地并行開發。后者指的是改一個需求要修改很多個文件,說明沒有把強內聚的代碼歸攏到一起。
大部分的注釋都是沒有必要的,注釋應該描述「做了什么」和「為什么做」而不是「怎么做」,方法體內的注釋基本都可以通過抽取方法并指定一個有意義的名字來解決。很多為了應對未來需求變化而寫的代碼基本永遠不會被執行。
你可能發現了,有些味道是比較容易識別的,比如重復代碼,注釋等。而有些就比較高級,比如特性依戀,中間人等,要識別高級味道,需要理解面向對象的特性和設計原則。
采用手法
識別到味道之后,就要知道有什么對應的手法可以消除這個味道,執行完這個手法之后代碼會變成什么樣子。
在《重構》一書中,列舉了 66 個常用手法,可以分為六大類:
- 重組函數
- 搬移特性
- 組織數據
- 簡化條件
- 簡化調用
- 處理概括
這些手法在書中都有詳細的講解,我就不在這里重復了。只整理出來,給大家一個宏觀的印象:
運行測試
在采用了手法修改代碼之后,就要執行測試以確保真的沒有改變軟件的行為。可能有時會發現,做了重構之后測試會失敗,但實現并沒有問題,我們需要修改測試代碼讓它成功。這就說明測試寫的不合理,給重構帶來了負擔,所以我們測試的粒度要把握好,太細的粒度就會增加維護成本。比如,有些人會給每個私有方法都寫單元測試,那有可能采用「內聯函數」這個手法之后這個方法就不存在了,就需要修改測試。這里說起來話就長了,以后再寫一篇如何寫有效的測試的文章吧。重點是重構之后,一定要執行測試,不管是手工測試或自動化測試。
提交代碼
最后,如果你采用了一個比較復雜的手法,或者即將采用一個復雜的手法,最好先提交一下代碼,以保證出現意外后能快速回滾,避免浪費時間。
重構要采取「小步快跑」的原則,盡量采用安全的手法,讓測試一直處于通過的狀態。
從低級的壞味道開始,消除低級味道之后,高級味道才會浮現出來。
進階
重構與設計的關系
在沒有重構這個技術之前,廣泛采用的是 Big Front Design,在開始編碼之前要進行非常詳細的設計,考慮應對未來出現的各種變化。而有了重構技術之后,前期設計的壓力就小了,畢竟可以隨時通過重構來改善設計,應對變化。所以你大可不必一上來就應用《設計模式》把代碼搞復雜,先用簡單的實現滿足當前需求即可。等變化真正來臨時,再通過重構技術調整設計,模式給我們提供了一個方向,但并不是最終目標。還記得簡單設計的四條原則嗎?通過測試,沒有重復,表達意圖,最少元素。除了這四條原則,還有 SOLID,DRY,KISS 等設計原則。只要最終的代碼符合好的原則,干凈整潔沒有壞味道,管它符不符合某個模式呢?!
大型遺留系統的重構
對于代碼上百萬,千萬行的遺留系統,怎么重構呢?滿地都是壞味道,一點點去重構,什么時候是個頭?
這時,選擇哪些代碼來重構就非常重要,影響到投資回報。如果對代碼進行分類,將會得出幾種類型:
- 不會被執行的爛代碼
- 運行穩定,基本不會改動的爛代碼
- 經常發現 Bug 的爛代碼
- 經常需要變更的爛代碼
不會被執行的代碼,直接刪除就好了。運行穩定的又不需要改動的,動它反而可能引入風險,當然,在時間充裕的情況下,還是可以重構的。真正有價值,值得重構的,投入產出比最高的,是經常出問題和經常會有需求變更的爛代碼。優化了這部分代碼,可以減少 Bug 和進行需求變更的時間。
好了。關于重構我想分享的就是這些,我們來回顧一下:
為什么要重構?
為了讓軟件始終可以維護,保證開發效率。
什么是重構?
一種以可控的方式整理代碼的技術,在不改變軟件可觀察行為的前提下改善其內部結構。
什么時候開始?
事不過三,添加功能,修復 Bug,代碼評審時。
什么時候停止?
重構到符合簡單設計四條原則的 Clean Code。
前提條件
測試保護,版本控制。
重構的過程
運行測試,識別味道(常見的 22 種),采用手法(66 個),運行測試,提交代碼。
重構與設計的關系
有了重構技術,我們不用在前期做非常詳細的設計,做適當的設計,然后通過重構讓設計浮現出來。不用在乎軟件是否符合模式,只要符合原則即可。
大型遺留系統的重構
在經常需要修改的爛代碼上做重構才有最大收益。
最后推薦一些學習資源:
- 《代碼整潔之道》
- 《編寫可讀代碼的藝術》
- 《重構》
- 《設計模式 - 可復用面向對象軟件的基礎》
- 《重構與模式》
- Transformation Priority Premise
- 用 IntelliJ IDEA 重構
- 重構十六字心法
- 練習重構的 Kata
本文最初發布于 GitChat,歡迎閱讀答疑實錄。