图0:代码质量 - 代码的历史是代码未来的预言

We can never see past the choices we don’t understand. – Oracle (The Matrix)

副标题是:“一个使用 code-maat, git, Python, D3.js 进行代码质量衡量的 case study“。

大概是在今年6月份吧,我当时在工作中作为一些小项目的负责人,收到了同事的一份报表,主要内容是通过 jsinspect ,检测出了有几个项目代码重复率比较高,提醒我是否有些项目需要优化或部分重构一下。我当时第一次萌生了作为周期性项目,代码的质量是应该作为一个指标随着迭代被量化,从而指导后续项目迭代的,虽然当时我听说过 sonar。随后我工作中参与了一个中心化代码质量评估系统一期的设计和开发(主要是针对 javascript 代码的规范和质量检测与持续集成进行耦合),但是当时这个系统没有解决这样一个问题,即能够告诉我代码里具体哪部分存在更高优先级被重构的需要。半年过去了,现在我尝试回答一下这个问题,并举一个与编程语言无关的一个通用可行的例子,通过这个例子揭示一个事实:

代码的历史,是代码未来的预言

如果把项目代码比喻成城市,那么系统中维护成本高的复杂部分就好比潜伏在城市中的罪犯,我们对于罪犯的搜查,显然不能对城市进行地毯式的搜索,而是应该找到之前有过犯罪记录的一些街区,划分重点区域进行排查,我们把这些区域,称为 hotspot,代码中也是,这些 hotspot 里,潜藏着具有最高优先级的需要调整,优化与被重构的对象。

图2:代码质量 - 代码的历史是代码未来的预言

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

图3:代码质量 - 代码的历史是代码未来的预言

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

图4:代码质量 - 代码的历史是代码未来的预言

汇总情况

可以看到两个版本之间提交次数,涉及文件的情况和开发者人数的信息。我们再来看一下代码变动的频率,并生成一个 csv 文件:

java -jar ../../code-maat/target/code-maat-1.1-SNAPSHOT-standalone.jar -l react_evo.log -c git -a revisions > react_freqs.csv

图5:代码质量 - 代码的历史是代码未来的预言

文件的修改次数排序

有了这些数据,就缩小了候选 hotspot 的范围,我们接下来,再增加另外一个判断模块规模的维度,即代码行数,代码行数这个维度简单粗暴,并且有两个好处,其一是能够查找方便快速;另外就是对于不同编程语言,它是中立的指标。我们使用 cloc 作为通过代码行数对项目进行分析的工具,它使用 Perl 编写(谁说 Perl 死了?),并且能够得到针对编程语言,文件,空格,注释和代码本身的很直观的输出,我们在 React 项目中来尝试一下:

cloc ./ --by-file --csv --quiet --report-file=react_lines.csv

在反应代码行数的 react_lines.csv 中,我们能看到一个新的维度:

图6:代码质量 - 代码的历史是代码未来的预言

代码行数排序

维度的合并以及可视化

单一维度对于我们来说,不足以说明某个可能存在 hotspot,接下来我们做一下维度的合并,我们把代码更改的频率和代码行数合并,形成代码修改次数 + 代码行数的一个新的综合维度,在这个维度中,代码修改次数要比代码行数拥有更多的权重。这个工作我们通过 Python 编写脚本完成,Python 脚本的代码位于 hotspots-helper。我们来看一下新的维度产生的数据:

python merge_comp_freqs.py ../complexity/data/react_freqs.csv ../complexity/data/react_lines.csv > react_hotspot_candidates.csv

图7:代码质量 - 代码的历史是代码未来的预言

代码修改次数 + 代码行数排序

这个维度非常清晰的表达了 React 从 15.0.0 到 16.0.0 的主要代码改变,来源于 React fiber,fiber 带来了大量的代码和多次的修正,其中排名前三的有两个都是渲染过程中 fiber 用来调度任务的部分。排名前几位的代码行数累计近万,这么多代码逐个去阅读源码还是很消耗精力的,不过通过这些数据,我们可以使用 D3.js 做一些可视化,通过 circle packing 算法,得到一个 enclosure diagram,图中每个圆的直径越大,代表这个模块/文件代码行数越多,颜色越深代表这个模块/文件修改的次数越多,改动越频繁:

图8:代码质量 - 代码的历史是代码未来的预言

React 代码从 15.0.0 到 16.0.0 的可视化

图9:代码质量 - 代码的历史是代码未来的预言

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的语言),代码缩进代表的是更深层次的逻辑,逻辑层次越深,需要控制的逻辑也就越复杂,可能产生的问题也就越多。下面两个文件,你更希望自己维护哪一个?我更希望自己负责维护的是左边的代码:)

图10:代码质量 - 代码的历史是代码未来的预言

代码的形状

我们还是使用 Python,对代码的缩进进行分析,我们把一个 tab 和 2 个空格(为了符合 React 代码的风格)作为一个逻辑上的缩进,忽略掉空行,每个缩紧作为一个复杂度的得分(1 分),看一看整个文件的总复杂度,平均复杂度,复杂度的方差以及最大复杂度。

python complexity_analysis.py ./react/src/renderers/shared/fiber/ReactFiberBeginWork.js 

图11:代码质量 - 代码的历史是代码未来的预言

不算空行共 766 行,复杂度总分 1981,平均分 2.59,方差 1.29,最大得分 8 分

n 代表不计算空行一共的行数,共 766 行;total 代表文件复杂度的总分 1981 分,分值很高(如果横向比对其他文件更明显);mean 代表平均分表现还好为 2.59 分;sd代表方差得分越低表示有越多的行的复杂度得分接近平均分,得分 1.29 比较不错,但是最大的行复杂度达到了 8 分,算是比较大的数值了。后续这个文件的职责能否被进一步单一化,把更多的逻辑剥离出来呢?这个就需要 React 的开发者们进行衡量了。

图12:代码质量 - 代码的历史是代码未来的预言

综合维度排名第一的 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 我们看下 复杂度的变化趋势:

图13:代码质量 - 代码的历史是代码未来的预言

ReactFiberBeginWork.js 复杂度在 15.0.0 版本到 16.0.0 版本之间的变化趋势

有了这些数据以及可视化的体验,我们不但能够在不熟悉一个项目的情况下了解代码的结构,找到可能的 hotspot,也能够通过版本控制系统,得到具体文件质量的趋势变化,从而能够指导 code review,也能够为后续代码的重构指明方向。

结语

对于软件质量,仅仅分析代码中的 hotspot 还是不够的,除了代码层面隐藏的缺陷,我们这里还没有分析更宏观的架构上的演进,以及开发者与代码之间社会学层面上的关系。在这篇之后,我做个预告,下一篇关于代码和工程质量分析的文章我将尝试从更宏观的角度,阐述如何量化代码架构,通过分析指导一个工程在架构上的迭代和重构,祝大家 Happy coding in 2018。

余下全文(1/3)

本文最初发表在zhuanlan.zhihu.com,文章内容属作者个人观点,不代表本站立场。

分享这篇文章:

请关注我们:

发表评论

电子邮件地址不会被公开。 必填项已用*标注