代碼質量 – 代碼的歷史是代碼未來的預言
We can never see past the choices we don’t understand. – Oracle (The Matrix)
副標題是:“一個使用 code-maat, git, Python, D3.js 進行代碼質量衡量的 case study“。
大概是在2017年6月份吧,我當時在工作中作為一些小項目的負責人,收到了同事的一份報表,主要內容是通過 jsinspect ,檢測出了有幾個項目代碼重復率比較高,提醒我是否有些項目需要優化或部分重構一下。我當時第一次萌生了作為周期性項目,代碼的質量是應該作為一個指標隨著迭代被量化,從而指導后續項目迭代的,雖然當時我聽說過 sonar。隨后我工作中參與了一個中心化代碼質量評估系統一期的設計和開發(主要是針對 javascript 代碼的規范和質量檢測與持續集成進行耦合),但是當時這個系統沒有解決這樣一個問題,即能夠告訴我代碼里具體哪部分存在更高優先級被重構的需要。半年過去了,現在我嘗試回答一下這個問題,并舉一個與編程語言無關的一個通用可行的例子,通過這個例子揭示一個事實:
“代碼的歷史,是代碼未來的預言“
如果把項目代碼比喻成城市,那么系統中維護成本高的復雜部分就好比潛伏在城市中的罪犯,我們對于罪犯的搜查,顯然不能對城市進行地毯式的搜索,而是應該找到之前有過犯罪記錄的一些街區,劃分重點區域進行排查,我們把這些區域,稱為 hotspot,代碼中也是,這些 hotspot 里,潛藏著具有最高優先級的需要調整,優化與被重構的對象。

CodeCity 生成的“代碼城市”,每個街區是一個 package,每個 class 是一個建筑,建筑的高度是 class 中方法的數量(這個工具是 OOP 語言專用的,比如 java,我們后續不談論它)
我們的目的就是確認一個項目中的 hotspot 存在在哪兒。首先,我們不把代碼復雜度作為確認 hotspot 的唯一維度,代碼復雜度很有用,但是單一把復雜度作為 hotspot 衡量的指標,會存在一些問題,最主要的問題就是一段復雜的代碼,只有當我們真的需要關注并修改它的時候,它才是問題所在,如果沒有人需要閱讀或修改這段復雜的代碼,它具體的復雜度是多是少又有什么關系呢?即便有人覺得這種代碼是定時炸彈,但是一個具有一定規模的系統中存在復雜模塊是很正常的事情,一次性把這些復雜模塊都作為 hotspot 標注出來并不合理,我們對 hotspot 進行調整和優化也存在風險,以合適的策略去優化真正需要優化的部分是很重要的。
選擇維度
我們使用代碼更改的頻率,作為耗費開發人員和時間的第一個維度。以 React 為例,我們看下它在 15.0.0(2016 年 4 月 9 日 release)和 16.0.0(2017 年 9 月 27 日 release)之間,都做了哪些修改。為了保證時間的一致性,我們先把項目時間切到 2017 年 9 月 27 日:
- git checkout `git rev-list -n 1 --before="2017-09-27" master`
接下來我們看一下從 2016 年 4 月 9 日到 2017 年 9 月 27 日之間結構化的 git 日志:
- git log --pretty=format:'[%h] %an %ad %s' --date=short --numstat --before=2017-09-27 --after=2016-04-09 > react_evo.log

git 提交日志
在我們自己重定向的 git 的日志文件 react_evo.log 中,我們使用 code-maat 去獲取代碼變動頻率的數據,code-maat 使用 Clojure 編寫,用于挖掘和分析版本控制軟件中的項目代碼變動數據,我們把他 clone 下來,通過 leiningen 去編譯它的 jar 包執行,或者你也可以把它放在系統路徑中做成一個系統命令,我自己也制作了一個 code-maat 的 Docker 鏡像,地址是 code-maat。
我們來試著使用一下 code-maat,先看一下 React 在兩個版本之間的一些匯總數據
- java -jar code-maat/target/code-maat-1.1-SNAPSHOT-standalone.jar -l react_evo.log -c git -a summary

匯總情況
可以看到兩個版本之間提交次數,涉及文件的情況和開發者人數的信息。我們再來看一下代碼變動的頻率,并生成一個 csv 文件:
- java -jar ../../code-maat/target/code-maat-1.1-SNAPSHOT-standalone.jar -l react_evo.log -c git -a revisions > react_freqs.csv

文件的修改次數排序
有了這些數據,就縮小了候選 hotspot 的范圍,我們接下來,再增加另外一個判斷模塊規模的維度,即代碼行數,代碼行數這個維度簡單粗暴,并且有兩個好處,其一是能夠查找方便快速;另外就是對于不同編程語言,它是中立的指標。我們使用 cloc 作為通過代碼行數對項目進行分析的工具,它使用 Perl 編寫(誰說 Perl 死了?),并且能夠得到針對編程語言,文件,空格,注釋和代碼本身的很直觀的輸出,我們在 React 項目中來嘗試一下:
- cloc ./ --by-file --csv --quiet --report-file=react_lines.csv
在反應代碼行數的 react_lines.csv 中,我們能看到一個新的維度:

代碼行數排序
維度的合并以及可視化
單一維度對于我們來說,不足以說明某個可能存在 hotspot,接下來我們做一下維度的合并,我們把代碼更改的頻率和代碼行數合并,形成代碼修改次數 + 代碼行數的一個新的綜合維度,在這個維度中,代碼修改次數要比代碼行數擁有更多的權重。這個工作我們通過 Python 編寫腳本完成,Python 腳本的代碼位于 hotspots-helper。我們來看一下新的維度產生的數據:
python merge_comp_freqs.py ../complexity/data/react_freqs.csv ../complexity/data/react_lines.csv > react_hotspot_candidates.csv

代碼修改次數 + 代碼行數排序
這個維度非常清晰的表達了 React 從 15.0.0 到 16.0.0 的主要代碼改變,來源于 React fiber,fiber 帶來了大量的代碼和多次的修正,其中排名前三的有兩個都是渲染過程中 fiber 用來調度任務的部分。排名前幾位的代碼行數累計近萬,這么多代碼逐個去閱讀源碼還是很消耗精力的,不過通過這些數據,我們可以使用 D3.js 做一些可視化,通過 circle packing 算法,得到一個 enclosure diagram,圖中每個圓的直徑越大,代表這個模塊/文件代碼行數越多,顏色越深代表這個模塊/文件修改的次數越多,改動越頻繁:

React 代碼從 15.0.0 到 16.0.0 的可視化

hotspots 候選文件
具體可視化頁面參考 Complexity Study,我也做了一個 Docker 鏡像,地址是 youngleehua/complexity-study。
文件命名和再引入的復雜度分析
現在我們嘗試在這些潛在的 hotspots 中找出真正的問題,我們再增加一個維度,即文件命名。文件,類,函數的命名,能夠非常好的體現出開發者的設計意圖,通常情況下,我們閱讀代碼都是通過文件,類和函數的命名領會之前開發者的設計和思想的。命名不但能夠區分出哪些是配置文件,哪些是代碼本身,而且也能區分出文件所負責的職責,好的文件命名,能夠確認模塊的代碼職責,讓代碼內聚,盡可能的減少后續其他職責代碼的加入,避免邏輯變得過于復雜,增加修改次數和bug。例如相比 ReactFiberBeginWork.js,ReactDOMComponent.js 就是一個更好的命名,如果不去看 ReactFiberCommitWork.js 以及 ReactFiberCompleteWork.js 得出 Fiber 自己有個調度周期的結論,ReactFiberBeginWork.js 顯得不那么友好和清晰(誰都知道一個模塊需要一個初始的代碼),而通過文件命名,我們也能明顯的排除一些文件比如 package.json 這種項目的配置文件(不過配置文件產生問題的情況并不是沒有,參考大型C語言項目的Makefile)。
接下來,我們針對命名上并不友好以及在綜合維度上排名靠前的文件做一次代碼復雜度分析,并根據復雜度分析,確定代碼復雜度在歷史提交中的變化,以及為后續代碼的修改提供指導和意見。復雜度分析的手段目前看來也比較多,比如圈復雜度,也存在針對語言的復雜度分析工具,例如針對 Javascript 的 es-analysis/plato,但是為了做到語言的中立,形成一個通用的方案,我通過代碼的縮進來標志代碼文件的復雜度情況。
對于大多數語言來說(尤其對于C-like的語言),代碼縮進代表的是更深層次的邏輯,邏輯層次越深,需要控制的邏輯也就越復雜,可能產生的問題也就越多。下面兩個文件,你更希望自己維護哪一個?我更希望自己負責維護的是左邊的代碼:)

代碼的形狀
我們還是使用 Python,對代碼的縮進進行分析,我們把一個 tab 和 2 個空格(為了符合 React 代碼的風格)作為一個邏輯上的縮進,忽略掉空行,每個縮緊作為一個復雜度的得分(1 分),看一看整個文件的總復雜度,平均復雜度,復雜度的方差以及最大復雜度。
- python complexity_analysis.py ./react/src/renderers/shared/fiber/ReactFiberBeginWork.js

不算空行共 766 行,復雜度總分 1981,平均分 2.59,方差 1.29,最大得分 8 分
n 代表不計算空行一共的行數,共 766 行;total 代表文件復雜度的總分 1981 分,分值很高(如果橫向比對其他文件更明顯);mean 代表平均分表現還好為 2.59 分;sd代表方差得分越低表示有越多的行的復雜度得分接近平均分,得分 1.29 比較不錯,但是最大的行復雜度達到了 8 分,算是比較大的數值了。后續這個文件的職責能否被進一步單一化,把更多的邏輯剝離出來呢?這個就需要 React 的開發者們進行衡量了。

綜合維度排名第一的 ReactFiberScheduler.js 的得分
我們也可以通過版本控制(從最初的 react_evo.log 得到首尾的版本),得到一個代碼復雜度的變化信息:
- python ../hotspots-helper/git_complexity_trend.py --start 95fed0163 --end 9ce135f86 --file ./src/renderers/shared/fiber/ReactFiberBeginWork.js > complexity_trend.csv
通過 excel 我們看下 復雜度的變化趨勢:

ReactFiberBeginWork.js 復雜度在 15.0.0 版本到 16.0.0 版本之間的變化趨勢
有了這些數據以及可視化的體驗,我們不但能夠在不熟悉一個項目的情況下了解代碼的結構,找到可能的 hotspot,也能夠通過版本控制系統,得到具體文件質量的趨勢變化,從而能夠指導 code review,也能夠為后續代碼的重構指明方向。
結語
對于軟件質量,僅僅分析代碼中的 hotspot 還是不夠的,除了代碼層面隱藏的缺陷,我們這里還沒有分析更宏觀的架構上的演進,以及開發者與代碼之間社會學層面上的關系。在這篇之后,我做個預告,下一篇關于代碼和工程質量分析的文章我將嘗試從更宏觀的角度,闡述如何量化代碼架構,通過分析指導一個工程在架構上的迭代和重構,祝大家 Happy coding in 2018。