代码质量-圈复杂度

背景

圈复杂度是 oasis(衡量前端工程质量平台)的检测指标之一,目的是为了检测出逻辑复杂,可能质量低,且难于测试和维护的模块,提高 CR 效率和推动优化提升前端工程质量。

概念

圈复杂度(Cyclomatic complexity,CC)也称为条件复杂度,其符号为 V(G),是衡量计算机程序复杂程度的一种措施。它根据程序从开始到结束的线性独立路径的数量计算得来的,其数量上为独立路径的条数,也可理解为覆盖所有的可能情况最少使用的测试用例个数。

衡量标准

圈复杂度大说明程序代码的判断逻辑复杂,可能质量低,且难于测试和维护。 代码复杂度低,代码不一定好,但代码复杂度高,代码一定不好。

圈复杂度 代码状况 可测性 维护成本
1 - 10 清晰
10 - 20 复杂
20 - 30 非常复杂
> 30 不可读 不可测 非常高

同时测试驱动的开发和圈复杂度存在紧密的关系。一个好的测试用例是创建数量与被测代码圈复杂度值相等的测试用例,以此提升测试用例对代码的分支覆盖率。

计算方法

圈复杂度有两种计算方法:点边计算法和节点判定法。

节点判定法

因为圈复杂度所反映的是“判定条件”的数量,所以圈复杂度实际上就是等于判定节点的数量再加上 1。从 1 开始,一直往下通过程序,一但遇到 P 关键字,或者其它同类的词,就加 1。

V (G) = P + 1

其中 P 为判定节点数,常见的判定节点有:

if 语句、while 语句、for 语句、case 语句、catch 语句、and 和 or 布尔操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function test(a) {
let b = 1;
if (a < 0 || a > 100) {
a = 1;
}
for (let i = 0; i < 10; i++) {
a++;
}
while (a < 30 ) {
a++;
}
switch (a) {
case 31:
b++ ;
break;
default:
break;
}
return b === 1 ? 0 : b;
}

1 个 if ,1 个 or ,1 个 for ,1 个 while,1 个 case,1 个三元 所以代码复杂度 7

点边计算法

圈复杂度由程序的控制流图来计算:节点对应程序中个别的代码,而若一个程序运行后会立刻运行另一代码,则会有边连接另一代码对应的节点。

V(G) = E - N + 2

E 表示控制流图中边的数量,N 表示控制流图中节点的数量。

如上图正常顺序的圈复杂度为 1;if else 的圈复杂度为 2;while 的圈复杂度也为 2。

如何检测

es6-plato

1
npm install --save-dev es6-plato
1
2
3
"scripts" : {
"complexity-report": "./node_modules/.bin/es6-plato -r -d ./report src",
}
1
npm run complexity-report

eslint

oasis 用的是 eslint 提供了检测代码圈复杂度的 rules ,根据 eslint 的 api 会自动检测出所有函数的代码复杂度输出检测信息,通过解析检测信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
new CLIEngine({
baseConfig: {
root: true,
parser: "vue-eslint-parser",
},
parserOptions: {
ecmaVersion: 9,
sourceType: "module",
ecmaFeatures: {
experimentalObjectRestSpread: true,
jsx: true,
globalReturn: true,
impliedStrict: true,
},
parser: "babel-eslint",
},
useEslintrc: false,
rules: {
complexity: ["error", { max: 0 }],
},
});

降低圈复杂度

降低复杂度有的是把函数的一部分提取成另一个子函数,不会降低整个项目的复杂度,只是把决策点移到其他地方,但是这样做可以降低你在同一时间必须关注的复杂度。

单一职责

函数应该做一件事,做好这件事,只做这一件事。 — 代码整洁之道

1
2
3
4
5
6
7
8
9
10
11
12
13
function add(a, b) {
if (a === 10) {
// dosomething
} else if (a === 12) {
// dosomething
}
if (b === 10) {
// dosomething
} else if (b === 12) {
// dosomething
}
return a + b;
}

转换后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function add(a, b) {
return calcA(a) + calcB(b);
}
function calcA(a) {
if (a === 10) {
return; // dosomething
} else if (a === 12) {
return; // dosomething
}
}
function calcB(b) {
if (b === 10) {
return; // dosomething
} else if (b === 12) {
return; // dosomething
}
}

查找表

1
2
3
4
5
6
7
8
9
10
11
12
13
function demo (a, b, c) {
if (f(a, b, c)) {
if (g(a, b, c)) {
// ...
}else if (h(a, b, c)) {
// ...
}
} else if (j(a, b, c)) {
// ...
} else if (k(a, b, c)) {
// ...
}
}

转换后

1
2
3
4
5
6
7
8
9
10
11
const rules = {
x: function (a, b, c) {},
y: function (a, b, c) {},
z: function (a, b, c) {}
...
}

function demo (a, b, c) {
const action = determineAction(a, b, c)
return rules[action](a, b, c)
}

职责链

1
2
3
4
5
6
7
8
9
10
11
12
13
function demo (a, b, c) {
if (f(a, b, c)) {
if (g(a, b, c)) {
// ...
}else if (h(a, b, c)) {
// ...
}
} else if (j(a, b, c)) {
// ...
} else if (k(a, b, c)) {
// ...
}
}

转换后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const rules = [
{
match: function (a, b, c) {},
action: function (a, b, c) {},
},
{
match: function (a, b, c) {},
action: function (a, b, c) {},
},
{
match: function (a, b, c) {},
action: function (a, b, c) {},
},
];
function demo(a, b, c) {
for (let i = 0; i < rules.length; i++) {
if (rules[i].match(a, b, c)) {
return rules[i].action(a, b, c);
}
}
}

总结

圈复杂度低,代码不一定好,但圈复杂度很高,代码一定不好,所以圈复杂度才作为一个 oasis 的指标之一。圈复杂度还需要具体情况具体分析,其只作为重构的一个度量指标,作为决策的一个参考依据。

oasisV0.1 beta 版本目前已经完成大文件、重复代码块,圈复杂度、最佳实践、基础库落地、 npm 依赖库的安全分析功能,接入 0 成本目前已经上线欢迎大家体验、建议、吐槽。