软件度量-圈复杂度

· 3 min read

最近在做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

{ ‘complexity’: [’error’, { ‘max’: 20 }] } ```

  • checkStyle

    
    <?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,地址戳这里


       /**
         * 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,重视圈复杂度,这是个很好的指标来督促我们写出易读的代码
  • 圈复杂度的意义是在缺陷成为缺陷之前捕获它们。

参考链接