软件度量-圈复杂度

最近在做ESLint规则配置,重新注意了下complexity规则,之前对该规则不够重视,理解有盲区,于是系统学习,这里mark下。

概念

循环复杂度 Cyclomatic complexity也称为条件复杂度圈复杂度,是一种软件度量,是由老托马斯·J·麦凯布在1976年提出,用来表示程序的复杂度

圈复杂度计算

公式

M = E − N + 2P
其中

  • E 为图中边的个数
  • N 为图中节点的个数
  • P 为连接组件的个数[程序的个数,对于单一的程序,P恒为1],如下图是1

如图,E = 9, N = 8, P = 1,因此其循环复杂度为 9 - 8 + (2*1) = 3

以上是标准的公式,但是简单的办法实际上是判定节点数量再加一

节点判定

个人喜欢这样直接算,较为简单

判定节点

  • if语句
  • conditional语句,比如?:
  • for语句
  • while语句
  • try语句
  • switch/case语句

注意

  • 从1开始,一直往下通过程序
  • 遇到以下关键字,或者其它同类的词,就加1:if,while,repeat,for,and,or
  • if-else-if,switch-case语句中的每一种情况都加1

圈复杂度参考值

这里贴下圈值与风险值对比表格。可以看出20是个已经在维护性边缘,当然10最佳。

  • 01 to 10 – Minimal Risk, Easy to maintain
  • 11 to 20 – Moderate Risk, Harder to maintain
  • 21 to 50 – High Risk,Candidates for refactoring/redesign
  • Over 50 – Very High Risk

注意

  • eslint中默认值是20,checkstype中默认值是10
  • 圈复杂度小不一定意味着代码好,但是太大,代码一定不好,不具备维护性,这点需要明确

检测手段

开发中为了确保代码风格的统一,我们往往通过eslint,checkStyle等工具检测代码,对于不满足的规范报错提示。当前这些检测工具都支持了圈复杂度

  • eslint-rules

    1
    2
    3
    {
    'complexity': ['error', { 'max': 20 }]
    }
  • checkStyle

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    <?xml version="1.0"?>
    <!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd">
    <module name = "Checker">
    <property name="charset" value="UTF-8"/>
    <module name="TreeWalker">
    <module name="CyclomaticComplexity">
    <property name="max" value="10"/>
    </module>
    </module>
    </module>

重构手法

面对圈值过高的函数,我们要做的是重构来解决,具体如何处理呢。

  • 提炼函数,做到函数职责单一,降低单个函数的复杂度,圈值自然降低
  • switch,if else使用的多时,可以考虑多态,考虑map映射来解决多分支问题
  • 简化逻辑判断,不断的逻辑合并收拢,有时会发现重复。

ESLint中圈复杂度统计源码分析

这里通过ESLint中相关rule了解下实现原理

eslint/lib.rules/complexity.js,地址戳这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

/**
* Increase the switch complexity in context
* @param {ASTNode} node node to evaluate
* @returns {void}
* @private
*/
function increaseSwitchComplexity(node) {

// Avoiding `default`
if (node.test) {
increaseComplexity();
}
}

return {
FunctionDeclaration: startFunction,
FunctionExpression: startFunction,
ArrowFunctionExpression: startFunction,
"FunctionDeclaration:exit": endFunction,
"FunctionExpression:exit": endFunction,
"ArrowFunctionExpression:exit": endFunction,

CatchClause: increaseComplexity,
ConditionalExpression: increaseComplexity,
LogicalExpression: increaseComplexity,
ForStatement: increaseComplexity,
ForInStatement: increaseComplexity,
ForOfStatement: increaseComplexity,
IfStatement: increaseComplexity,
SwitchCase: increaseSwitchComplexity,
WhileStatement: increaseComplexity,
DoWhileStatement: increaseComplexity
};

计算办法就是,根据AST树,找到这些判定节点,进行对应的复杂度计算即可。如上,switch/case中default不算。

写在最后

  • 记得以前看书,印象深的一句话,代码易读性是为了人,面对运行代码的机器,怎么写理论上都行,但问题是人要读和看,那么易读性,可维护性就显得尤为重要。so,重视圈复杂度,这是个很好的指标来督促我们写出易读的代码
  • 圈复杂度的意义是在缺陷成为缺陷之前捕获它们。

参考链接