Effective Refactoring in C++ (一)

来源:http://www.cnblogs.com/magicbowen/archive/2016/05/30/5541007.html
-Advertisement-
Play Games

引言 Martin Fowler的《重構:改善既有代碼的設計》一書從2003年問世至今已有十幾年時間了,按照電腦領域日新月異的變化速度,重構已經算是一門陳舊的技術了。但是陳舊並不代表不重要,恰恰隨著演進式設計被越來越廣泛的使用,重構技術已經被認為是現代軟體開發中的一項必備的基本技能!所以今天在任何 ...


引言

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種低層次的代碼等價變換操作(例如: 重命名變數,為函數增加一個參數,刪除一個不被引用的類...). 按照一定設計好的順序組合上述低層次的代碼等價變換操作,我們可以完成一次安全的代碼重構,保證代碼重構前後的行為保持要求.

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

\[ \frac{a^2 + 2ab + b^2}{a + b} => \frac{(a + b)^2}{a + b} => a + b \]

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

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

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

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

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

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

高效地重構

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

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

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

哪些不是重構?

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

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

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

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

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

關於本文

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

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

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

作者 : MagicBowen, Email:[email protected] , 轉載請註明作者信息,謝謝!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 上一篇我們已經獲得了制定類型的實例,但我們還無法對其進行有效的控制。 我們用ExportMetadata屬性可以對具體的某個實例做標記,相當於命名。這麼理解不知道對否。 在IPart項目中添加一個介面IPatMetadata 在導出的地方添加具體的導出元數據ExportMetadata,以txtFi ...
  • 1、創建線程的一種簡單方法是定義一個委托,並非同步調用它。 增加代碼中第17行的Sleep方法的參數值,輸出的星號的個數會減少,反之會增加。 運行結果: 2、IAsyncResult有一個名字叫AsyncWaitHandle的屬性,該屬性的類型是WaitHandle類。該類的WaitOne方法會“將一 ...
  • 分別向Set集合以及List集合中添加“A”,“a”,“c”,“C”,“a”5個元素,觀察重覆值“a”能否在List集合以及Set集合中成功添加。 運行: ...
  • 使用源文件 現在讓我們重新開始編程。當你學習一種新的編程語言的時候,你編寫運行的第一個程式通常都是“Hello World”程式,這已經成為一種傳統了。在你運行“Hello World”程式的時候,它所做的事只是說聲:“Hello World”。正如提出“Hello World”程式的Simon C ...
  • 開發PHP的朋友都知道,其實最擔心的就是程式中出現一些異常或錯誤,這些狀況如果輸出到用戶的螢幕會把用戶給嚇壞,甚至為此丟了工作,如果不輸出到螢幕就得想辦法記錄到日誌中,但是似乎不是每個人都有查看錯誤日誌的習慣,爲瞭解決這個尷尬的問題,所以我寫了這段代碼,其用意就是當我們寫的php程式出錯的時候把錯誤 ...
  • 首先,移位運算符有三種,其操作類型只支持:byte / short / char / int和long五種。 << 左移運算符,表示將左邊的操作數的二進位數據向左移動*位,移動後空缺位以0填充,多餘位捨棄。(等同於乘2的n次方) >> 右移運算符,二進位數據向右移動*位,就在其二進位數據後抹掉幾位? ...
  • ...
  • 上篇文章介紹了PHP添加元素到數組的函數,那麼當然有從數組中刪除元素。array_pop和array_shift只從數組的頭或尾刪除一個元素。經過閱讀源碼,發現這兩個函數的實現都是調用了同一個函數--_phpi_pop來實現從數組中刪除一個數組元素的功能。因此解讀時將這兩個函數一併講了。 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...