高效重構 C++ 程式碼(上)

魔術大師發表於2016-09-24

引言

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

總結以上問題,一部分原因是因為沒有正確的理解重構,不知道重構的起點和目標,對重構的物件和目標沒有衡量和比較的標準;其次是因為沒有掌握形式化的重構手法和步驟,重構過程往往只是跟著感覺走;最後實踐重構的過程中,沒有先理順自己的開發、構建和測試環境,導致重構成本很高! 對於開發、構建和測試環境的問題,C/C++領域尤其嚴重,除了沒有像Java領域那麼好用的自動化重構工具,很多開發人員連一個好用的IDE都找不到,更不要說普遍認知的構建速度慢,自動化測試匱乏等等問題!

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

什麼是重構?

重構的定義

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

第一個是名詞形式:

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

第二個是動詞形式:

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

重構的目標

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

Matin Fowler利用上面兩個定義,指出了重構的目標:

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

而對於上述目標,我們再深入一點分析,發現其實已經有更經典的定義. 那就是Kent Beck的簡單設計四原則:

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

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

到目前為止,簡單設計四原則是對”什麼是好的軟體設計”最好的定義!

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

Matin定義的重構的其它兩條目標,對應了簡單設計原則的第2和第3條:
2) 重構應該消除重複: 降低軟體修改成本;
3) 重構應該讓程式顯示錶達意圖: 提高軟體可理解性;

最後,我們把簡單設計四原則的最後一條也加入重構的目標:
4) 重構應該消除冗餘:降低軟體不必要的複雜度.

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

從哪裡開始?

對於重構的目標達成一致後,我們回到起點:什麼樣的軟體需要重構? 以及什麼時候進行重構?

對於第一個問題,由於我們重構的目標是使軟體滿足簡單設計四原則,那麼任何違反簡單設計四原則的程式碼都應該是我們重構的目標.例如1)程式碼很容易出現bug,導致測試失敗! 或者 2)程式碼存在知識重複使得不易修改! 或者 3)程式碼寫的晦澀非常難以理解! 或者 4)程式碼存在過度設計,存在冗餘導致複雜!

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

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

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

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

事實上在我的工作過程中,重構是隨時隨地進行的. 尤其對於採用演進式設計方法論,重構和程式碼開發是緊密結合難以分割的,甚至很多時候只有依託重構才能完成程式碼的開發.

重構的手法

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

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

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

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.

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

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

高效重構 C++ 程式碼(上)

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

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

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

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

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

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

高效地重構

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

  • 覆蓋良好高效的自動化測試
  • 合適的IDE,最好提供基本的自動化重構選單
  • 良好的工程設定
  • 高效的構建環境
  • 良好的編碼習慣

對於上面這些,不同語言面臨的現狀不同,針對C++語言我們後面會專門總結.

哪些不是重構?

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

  • “我把bug重構掉了!”
  • “Debug一下剛才的重構那裡出錯了”
  • “昨晚重構出來的Bug到現在還沒有查出來”
  • “先把程式碼重構好,再看測試為啥不過”
  • “我把軟體架構由集中式重構成分散式了”

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

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

軟體重建一般是站在之前開發、測試的基礎上,伴隨著對軟體要解決的問題和解決方式本身有了更深入的理解,通過修改軟體把這些學習成果反映到軟體的結構中去,使得軟體可以更好、更精煉的解決業務問題。站在DDD(領域驅動設計)的角度,軟體重建一般是對領域模型的進一步精練,使得軟體更加貼合業務的本質!雖然成功的軟體重建往往能對組織帶來較大的收益,但是由於軟體重建的開銷普遍較大,而軟體開發又是一項商業活動,所以需要對軟體重建謹慎評估其成本收益比以及過程風險後才能決定是否啟動。而本文中的重構技術,則只是一項日常編碼中頻繁使用的安全、高效的程式碼修改技術,被普遍認為是現代軟體開發技術中必備的一項基本技能,是演進式軟體設計或者軟體重建目標達成的一項必要手段!

關於本文

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

高效重構 C++ 程式碼(上)

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

由於重構手法和實施順序是學習重構的關鍵,所以本文後面會重點講述這個主題. 另外,在實踐中如何高效和安全的進行重構,和具體使用的程式語言及其開發、構建、測試環境關係也很密切.本文最後會針對C++語言總結這方面相關問題.

相關文章