DevUI是一支兼具设计视角和工程视角的团队,服务于华为云DevCloud平台和华为内部数个中后台系统,服务于设计师和前端工程师。
官方网站:devui.design
Ng组件库:ng-devui(欢迎Star)
引言
重构,是我们开发过程中不可避免需要进行的一项工作。重构代码,以适配当前模块设计之初未考虑到的多样化场景,并增加模块的可维护性、健壮性、可测试性。那么,如何明确重构的方向,以及量化重构的结果呢?
代码圈复杂度(Cyclomatic complexity,CC)可以是一个供选择的指标。
什么是圈复杂度
圈复杂度(Cyclomatic complexity,CC)也称为条件复杂度或循环复杂度,是一种软件度量,是由老托马斯·J·麦凯布(Thomas J. McCabe, Sr.)在1976年提出,用来表示程序的复杂度,其符号为VG或是M。圈复杂度即程序的源代码中线性独立路径的个数。
为何要降低模块(函数)的圈复杂度
下表为模块(函数)圈复杂度与代码状况的一个基本对照表。除了表中给出的代码状况、可测性、维护成本等指标外,圈复杂度高的模块(函数),也对应着高软件复杂度、低内聚、高风险、低可读性。我们要降低模块(函数)的圈复杂度,就是要降低其软件复杂度、增加内聚性、减少可能出现的软件缺陷个数、增强可测试性、可读性。
降低软件复杂度
麦凯布提出圈复杂度时,其原始目的之一就是希望在软件开发过程中就限制其复杂度。他建议程序设计者需计算其开发模块的复杂度,若一模块的圈复杂度超过10,需再分割为更小的模块。NIST(国家标准技术研究所)的结构化测试方法论已此作法略作调整,在一些特定情形下,模块圈复杂度上限放宽到15会比较合适。此方法论也承认有些特殊情形下,模块的复杂度需要超过上述的上限,其建议为“模块的循环复杂度需在上限范围以内,否则需提供书面数据,说明为何此模块循环复杂度有必要超过上限。”
增加模块内聚性
优秀的代码模块间总是低耦合,高内聚的。可以预期一个复杂度较高模块的内聚性会比较低,至少不会到功能内聚性的程度。一个有高复杂度及低内聚性的模块中会有许多的决策点,这类的模块多半运行超过一个明确定义的任务,因此内聚性较低。
减少可能出现的软件缺陷个数
许多研究指出模块(函数)的圈复杂度和其中的缺陷个数有相关性,许多这类研究发现圈复杂度和缺陷个数有高度的正相关:圈复杂度最高的模块及方法,其中的缺陷个数也最多。
增强模块可测试性
一个圈复杂度高的模块(函数),由下文中将描述到的计算方法来看,必然会有更多的运行分支,要对这样的模块进行如单元测试用例的编写,将会十分复杂,并且后期用例维护也是一个问题。
增强代码可读性
代码可读性是大型项目与团队协作间必须要考虑的一个因素。圈复杂度高的模块(函数),随着逻辑复杂度的增加,代码可读性也将降低,不利于成员间相互协作与后期维护。
圈复杂度如何计算
计算方法
一段程序的圈复杂度是其线性独立路径的数量。若程序中没有像IF指令或FOR循环的控制流程,因为程序中只有一个路径,其圈复杂度为1,若程序中有一个IF指令,会有二个不同路径,分别对应IF条件成立及不成立的情形,因此圈复杂度为2。
数学上,一个结构化程序的圈复杂度是利用程序的控制流图来定义,控制流图是一个有向图,图中的节点为程序的基础模块,若一个模块结束后,可能会运行另一个模块,则用箭头链接二个模块,并标示可能的运行顺序。圈复杂度M可以用下式定义:
M = E − N + 2P
其中
E 为图中边的个数
N 为图中节点的个数
P 为连接组件的个数
度量工具
CodeMetrics
一款VSCode插件,用于度量TS、JS代码圈复杂度。
ESLint
eslint
也可以配置关于圈复杂度的规则,如:
rules: {
complexity: [
'error',
{
max: 15
}
]
}
代表了当前每个函数
的最高圈复杂度
为15,否则eslint
将给出错误提示
。
conard cc
一款开源的代码圈复杂度检测工具(github:https://github.com/ConardLi/awesome-cli),可以生成当前项目下代码圈复杂度报告。
如何降低模块(函数)圈复杂度
常用结构圈复杂度
要降低圈复杂度,我们就需要了解是哪些语句哪些结构导致了我们复杂度的增加,以下为常见结构圈复杂度说明。
顺序结构
顺序结构复杂度为1。
例:
function func() {
let a = 1, b = 1, c;
c = a * b;
}
如上代码,func函数内部为顺序结构,其控制流图如下:
边:1,点:2,连通分支:1,
圈复杂度:
M = 1 - 2 + 2 * 1 = 1
if-else-else、switch-case
每增加一个分支,复杂度增加1,&& 、|| 运算也为一个分支。
例:
function func() {
let a = 1, b = 1, c;
if (a = 1) {
c = a + b;
} else {
c = a - b;
}
}
边:4,点:4,连通分支:1,
圈复杂度:
M = 4 - 4 + 2 * 1 = 2
循环结构
增加一个循环结构,复杂度增加1。、
例:
function func() {
let a = 1, b = 1, c = 2;
for (let i = 1; i < 10; i++) {
b += a;
}
c = a + b;
}
边:4,点:4,连通分支:1,
圈复杂度:
M = 4 - 4 + 2 * 1 = 2
return
从理论上来讲,return并不会增加当前模块圈复杂度,但在某些度量工具看来,一条return语句将增加整体程序的一条路径,并且如果提前返回,将增加程序的不确定性,所以在大多数计算工具中,每增加一条return语句,复杂度将加1。
常用降低模块(函数)圈复杂度方法
1. 函数提炼与拆分,单一职责(推荐)
既然是降低一个模块(函数)圈复杂度,那么对于复杂度极高的函数,首先需要进行就是功能的提炼与函数拆分,每个函数职责要单一。
例:
function add(a, b) {
let tempA;
if (a === 10) {
tempA = 11;
} else if (a === 12) {
tempA = 12;
}
let tempB;
if (b === 10) {
tempB = 13;
} else if (b === 12) {
tempB = 12;
}
return tempB + tempA;
}
重构为:
function add(a, b) {
return calcA(a) + calcB(b);
}
function calcA(a) {
if (a === 10) {
return 11;
} else if (a === 12) {
return 12;
}
}
function calcB(b) {
if (b === 10) {
return 13;
} else if (b === 12) {
return 12;
}
}
不仅降低了add函数圈复杂度,并且代码结构更加清晰,增加了可读性,同时还增加了当前代码可维护性、可测试性。
当然,过犹不及,我们的目标为提炼函数,保持函数单一职责,不能为了降低圈复杂度而进行暴力拆分。
2. 优化算法(减少不必要条件、循环分支)
从圈复杂度计算上来看,条件、循环分支均会增加模块圈复杂度。从某些程度上,复杂的条件与循环结构是可优化,减少不必要结构,从而降低圈复杂度。
例:
let a = 'a', c;
if (a === 'a') {
c = a + 1;
} else if (a === 'b') {
c = a + 2;
} else if (a === 'c') {
c = a + 3;
} else if (a === 'd') {
c = a + 4;
}
return c;
重构为:
let a = 'a', c;
let conditionMap = {
a: 1,
b: 2,
c: 3,
d: 4
}
c = a + conditionMap[a];
return c;
消除了所有条件分支,从而大幅降低了当前函数圈复杂度。
3. 表达式逻辑优化
逻辑计算也将增加圈复杂度,优化一些结构复杂的逻辑表达式,减少不必要的逻辑判断,也将一定程度上降低圈复杂度。
例:
a && b || a && c
可进行简单优化为:
a && (b || c)
从而使表达式圈复杂度降低1。
5. 减少提前return(此方法需辩证看待)
单从降低圈复杂度上来看,由于当前大多数圈复杂度计算工具将对return个数进行计算,故若要针对这些工具衡量规则进行优化,减少return语句个数也为一种方式。
例:
let a = 1, b = 1;
if (a = 1) {
return a + b;
} else {
return a - b;
}
重构为:
let a = 1, b = 1, c;
if (a = 1) {
c = a + b;
} else {
c = a - b;
}
return c;
圈复杂度将降低1。
总结
圈复杂度(Cyclomatic complexity,CC)高的代码一定不是好代码,对于我们代码好坏的衡量,圈复杂度可以作为一个参考指标;可以通过控制流图计算圈复杂度;要降低模块(函数)圈复杂度,提炼拆分函数、优化算法、优化逻辑表达式均为可以尝试的方法。
参考
循环复杂度:https://zh.wikipedia.org/wiki/%E5%BE%AA%E7%92%B0%E8%A4%87%E9%9B%9C%E5%BA%A6
详解圈复杂度:http://kaelzhang81.github.io/2017/06/18/%E8%AF%A6%E8%A7%A3%E5%9C%88%E5%A4%8D%E6%9D%82%E5%BA%A6/
前端代码质量-圈复杂度原理和实践:https://juejin.im/post/5da34216e51d4578502c24c5
加入我们
我们是DevUI团队,欢迎来这里和我们一起打造优雅高效的人机设计/研发体系。招聘邮箱:muyang2@huawei.com。
文/DevUI 砰砰砰砰
往期文章推荐
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。