作者 | ConardLi
责编 | maozz
出品 | CSDN(ID:CSDNnews)
写程序时时刻记着,这个将来要维护你写的程序的人是一个有严重暴力倾向,并且知道你住在哪里的精神变态者。
导读
你们是否也有过下面的想法?
重构一个项目还不如新开发一个项目...
这代码是谁写的,我真想...
你们的项目中是否也存在下面的问题?
单个项目也越来越庞大,团队成员代码风格不一致,无法对整体的代码质量做全面的掌控
没有一个准确的标准去衡量代码结构复杂的程度,无法量化一个项目的代码质量
重构代码后无法立即量化重构后代码质量是否提升
针对上面的问题,本文的主角 圈复杂度 重磅登场,本文将从圈复杂度原理出发,介绍圈复杂度的计算方法、如何降低代码的圈复杂度,如何获取圈复杂度,以及圈复杂度在公司项目的实践应用。
圈复杂度
2.1 定义
圈复杂度 (Cyclomatic complexity) 是一种代码复杂度的衡量标准,也称为条件复杂度或循环复杂度,它可以用来衡量一个模块判定结构的复杂程度,数量上表现为独立现行路径条数,也可理解为覆盖所有的可能情况最少使用的测试用例数。简称 CC 。其符号为 VG 或是 M 。
圈复杂度 在 1976 年由 Thomas J. McCabe, Sr. 提出。
圈复杂度大说明程序代码的判断逻辑复杂,可能质量低且难于测试和维护。程序的可能错误和高的圈复杂度有着很大关系。
2.2 衡量标准
代码复杂度低,代码不一定好,但代码复杂度高,代码一定不好。
圈复杂度代码状况可测性维护成本
计算方法
3.1 控制流程图
控制流程图,是一个过程或程序的抽象表现,是用在编译器中的一个抽象数据结构,由编译器在内部维护,代表了一个程序执行过程中会遍历到的所有路径。它用图的形式表示一个过程内所有基本块执行的可能流向, 也能反映一个过程的实时执行过程。
下面是一些常见的控制流程:
 
3.2 节点判定法
有一个简单的计算方法,圈复杂度实际上就是等于判定节点的数量再加上1。向上面提到的:ifelse 、 switchcase 、 for循环、三元运算符等等,都属于一个判定节点,例如下面的代码
function
testComplexity(*param*) {
let result = 1;
if(param > 0) {
result--;
}
for(let i = 0; i < 10; i++) {
result += Math.random();
}
switch(parseInt(result)) {
case1:
result += 20;
break;
case2:
result += 30;
break;
default:
result += 10;
break;
}
return result > 20? result : result;
}
 
     上面的代码中一共有 1个 if语句,一个 for循环,两个 case语句,一个元运算符,所以代码复杂度为 4+1+1=6。另外,需要注意的是 ||和&& 语句也会被算作一个判定节点,例如下面代码的代码复杂为 3:
function
 testComplexity(*param*) {
    let result = 1;
if(param > 0&& param < 10) {
        result--;
}
return result;
}
 
   3.3 点边计算法
M = E − N + 2P
E:控制流图中边的数量
N:控制流图中的节点数量
P:独立组件的数目
前两个,边和节点都是数据结构图中最基本的概念:
 P代表图中独立组件的数目,独立组件是什么意思呢?来看看下面两个图,左侧为连通图,右侧为非连通图:
连通图:对于图中任意两个顶点都是连通的
 一个连通图即为图中的一个独立组件,所以左侧图中独立组件的数目为1,右侧则有两个独立组件。
对于我们的代码转化而来的控制流程图,正常情况下所有节点都应该是连通的,除非你在某些节点之前执行了 return,显然这样的代码是错误的。所以每个程序流程图的独立组件的数目都为1,所以上面的公式还可以简化为 M=E−N+2 。
降低代码的圈复杂度
我们可以通过一些代码重构手段来降低代码的圈复杂度。
重构需谨慎,示例代码仅仅代表一种思想,实际代码要远远比示例代码复杂的多。
4.1 抽象配置
通过抽象配置将复杂的逻辑判断进行简化。例如下面的代码,根据用户的选择项执行相应的操作,重构后降低了代码复杂度,并且如果之后有新的选项,直接加入配置即可,而不需要再去深入代码逻辑中进行改动:
 
4.2 单一职责 - 提炼函数
单一职责原则 (SRP):每个类都应该有一个单一的功能,一个类应该只有一个发生变化的原因。
在 JavaScript 中,需要用到的类的场景并不太多,单一职责原则则是更多地运用在对象或者方法级别上面。
函数应该做一件事,做好这件事,只做这一件事。— 代码整洁之道
关键是如何定义这 “一件事” ,如何将代码中的逻辑进行抽象,有效的提炼函数有利于降低代码复杂度和降低维护成本。
 
4.3 使用 break 和 return 代替控制标记
我们经常会使用一个控制标记来标示当前程序运行到某一状态,很多场景下,使用 break 和 return 可以代替这些标记并降低代码复杂度。
4.4 用函数取代参数
setField 和 getField 函数就是典型的函数取代参数,如果么有 setField、getField 函数,我们可能需要一个很复杂的 setValue、getValue 来完成属性赋值操作:
4.5 简化条件判断 - 逆向条件
某些复杂的条件判断可能逆向思考后会变的更简单。
4.6 简化条件判断 -合并条件
将复杂冗余的条件判断进行合并。
4.7 简化条件判断 - 提取条件
将复杂难懂的条件进行语义化提取。
 
圈复杂度检测方法
5.1 eslint规则
eslint提供了检测代码圈复杂度的 rules:
我们将开启 rules 中的 complexity 规则,并将圈复杂度大于 0 的代码的 rule severity 设置为 warn 或 error 。
rules: {
complexity: [
'warn',
{ max: 0}
]
}
 
     这样 eslint 就会自动检测出所有函数的代码复杂度,并输出一个类似下面的 message。
Method
'testFunc' has a complexity of 12.Maximum allowed is 0
Asyncfunction has a complexity of 6.Maximum allowed is 0.
...
 
   5.2 CLIEngine
我们可以借助 eslint 的 CLIEngine ,在本地使用自定义的 eslint 规则扫描代码,并获取扫描结果输出。
初始化 CLIEngine :
const
eslint = require('eslint');
const{ CLIEngine} = eslint;
const cli = newCLIEngine({
parserOptions: {
ecmaVersion: 2018,
},
rules: {
complexity: [
'error',
{ max: 0}
]
}
});
 
        
   
     
    
    
      const
    
    
      
reports = cli.executeOnFiles([
    
    
      '.']).results;
    
    
      
    
    
      
    
    
      for(
    
    
      let i = 
    
    
      0; i < reports.length; i++) {
    
    
      
    
    
      
    
    
      const{ messages } = reports[i];
    
    
      
    
    
      
    
    
      for(
    
    
      let j = 
    
    
      0; j < messages.length; j++) {
    
    
      
    
    
      
    
    
      const{ message, ruleId } = messages[j];
    
    
      
    
    
      
    
    
      if(ruleId === 
    
    
      'complexity') {
    
    
      
    
    
      
    
    
      console.log(message);
    
    
      
    
    
      
}
    
    
      
    
    
      
}
    
    
      
    
    
      
}
    
    
      
   
   
       
        
     
       
      
      
        function
func1() {
      
      
        
      
      
        
      
      
        console.log(
      
      
        1);
      
      
        
      
      
        
}
      
      
        
      
      
        
      
      
        
      
      
        
      
      
        const func2 = 
      
      
        () => {
      
      
        
      
      
        
      
      
        console.log(
      
      
        2);
      
      
        
      
      
        
};
      
      
        
      
      
        
      
      
        
      
      
        
classTestClass{
      
      
        
      
      
        
func3() {
      
      
        
      
      
        
      
      
        console.log(
      
      
        3);
      
      
        
      
      
        
}
      
      
        
      
      
        
}
      
      
        
      
      
        
      
      
        
      
      
        
asyncfunction func4() {
      
      
        
      
      
        
      
      
        console.log(
      
      
        1);
      
      
        
      
      
        
}
      
      
        
     
     
         
        
   
     
      
    
    Function
    
    
      'func1' has a complexity 
    
    
      of 
    
    
      1.Maximum allowed 
    
    
      is 
    
    
      0.
    
    
      
    
    
      
Arrowfunction has a complexity 
    
    
      of 
    
    
      1.Maximum allowed 
    
    
      is 
    
    
      0.
    
    
      
    
    
      
Method
    
    
      'func3' has a complexity 
    
    
      of 
    
    
      1.Maximum allowed 
    
    
      is 
    
    
      0.
    
    
      
    
    
      
Asyncfunction
    
    
      'func4' has a complexity 
    
    
      of 
    
    
      1.Maximum allowed 
    
    
      is 
    
    
      0.
    
    
      
   
   
       
        
     
       
      
      
        const
      
      
        
REG_FUNC_TYPE = 
      
      
        /^(Method |Async function |Arrow function |Function )/g;
      
      
        
      
      
        
      
      
        
      
      
        
      
      
        function getFunctionType(message) {
      
      
        
      
      
        
      
      
        let hasFuncType = REG_FUNC_TYPE.test(message);
      
      
        
      
      
        
      
      
        return hasFuncType && 
      
      
        RegExp.$
      
      
        1;
      
      
        
      
      
        
}
      
      
        
     
     
         
        
   
     
    
    
      const
    
    
      
 MESSAGE_PREFIX = 
    
    
      'Maximum allowed is 1.';
    
    
      
    
    
      
    
    
      const MESSAGE_SUFFIX = 
    
    
      'has a complexity of ';
    
    
      
    
    
      
    
    
      
    
    
      
    
    
      function getMain(message) {
    
    
      
    
    
      
    
    
      return message.replace(MESSAGE_PREFIX, 
    
    
      '').replace(MESSAGE_SUFFIX, 
    
    
      '');
    
    
      
    
    
      
}
    
    
      
   
   
       
      
   
     
    
    
      function getFunctionName(message) {
    
    
      
    
    
      
    
    
      const main = getMain(message);
    
    
      
    
    
      
    
    
      let test = 
    
    
      /'([a-zA-Z0-9_$]+)'/g.test(main);
    
    
      
    
    
      
    
    
      return test ? 
    
    
      RegExp.$
    
    
      1 : 
    
    
      '*';
    
    
      
    
    
      
}
    
    
      
   
   
       
      
   
     
    
    
      function
getComplexity(message) {
    
    
      
    
    
      
    
    
      const main = getMain(message);
    
    
      
    
    
      
(
    
    
      /(\d+)\./g).test(main);
    
    
      
    
    
      
    
    
      return+
    
    
      RegExp.$
    
    
      1;
    
    
      
    
    
      
}
    
    
      
   
   
       
      
   
     
      
    
    filePath.replace(process.cwd(), 
    
    
      '').trim()
    
    
      
   
   
       
        
     
       
      
      
        npm i c-scan --save
      
      
        
      
      
        
      
      
        
      
      
        
const scan = 
      
      
        require(
      
      
        'c-scan');
      
      
        
      
      
        
scan({
      
      
        
      
      
        
extensions:
      
      
        '**/*.js',
      
      
        
      
      
        
rootPath:
      
      
        'src',
      
      
        
      
      
        
defalutIgnore:
      
      
        'true',
      
      
        
      
      
        
ignoreRules:[],
      
      
        
      
      
        
ignoreFileName:
      
      
        '.gitignore'
      
      
        
      
      
        
});
      
      
        
     
     
         
          
     
       
      
      
        const
      
      
        
DEFAULT_IGNORE_PATTERNS = [
      
      
        
      
      
        
      
      
        'node_modules/**',
      
      
        
      
      
        
      
      
        'build/**',
      
      
        
      
      
        
      
      
        'dist/**',
      
      
        
      
      
        
      
      
        'output/**',
      
      
        
      
      
        
      
      
        'common_build/**'
      
      
        
      
      
        
];
      
      
        
     
     
         
          
     
       
      
      
        /**
* 获取glob扫描的文件列表
* @param {*} rootPath 跟路径
* @param {*} extensions 扩展
* @param {*} defalutIgnore 是否开启默认忽略
*/
      
      
        
      
      
        
      
      
        function getGlobScan(rootPath, extensions, defalutIgnore) {
      
      
        
      
      
        
returnnewPromise(
      
      
        resolve => {
      
      
        
      
      
        
glob(
      
      
        `${rootPath}${extensions}`,
      
      
        
      
      
        
{ 
      
      
        dot: 
      
      
        true, 
      
      
        ignore: defalutIgnore ? DEFAULT_IGNORE_PATTERNS : [] },
      
      
        
      
      
        
(err, files) => {
      
      
        
      
      
        
      
      
        if(err) {
      
      
        
      
      
        
      
      
        console.log(err);
      
      
        
      
      
        
process.exit(
      
      
        1);
      
      
        
      
      
        
}
      
      
        
      
      
        
resolve(files);
      
      
        
      
      
        
});
      
      
        
      
      
        
});
      
      
        
      
      
        
}
      
      
        
      
      
        
      
      
        
      
      
        
      
      
        /**
* 加载ignore配置文件,并处理成数组
* @param {*} ignoreFileName
*/
      
      
        
      
      
        
      
      
        async 
      
      
        function loadIgnorePatterns(ignoreFileName) {
      
      
        
      
      
        
      
      
        const ignorePath = path.resolve(process.cwd(), ignoreFileName);
      
      
        
      
      
        
      
      
        try{
      
      
        
      
      
        
      
      
        const ignores = fs.readFileSync(ignorePath, 
      
      
        'utf8');
      
      
        
      
      
        
      
      
        return ignores.split(
      
      
        /[\n\r]|\n\r/).filter(
      
      
        pattern => 
      
      
        Boolean(pattern));
      
      
        
      
      
        
} 
      
      
        catch(e) {
      
      
        
      
      
        
      
      
        return[];
      
      
        
      
      
        
}
      
      
        
      
      
        
}
      
      
        
      
      
        
      
      
        
      
      
        
      
      
        /**
* 根据ignore配置过滤文件列表
* @param {*} files
* @param {*} ignorePatterns
* @param {*} cwd
*/
      
      
        
      
      
        
      
      
        function filterFilesByIgnore(files, ignorePatterns, ignoreRules, cwd = process.cwd()) {
      
      
        
      
      
        
      
      
        const ig = ignore().add([...ignorePatterns, ...ignoreRules]);
      
      
        
      
      
        
      
      
        const filtered = files
      
      
        
      
      
        
.map(
      
      
        raw => (path.isAbsolute(raw) ? raw : path.resolve(cwd, raw)))
      
      
        
      
      
        
.map(
      
      
        raw => path.relative(cwd, raw))
      
      
        
      
      
        
.filter(
      
      
        filePath => !ig.ignores(filePath))
      
      
        
      
      
        
.map(
      
      
        raw => path.resolve(cwd, raw));
      
      
        
      
      
        
      
      
        return filtered;
      
      
        
      
      
        
}
      
      
        
     
     
         
          
     
       
      
      
        npm i c-complexity --save
      
      
        
      
      
        
const cc = 
      
      
        require(
      
      
        'c-complexity');
      
      
        
      
      
        
cc({},
      
      
        10);
      
      
        
     
     
         
     部分截图来源于我们内部的项目质量监控平台,圈复杂度作为一项重要的指标,对于衡量项目代码质量起着至关重要的作用。 
实际开发中并不一定所有的代码都需要被分析,例如打包产物、静态资源文件等等,这些文件往往会误导我们的分析结果,现在分析工具会默认忽略一些规则,例如:.gitignore文件、static目录等等,实际这些规则还需要根据实际项目的情况去不断完善,使分析结果变得更准确。 
热 文 推 荐 
☞【建议珍藏系列】如果你这样回答「什么是线程安全」,面试官都会对你刮目相看!
☞985 高校计算机系学生都在用的笔记本,我被深深地种草了!