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

CodeCity 生成的“代碼城市”,每個(gè)街區(qū)是一個(gè) package,每個(gè) class 是一個(gè)建筑,建筑的高度是 class 中方法的數(shù)量(這個(gè)工具是 OOP 語(yǔ)言專(zhuān)用的,比如 java,我們后續(xù)不談?wù)撍?
我們的目的就是確認(rèn)一個(gè)項(xiàng)目中的 hotspot 存在在哪兒。首先,我們不把代碼復(fù)雜度作為確認(rèn) hotspot 的唯一維度,代碼復(fù)雜度很有用,但是單一把復(fù)雜度作為 hotspot 衡量的指標(biāo),會(huì)存在一些問(wèn)題,最主要的問(wèn)題就是一段復(fù)雜的代碼,只有當(dāng)我們真的需要關(guān)注并修改它的時(shí)候,它才是問(wèn)題所在,如果沒(méi)有人需要閱讀或修改這段復(fù)雜的代碼,它具體的復(fù)雜度是多是少又有什么關(guān)系呢?即便有人覺(jué)得這種代碼是定時(shí)炸彈,但是一個(gè)具有一定規(guī)模的系統(tǒng)中存在復(fù)雜模塊是很正常的事情,一次性把這些復(fù)雜模塊都作為 hotspot 標(biāo)注出來(lái)并不合理,我們對(duì) hotspot 進(jìn)行調(diào)整和優(yōu)化也存在風(fēng)險(xiǎn),以合適的策略去優(yōu)化真正需要優(yōu)化的部分是很重要的。
選擇維度
我們使用代碼更改的頻率,作為耗費(fèi)開(kāi)發(fā)人員和時(shí)間的第一個(gè)維度。以 React 為例,我們看下它在 15.0.0(2016 年 4 月 9 日 release)和 16.0.0(2017 年 9 月 27 日 release)之間,都做了哪些修改。為了保證時(shí)間的一致性,我們先把項(xiàng)目時(shí)間切到 2017 年 9 月 27 日:
- git checkout `git rev-list -n 1 --before="2017-09-27" master`
接下來(lái)我們看一下從 2016 年 4 月 9 日到 2017 年 9 月 27 日之間結(jié)構(gòu)化的 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 去獲取代碼變動(dòng)頻率的數(shù)據(jù),code-maat 使用 Clojure 編寫(xiě),用于挖掘和分析版本控制軟件中的項(xiàng)目代碼變動(dòng)數(shù)據(jù),我們把他 clone 下來(lái),通過(guò) leiningen 去編譯它的 jar 包執(zhí)行,或者你也可以把它放在系統(tǒng)路徑中做成一個(gè)系統(tǒng)命令,我自己也制作了一個(gè) code-maat 的 Docker 鏡像,地址是 code-maat。
我們來(lái)試著使用一下 code-maat,先看一下 React 在兩個(gè)版本之間的一些匯總數(shù)據(jù)
- java -jar code-maat/target/code-maat-1.1-SNAPSHOT-standalone.jar -l react_evo.log -c git -a summary

匯總情況
可以看到兩個(gè)版本之間提交次數(shù),涉及文件的情況和開(kāi)發(fā)者人數(shù)的信息。我們?cè)賮?lái)看一下代碼變動(dòng)的頻率,并生成一個(gè) csv 文件:
- java -jar ../../code-maat/target/code-maat-1.1-SNAPSHOT-standalone.jar -l react_evo.log -c git -a revisions > react_freqs.csv

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

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

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

React 代碼從 15.0.0 到 16.0.0 的可視化

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

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

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

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

ReactFiberBeginWork.js 復(fù)雜度在 15.0.0 版本到 16.0.0 版本之間的變化趨勢(shì)
有了這些數(shù)據(jù)以及可視化的體驗(yàn),我們不但能夠在不熟悉一個(gè)項(xiàng)目的情況下了解代碼的結(jié)構(gòu),找到可能的 hotspot,也能夠通過(guò)版本控制系統(tǒng),得到具體文件質(zhì)量的趨勢(shì)變化,從而能夠指導(dǎo) code review,也能夠?yàn)楹罄m(xù)代碼的重構(gòu)指明方向。
結(jié)語(yǔ)
對(duì)于軟件質(zhì)量,僅僅分析代碼中的 hotspot 還是不夠的,除了代碼層面隱藏的缺陷,我們這里還沒(méi)有分析更宏觀的架構(gòu)上的演進(jìn),以及開(kāi)發(fā)者與代碼之間社會(huì)學(xué)層面上的關(guān)系。在這篇之后,我做個(gè)預(yù)告,下一篇關(guān)于代碼和工程質(zhì)量分析的文章我將嘗試從更宏觀的角度,闡述如何量化代碼架構(gòu),通過(guò)分析指導(dǎo)一個(gè)工程在架構(gòu)上的迭代和重構(gòu),祝大家 Happy coding in 2018。