1. 程式人生 > >高效重構 C++ 程式碼(下)

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

本節針對C++語言,總結了一些對實施重構非常有用的工程實踐經驗. 當然其中決大部分也適用於C語言開發.

語言相關

由於C++的複雜性,導致業界對C++的自動化重構工具支援一直非常不理想.很多重構到目前為止只能手工進行. 重構工具對語言的支援的一個難點在於對程式碼引用點的分析查詢. C++中有很多語法會干擾到這一分析,導致難以進行自動化重構.
具體會讓重構難以進行的語法包含以下:

  • 巨集
  • 指標
  • 函式過載
  • 操作符過載
  • 預設引數
  • 型別強轉
  • 隱式轉換
  • 友元
  • 模板

還有一些不合理的編碼規範,也會阻礙到重構的進行,例如

  • 匈牙利命名
  • 函式單一出口
  • 大而全的common標頭檔案

上述語法或者規範不僅會讓自動化重構難以進行,也會干擾到手動重構. 對於不好的編碼規範要及時摒棄! 對於會妨礙重構的語言元素要慎重使用,最好保持其影響在一定範圍內,縮小重構時查詢引用不確定性的範圍.

當然C++語言中也有一些語法非常有利於重構,例如:

  • const
  • 訪問限制修飾符private,protected
  • 匿名namespace
  • 區域性變數延遲定義

這些語言元素有些會讓程式碼語義更明確,有些會限制程式碼元素的可見度,縮小引用查詢範圍. 對於這些語言元素在開發中儘可能多的去使用,可以讓重構變得更加容易.

最後,保持好的編碼習慣,例如一開始就不要將檔案類函式搞得過大,儘可能多的對外隱藏細節,對複雜語法的使用保持慎重態度,這樣每次重構的成本都會小很多.

自動化測試

由於重構依賴的測試需要快速高效,所以我們往往選擇xUnit測試套件,可以對軟體進行小模組級別的黑盒測試或者單元測試.
目前針對C++

,可供選擇的xUnit套件有gtest,Testngpp,CppUnit,CXXTest以及Boost Test等. 目前最多人使用的應該是gtest,由於使用廣泛所以非常成熟,支援跨平臺,方便簡單容易上手,提供了豐富的斷言機制和成熟的用例管理. 當然gtest也有其問題,主要是測試註冊機制不夠優雅導致限制過多,測試用例執行期不能隔離導致互相影響,測試工程的組織不能模組化導致單元測試不能顯示化物理依賴,無法低成本完成測試依賴檔案的物理替換.
經過個人使用,目前功能最強大的C++測試套件應當是Testngpp,它也支援跨平臺,測試註冊依靠指令碼掃描所以限制很少,另外和gtest一樣斷言豐富,用例管理成熟. 它最大的強大之處是支援沙盒模式,測試用例執行期間互相隔離,不會彼此影響. 另外,Testngpp支援模組化管理測試工程,對於單元測試可以顯示化物理依賴. 同時模組化內可以方便的進行測試依賴檔案的物理替換,這一特性對於沒有虛介面情況下的mock非常實用.但是由於Testngpp的強大,也導致了它某些情況下不如gtest方便使用.

在使用xUnit的同時再能結合一款合適的Mock框架,可以大大降低單元測試的成本.對於C++目前最好用Mock框架是Mockcpp,沒有之一! 用gmock的同學儘快轉過來:)! 由於Mock經常容易被濫用,導致過多的針對行為進行驗證.而且往往對於測試直接打樁進行狀態驗證成本並不高,所以目前來說我已經很少使用Mock工具了.但是留著它在適合的時候偶爾使用一下也是不錯的.

最後,我們都知道重構需要自動化測試的支援! 但是並不是沒有自動化測試,重構就不能開展! 有些歷史遺留程式碼,一開始很難加進去測試,必須要先著手修改,才能逐漸新增測試. 這時起步需要的就是對安全小步的重構手法的熟練掌握,以及利用原有手工測試進行安全保證. 當對軟體進行修改從而可以新增自動化測試後,重構慢慢就上軌道了.

IDE

為了提高重構效率,我們需要一款智慧的IDE.它需要支援基本的自動化重構,能夠高效準確的搜尋到程式碼的引用點,支援良好的面向物件風格程式碼瀏覽,高效流暢地快捷鍵…

本人使用過的一些不錯的C++ IDE有:

  • Visual studio C++
    自身的重構工具並不完善,但是加上番茄小助手就很不錯了.支援自動重新命名,自動提取函式,重新命名類還會自動把類的標頭檔案和所有的#include點的檔名都做一修改.最大的缺點是不能跨平臺,而且耗費機器資源較多.最新發布的Visual studio 2015據說已經內建了很多重構功能,不過還沒有試過.
  • clion
    Jetbrains公司出品的跨平臺C/C++IDE,號稱要做最強大的C++ IDE. 目前是收費的,免費的只能試用30天。工程構建只支援CMake,另外只能在64位機器上使用。 Clion內建的自動化重構選單是最強大(支援rename,inline,extract,Pull member up/down …)。目前釋出了1.0版本,使用者還不是特別多,可以持續關注.
  • eclipse-cdt
    個人目前使用最多的C++ IDE. 最大的好處是支援跨平臺. 用eclipse在類的繼承引用關係之間跳轉非常流暢. 支援簡單的重新命名,提煉函式,提煉常量等自動化重構. 從luna版本開始支援重新命名檔案或者移動檔案的時候會自動替換所有#include中的路徑名. 個人目前最滿意的C++ IDE.

對於靜態型別語言,好的IDE可以在很多地方幫助開發者提高效率. 如果你還在使用Source Insight或者其它高階文字編輯器來進行大規模C++程式碼開發,我建議你試試上面推薦的IDE,總有一款適合你,保證你一旦使用就會離不開它! 如果你還有其它更好的,也請拿出來分享給大家!

物理重構

物理設計對C/C++程式來說非常重要! 好的物理設計不僅可以減少物理依賴,還會有利於軟體的構建和釋出,並且還會方便理解和查詢程式碼元素. 所以C/C++程式設計師除了一般的程式碼元素級別的重構,還需要經常進行物理級別的重構,包括檔案重新命名,檔案提煉,檔案移動,或者目錄結構調整等等. 對於C/C++來說物理重構比較繁瑣的地方在於經常需要同步修改makefile或者標頭檔案的#include引用點. 以下是一些和物理重構相關的經驗技巧(make相關的見下節):

  • 每個檔案只包含一個類,並且檔名和類名相同. 這樣方便重新命名類的時候IDE自動對檔案進行重新命名.
  • 利用IDE提供的自動化重新命名或者移動選單進行檔案/目錄的重新命名或者移動,以便IDE可以對涉及檔案的所有#include引用點進行路徑自動替換.
  • 使用IDE提供的自動化標頭檔案新增功能來新增物理依賴,例如在eclipse中是ctrl+shift+n
  • 配置IDE的標頭檔案模板,讓自動生成標頭檔案的Include Guard使用Unique Identifier,避免每次標頭檔案重新命名後還得要修改Include Guard(見下). (最新的eclipse mars版本中在Windows -> Preferences -> C/C++ -> Code Style -> Name Style -> Code -> Include Guard 中選擇 Unique Identifier,較老版本需要在workspace中修改配置檔案,可以自行google修改辦法).
12345678910 // Runtime.h#ifndef HDEA41619_5212_4A92_8A09_3989000E6BAE#define HDEA41619_5212_4A92_8A09_3989000E6BAEstructRuntime{voidrun();};#endif

編譯構建

不好的編譯構建工程不僅干擾到物理重構,更重要的可能導致編譯效率底下從而讓重構無法正常開展. 沒人願意在每次構建都要幾十分鐘到數小時的工程上進行頻繁重構. 大型C/C++工程要對其編譯構建過程進行持續地分析優化,不斷提高版本的編譯構建速度. 另一方面,工程需要支援增量編譯,以及可以對任意一個指定的子模組或者檔案單獨進行編譯.

關於編譯構建的一些實踐經驗如下:

  • makefile中儘可能使用模式規則.自動搜尋檔案.不要顯示使用原始檔名,否則每次重新命名檔案後都得要修改makefile.
  • makefile中為預編譯目標檔案設定規則,不僅有利於解決一些巨集展開的問題,更重要的可以快速解決一些標頭檔案包含上的編譯難題.
    1234567891011 # makefile example...SRCS+=$(abspath($(shell find$(SOURCE_HOME)-name"*.cpp")))OBJS=$(subst$(SOURCE_HOME)$(TARGET_HOME)$(SRCS:.cpp=.o))...$(TARGET_HOME)%.i:$(SOURCE_HOME)%.cpp$(CXX)-E-o$@-c$<$(TARGET_HOME)%.o:$(SOURCE_HOME)%.cpp$(CXX)-o$@-c$<$(TARGET):$(OBJS)@$(generate-cmd)
  • makefile採用自底向上組織,保證每個原始碼檔案單獨可編譯,每個模組單獨可編譯. 最終產品版本的構建呼叫每個模組的makefile生成編譯中間產物後進行連結. 不要採用自頂向下傳遞make引數的makefile工程管理模式,否則每次編譯任何一個檔案或者模組都要全編譯所有程式碼.
  • 儘可能使用並行編譯. 在Visual Studio下可以使用IncrediBuild分散式編譯工具. 對於make使用-j選項,並且可以使用distcc來進行分散式編譯.另外可以使用ccache來做快取加速編譯。
  • 將測試工程的編譯構建和真實產品版本的構建分離,測試工程的編譯構建可以採用更好的工具:例如cmake,rake等.
  • 保證增量編譯可用,並且是可靠的.

很多專案雖然有增量編譯,但是都不夠可靠,尤其是當有標頭檔案刪除的時候! 另外每次無論是否有依賴變化,makefile都要重新生成載入.d檔案,效率也很低下. 所以大多數時候增量編譯功能是關閉的!
《GNU Make專案管理》一書中提供了很多大型工程中make組織的優秀實踐,其中有一段對增量編譯可靠高效的makefile片段,我將其提煉成了一段make函式,見下面,大家可以使用.

make%e5%87%bd%e6%95%b0

總結

我過去幾年的工作主要在大型電信系統的軟體重構, 這篇文章基本上是自己工作實踐的一些心得,包括對《重構》一書的一些精煉和總結. 當然關於重構最精華的部分還是在原書的每一處細節中. 勵志學好重構的同學對於《重構》一書最好能夠反覆閱讀實踐.

對於再大型的軟體重構,無論你的目標架構多麼漂亮,最終還必須是一行一行的程式碼修改。如何安全可靠並且高效地修改程式碼,必然是落地的基本技能!

對於日常開發也是如此, 保持程式碼符合簡單設計是一項日常行為,重構是達成它的唯一方式. 對重構的使用應該融入到程式碼開發的每時每刻,到最後不必強行區分是在開發還是重構,就像《重構》的序言中所說”變得像空氣和水一樣普通”. 希望每個學習重構的同學都能體會到這種感覺!

千里之行,始於足下!