高效重構(一): 正確理解重構

引言

Martin Fowler的《重構:改善既有代碼的設計》一書從問世至今已有十幾年時間了,按照計算機領域日新月異的變化速度,重構已經算是一門陳舊的技術了。但是陳舊并不代表不重要,恰恰隨著演進式設計被越來越廣泛的使用,重構技術已經被認為是現(xiàn)代軟件開發(fā)中的一項必備的基本技能!所以今天在任何軟件開發(fā)團隊中,你都會不時聽到或看到和重構相關的代碼活動。然而對于這樣一種被認為應該是如同“軟件開發(fā)中的空氣和水”一樣的技術,在現(xiàn)實中卻比比皆見對重構的錯誤理解和應用。首先是不知道重構使用的正確場合,總是等到代碼已經腐化到積重難返的時候才想起重構;其次面對一堆的代碼壞味道沒有選擇標準、無從下手;接下來修改代碼的過程中不懂得安全、小步的重構手法,總是大刀闊斧地將代碼置于危險的境地,很難再收回來;最后要么構建、測試失敗后無法恢復只能推到重來,或者最終結果只是將代碼從一種壞味道修改到了另一種壞味道!

總結以上問題,一部分原因是因為沒有正確的理解重構,不知道重構的起點和目標,對重構的對象和目標沒有衡量和比較的標準;其次是因為沒有掌握形式化的重構手法和步驟,重構過程往往只是跟著感覺走;最后實踐重構的過程中,沒有先理順自己的開發(fā)、構建和測試環(huán)境,導致重構成本很高!

本文站在作者多年來實踐重構的基礎上,為大家梳理重構技術,帶領大家重新認識重構的目標和起點,重構手法背后的原理以及實踐方式。最后會以C/C++語言為例,介紹在實踐中高效實施重構的經驗、技巧和工具。

什么是重構?

重構的定義

Martin Fowler在《重構:改善既有代碼的設計》一書中給出了重構的兩個定義.

第一個是名詞形式:

Refactoring: 對軟件內部結構的一種調整,目的是在不改變軟件可觀察行為的前提下,提高其可理解性,降低其修改成本.

第二個是動詞形式:

Refactor: 使用一系列重構手法,在不改變軟件可觀察行為的前提下,調整其結構.

重構的目標

重構的目標是什么? 重構的目標絕不是將代碼從別人的taste改成自己的taste,也不是將代碼從一種壞味道改到另一種壞味道!

以下是Matin Fowler給出的重構的目標:

  • 不改變軟件可觀察行為
  • 提高軟件可理解性
  • 降低軟件修改成本

而對于上述目標,我們再深入一點分析,發(fā)現(xiàn)其實已經有更經典的定義了。 那就是Kent Beck的簡單設計四原則:

  • Pass All Test: 通過全部測試;
  • No Duplication: 沒有重復(DRY)
  • Reveals Intent: 程序表達意圖,易于理解
  • Has no superfluous parts: 沒有冗余,或者YAGNI原則

上述四條的重要程度依次降低.

到目前為止,簡單設計四原則是對 “什么是好的軟件設計” 最好的定義! 所以我們可以說重構的目標就是為了使得軟件更加的符合簡單設計原則。

簡單設計四原則第一條定義了“好的軟件首先應該通過所有測試”,即正確滿足所有功能需求。而重構的目標中最基本的就是"不改變軟件的可觀察行為",也就是說:

  1. 重構后的軟件不能破壞原來所有測試!

Matin定義的重構的其它兩條目標,對應了簡單設計原則的第2和第3條:

  1. 重構應該消除重復: 降低軟件修改成本;
  2. 重構應該讓程序顯示表達意圖: 提高軟件可理解性;

最后,我們把簡單設計四原則的最后一條也加入重構的目標:

  1. 重構應該消除冗余:降低軟件不必要的復雜度.

所以以后當我們再來討論重構的目標,或者評判重構有沒有收益的時候,就用簡單設計四原則來衡量它.

從哪里開始?

對于重構的目標達成一致后,我們回到起點:什么樣的軟件需要重構? 以及什么時候進行重構?

對于第一個問題,由于我們重構的目標是使軟件滿足簡單設計四原則,那么任何違反簡單設計四原則的代碼都應該是我們重構的目標。例如:

  • 代碼很容易出現(xiàn)bug,導致測試失敗;
  • 代碼存在知識重復使得不易修改;
  • 代碼寫的晦澀非常難以理解;
  • 代碼存在過度設計,存在冗余導致復雜! **

但現(xiàn)實中可能有一堆的代碼問題等待我們解決,而時間、成本、人力是有限的,所以我們需要從最有價值,最沒有爭議的部分開始重構。由于簡單設計四原則的重要程度是依次降低的,對于四條原則的判定從上往下也是逐漸主觀化,所以我們選擇重構的代碼的優(yōu)先級順序也是按照它們破壞簡單四原則的順序依次降低! 如果一坨代碼存在很多重復,另外一坨代碼不易理解,那么我們優(yōu)先選擇去解決重復代碼的問題,因為按照簡單四原則消除重復更重要,也更容易被客觀評價.

在《重構》一書中Martin為了避免引起所謂編程美學的含混爭辯,總結了代碼的22條壞味道. 在實踐中我們一般都是從某一代碼壞味道著手重構的,但是對于優(yōu)先重構哪個壞味道,我們遵守上面描述的原則.

對于進行重構的時機,Matin給出:

  • 重復地做某一件事情的時候 (三次法則)
  • 添加新功能的時候
  • 修改Bug的時候
  • Code Review的時候

事實上在我的工作過程中,重構是隨時隨地進行的。尤其是對于采用演進式設計方法論的團隊,重構和代碼開發(fā)是緊密結合難以分割的,甚至很多設計只有依托先重構才能達成目標。

重構的手法

明白了起點和目標,下來最重要的就是掌握完成這一過程的手段! 而重構的手法則是帶領我們正確到達目標的工具.

很多人認為學習重構只要掌握背后的思想就足夠了,其詳細繁瑣的操作手法并不重要.于是乎現(xiàn)實中我們看到很多人在實際操作重構的過程中章法全無,一旦開始半天停不下來,代碼很多時候處于不可編譯或者測試不能通過的狀態(tài),有時改的出錯了很難再使代碼回到初始狀態(tài),只能推倒重來! 實際上重構是一項非常實踐性的技術,能夠正確合理地使用重構操作,安全地,小步地,高效地完成代碼修改,是評價重構能力的核心標準.

那么什么才是正確的重構手法?

Martin對重構的第二個定義中提到使用一系列的重構手法,但是對這一系列的重構手法卻沒有概括.

而William Opdyke在他的論文"Refactoring Objected-Oriented Frameworks"里面對重構給出了如下定義:

重構:行為保持(Behavior Preservation)的程序重建和程序變換.

在論文里面將重構手法定義為一些程序重建或者程序變換的操作,這些操作滿足行為保持(Behavior Preservation)的要求. 論文里面對行為保持的定義如下:

Behavior Preservation : For the same set of input values,the resulting set of output values should be the same before and after the refactoring.

也就是說存在一系列代碼變換的操作,應用這些操作之后,在相同的輸入條件下,軟件的輸出不會發(fā)生變化. 我們把滿足上述要求的代碼操作稱之為代碼等價變換操作. 在William Opdyke的論文中針對C++提出了26種低層次的代碼等價變換操作(例如: 重命名變量,為函數增加一個參數,刪除一個不被引用的類...). 按照一定設計好的順序組合上述低層次的代碼等價變換操作,我們可以完成一次安全的代碼重構,保證代碼重構前后的行為保持要求.

這里代碼等價變換的過程. 類似于初等數學中的多項式變換.例如對于如下公式變化:

每一步我們運用一次多項式等價變換公式,一步一步地對多項式進行化簡,每次變換前后多項式保持等價關系.

在多項式化簡的這個例子中,承載簡化過程的是已經被數學證明過的多項式等價變換的公式. 同理承載重構的則是被證明過的一個個代表代碼等價變換操作的重構手法.

另外,由于完成一項重構需要使用一系列的重構手法,這些手法的使用順序也是至關重要的!

我們學習重構,就是要來學習每種場景下所使用的小步安全的重構手法及其使用順序,并不斷加以練習! 能夠靈活而流暢地使用一系列重構手法完成一項重構,是衡量重構能力的一個非常重要的指標.

而本文后面的一個重點就是對常用的重構手法以及運用順序進行提煉,降低大家的學習難度.

最后,既然重構中使用的是安全小步的代碼等價變換手法,為什么我們還需要測試? 首先是因為我們是人,我們總會犯錯! 另外由于編程語言的復雜性導致所謂的等價變換是受上下文約束的,例如在C++中為一個存在繼承關系的類的成員方法重命名,有可能導致新的方法名和它某一父類中有默認實現(xiàn)的虛方法重名,而即使編譯器也不能發(fā)現(xiàn)該錯誤.

高效地重構

雖然我們了解了如何/何時開始,目標,以及重構的手法,但是如果我們有了下面這些因素的輔助,會讓我們更加安全和高效.

  • 覆蓋良好\高效的自動化測試
  • 合適的IDE,最好提供基本的自動化重構菜單
  • 良好的工程設置
  • 高效的構建環(huán)境
  • 良好的編碼習慣

對于上面這些,不同語言面臨的現(xiàn)狀不同,針對C++語言我們后面會專門總結.

哪些不是重構?

針對上面的討論,我們站在嚴格的重構定義上來看看下面這些反模式:

  • "我把bug重構掉了!"
  • "Debug一下剛才的重構那里出錯了"
  • "昨晚重構出來的Bug到現(xiàn)在還沒有查出來"
  • "先把代碼重構好,再看測試為啥不過"
  • "我把軟件架構由集中式重構成分布式了"

想想上面的場景哪里存在問題?

在實際的開發(fā)過程中,我們還經常面臨另外一種場景,那就是對某一已經開發(fā)完成的軟件模塊進行整體重構。在這樣的過程中,雖然也存在頻繁地使用重構手法對原有模塊代碼進行修改,但是更多的是進行大量的架構和設計方案上的修改。為了與我們要討論的重構進行區(qū)分,對于這樣的過程,我們稱其為軟件重建(re-engineering)。

軟件重建一般是伴隨著對軟件要解決的領域問題和解決方式本身有了更深入的理解后,通過修改軟件把這些學習成果反映到軟件的結構中去,使得軟件可以更好、更精煉的解決業(yè)務問題。站在DDD(領域驅動設計)的角度,軟件重建一般是對領域模型的進一步精練,使得軟件解決方案更加貼合業(yè)務的本質,更好地應對未來業(yè)務的變化!

軟件重建可以選擇完全重寫代碼,也可以選擇通過小步重構的方式逐步達成軟件重建目標。完全重寫一般來說容易更快達成目標,但是過程風險不易控制,所以需要謹慎評估其成本收益比以及過程風險后才能決定是否啟動。而本文所說的重構技術,則是一項日常編碼中頻繁使用的安全、高效的代碼修改技術。通過對代碼的持續(xù)重構,也可以逐步達成軟件重建的目標,雖然看似周期較長,但是過程更加可控。

關于本文

我們總結一下,重構有三個要點,見下圖:

  1. 你要有一個敏感的鼻子,能夠嗅出代碼中的壞味道; 一般只要發(fā)現(xiàn)不符合簡單設計四原則的Code,就是我們需要重構的目標對象. 而Martin總結的22條代碼壞味道給我們一個很好的實踐起點.
  2. 你要知道重構的目標,就是讓代碼逐漸靠近簡單設計四原則.
  3. 需要掌握小的安全的重構手法,以及在不同場景下合理的使用順序,以便安全高效地承載重構目標的達成.

由于重構手法和實施順序是學習重構的關鍵,所以本文后面會重點講述這個主題. 另外,在實踐中如何高效和安全的進行重構,和具體使用的編程語言及其開發(fā)、構建、測試環(huán)境關系也很密切.本文最后會以C++語言為例總結這方面相關問題.


高效重構(二):掌握重構手法
高效重構(三):構建高效工程環(huán)境

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 如何實施重構 稍微復雜的重構過程,都是由一系列的基本重構手法組成. 《重構》一書中針對各種重構場景,給出了大量的重...
    MagicBowen閱讀 4,470評論 0 3
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,057評論 25 709
  • 1.測試與軟件模型 軟件開發(fā)生命周期模型指的是軟件開發(fā)全過程、活動和任務的結構性框架。軟件項目的開發(fā)包括:需求、設...
    Mr希靈閱讀 22,405評論 7 278
  • 1.測試與軟件模型 軟件開發(fā)生命周期模型指的是軟件開發(fā)全過程、活動和任務的結構性框架。軟件項目的開發(fā)包括:需求、設...
    宇文臭臭閱讀 6,873評論 5 101
  • 凡是遙遠的地方 對我們都有一種誘惑 不是誘惑于美麗 就是誘惑于傳說 ——汪國真《旅行》 泰國便是這樣美麗的傳說。宜...
    Reneloveslife閱讀 842評論 0 0

友情鏈接更多精彩內容