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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...