在写程序的时候,永远记住,以后维护你写的程序的人是一个有严重暴力倾向并且知道你住在哪里的心理变态者。1.前言你有过以下的想法吗?重构一个项目还不如开发一个新项目。。。这代码是谁写的,真纳闷。。。你的项目是否也存在以下问题?单个项目越来越大,团队成员的代码风格不一致,无法完全控制整体代码质量。没有准确的标准来衡量代码结构的复杂程度,一个项目的代码质量在重构代码后并不能立即量化。量化重构后代码质量是否提升针对以上问题,本文的主角是圈复杂度。本文将从圈复杂度的原理出发,介绍圈复杂度的计算方法,如何降低代码的圈复杂度,如何获取圈复杂度,以及圈复杂度在企业项目中的实际应用。2.圈复杂度2.1定义圈复杂度(Cyclomaticcomplexity)是代码复杂度的度量,也称为条件复杂度或循环复杂度,可以用来衡量一个模块决策结构的复杂度,用数量表示为个数ofindependentcurrentpaths,也可以理解为用于覆盖所有可能情况的最少数量的测试用例。缩写为CC。其符号为VG或M。圈复杂度由ThomasJ.McCabe,Sr.于1976年提出。圈复杂度大表示程序代码的判断逻辑复杂,可能质量低下,难以测试和维护.程序可能出现的错误与高圈复杂度有很大关系。2.2衡量标准代码复杂度低的代码不一定好,代码复杂度高的代码一定不好。CyclomaticComplexityCodeStatusTestabilityMaintenanceCost1-10ClearandStructuredHighLow10-20ComplexMediumMedium20-30VeryComplexLowHigh>30UnreadableUntestableVeryHigh3.计算方法3.1控制流程图控制流程图,是一个过程的抽象表示或程序。它是编译器中使用的一种抽象数据结构,由编译器内部维护,代表程序在执行过程中会经过的所有路径。它以图形的形式展示了一个流程中所有基本块可能的流向,也可以反映一个流程的实时执行过程。以下是一些常用的控制过程:3.2节点判定法有一种简单的计算方法,圈复杂度实际上等于判定节点数加1。如前所述:ifelse、switchcase、for循环、三元运算符等,都属于一个决策节点,比如下面的代码:functiontestComplexity(*param*){letresult=1;if(param>0){result--;}for(leti=0;i<10;i++){result+=Math.random();}switch(parseInt(result)){case1:result+=20;break;case2:result+=30;break;default:result+=10;break;}returnresult>20?result:result;}上面代码中,有1条if语句,一个for循环,两个case语句,一个三元运算符,所以代码复杂度是4+1+1=6。另外需要注意的是||和&&语句也算作一个决策节点。例如下面代码的代码复杂度为3:functiontestComplexity(*param*){letresult=1;if(param>0&¶m<10){result--;}returnresult;}3.3点边计算方法M=E?N+2PE:控制流图中边的个数N:控制流图中节点的个数P:独立分量的个数前两个分别是边和节点都是数据结构图中最基本的概念:P代表图中独立分量的个数。独立分量是什么意思?看下面两张图,左边是连通图,右边是断开图:连通图:图中任意两个顶点相连的连通图是图中的一个独立分量,所以左图独立分量个数为1,右边有两个独立分量。对于我们代码转换出来的控制流图,一般情况下应该是所有节点都连接起来的,除非你在某些节点之前执行了return,显然这样的代码是错误的。所以每个程序流程图的独立分量个数为1,所以上式也可以简化为M=E?N+2。4、降低代码的圈复杂度我们可以通过一些代码重构的方法来降低代码的圈复杂度。重构需要谨慎。示例代码仅代表一种思路,实际代码远比示例代码复杂。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.圈复杂度检测方法5.1eslint规则eslint提供了检测代码圈复杂度的规则:我们会在规则中启用复杂度规则,并设置圈复杂度大于0的代码的规则严重程度为warn或error。rules:{complexity:['warn',{max:0}]}这样eslint会自动检测所有函数的代码复杂度,并输出类似下面的信息。Method'testFunc'hasacomplexityof12.Maximumallowedis0Asyncfunctionhasacomplexityof6.Maximumallowedis0....5.2CLIEngine我们可以使用eslint的CLIEngine在本地使用自定义的eslint规则扫描代码,得到扫描结果输出。初始化CLIEngine:consteslint=require('eslint');const{CLIEngine}=eslint;constcli=newCLIEngine({parserOptions:{ecmaVersion:2018,},rules:{complexity:['error',{max:0}]}});使用executeOnFiles扫描指定文件,得到结果,过滤掉所有复杂的消息信息。constreports=cli.executeOnFiles(['.']).results;for(leti=0;i{console.log(2);};classTestClass{func3(){console.log(3);}}asyncfunctionfunc4(){console.log(1);}执行结果:Function'func1'hasacomplexityof1.Maximumallowedis0.Arrowfunctionhasacomplexityof1.Maximumallowedis0.Method'func3'hasacomplexityof1.Maximumallowedis0.Asyncfunction4function''具有1.Maximumallowedis0的复杂性。可以发现,除了前面的函数类型和后面的复杂度外,其他都一样。函数类型:函数:普通函数箭头函数:箭头函数方法:类方法异步函数:异步函数拦截方法类型:constREG_FUNC_TYPE=/^(Method|Asyncfunction|Arrowfunction|Function)/g;functiongetFunctionType(message){lethasFuncType=REG_FUNC_TYPE.test(message);returnhasFuncType&&RegExp.$1;}提取有用的部分:constMESSAGE_PREFIX='Maximumallowedis1.';constMESSAGE_SUFFIX='hasacomplexityof';functiongetMain(message){returnmessage.replace(MESSAGE_PREFIX,'').replace(MESSAGE_SUFFIX,'').replace(MESSAGE_SUFFIX');}提取方法名:functiongetFunctionName(message){constmain=getMain(message);lettest=/'([a-zA-Z0-9_$]+)'/g.test(main);返回测试?正则表达式。$1:'*';}拦截代码复杂度:functiongetComplexity(message){constmain=getMain(message);(/(\d+)\./g).test(main);return+RegExp.$1;}exceptmessage,等有用信息:函数位置:获取消息中的行和列,即函数的行和列位置当前文件名:当前扫描文件的绝对路径filePath可以从报告结果中获取,并且真实文件名可以通过以下操作获取:filePath.replace(process.cwd(),'').trim()复杂度,根据函数的复杂度给出重构建议:Cyclomaticcomplexitycodestatus可测试性维护成本1-10清晰、结构化高低10-20复杂中等中等20-30非常复杂低高>30不可读不可测试非常高循环复杂度代码状态1-10无需重构11-15建议重构配置>15强烈推荐重构6.架构设计将代码复杂度检测封装成一个基础包,根据自定义配置输出检测数据,供其他应用调用以上展示了使用eslint获取代码复杂度的思路。它被打包为一个通用工具。考虑到该工具可能在不同的场景下使用,比如:web版的分析报告,cli版的命令行工具,我们将通用的能力抽象出来,以npm包的形式用于其他应用。在计算项目代码的复杂度之前,我们首先要有一个基本的能力,代码扫描,也就是我们要知道我们要分析的项目中有哪些文件。首先eslint有这个能力,我们也可以直接使用glob来遍历文件。但是它们都有一个缺点,就是忽略规则不同,对于用户来说有一定的学习成本,所以这里我会手动封装代码扫描,使用通用的npm忽略规则,这样代码扫描就可以使用了直接地。像gitignore这样的配置文件。另外,扫码是代码分析的基础能力,其他代码分析也可以共享。基本能力代码扫描能力复杂度检测能力...应用命令行工具代码分析报告...七、基本能力-代码扫描本文涉及的npm包和cli命令的源码可以在我的开源项目中查看真棒客户端。awesome-cli是我新建的一个开源项目:一个有趣实用的命令行工具,后期会持续维护,敬请期待,欢迎star。代码扫描(c-scan)源码:https://github.com/ConardLi/a...代码扫描是代码分析的底层能力。主要是帮助我们得到我们想要的文件路径。应该满足以下两个需求:A需求:我要获取什么类型的文件?我不想要哪些文件?7.1使用npmic-scan--saveconstscan=require('c-scan');scan({extensions:'**/*.js',rootPath:'src',defaultIgnore:'true',ignoreRules:[],ignoreFileName:'.gitignore'});7.2返回值为满足规则的文件路径数组:7.3参数extensions扫描文件扩展名默认值:**/*.jsrootPath扫描文件路径默认值:.defalutIgnore是否启用默认忽略(glob规则)glob忽略规则供内部使用。为了统一忽略规则,自定义规则使用gitignore规则默认值:true默认启用glob忽略规则:constDEFAULT_IGNORE_PATTERNS=['node_modules/**','build/**','dist/**','输出/**','common_build/**'];ignoreRules自定义忽略规则(gitignorerules)默认值:[]ignoreFileNamefromDefine忽略规则配置文件路径(gitignorerule)默认值:如果.gitignore设置为null,将不会启用忽略配置文件。7.4核心实现基于glob,自定义忽略规则进行二次包装。/***获取glob扫描的文件列表*@param{*}rootPathandpath*@param{*}extensionsextension*@param{*}默认启用defalutIgnore*/functiongetGlobScan(rootPath,extensions,defalutIgnore){returnnewPromise(resolve=>{glob(`${rootPath}${extensions}`,{dot:true,ignore:defaultIgnore?DEFAULT_IGNORE_PATTERNS:[]},(err,files)=>{if(err){console.log(err);process.exit(1);}resolve(files);});});}/***加载ignore配置文件,处理成数组*@param{*}ignoreFileName*/asyncfunctionloadIgnorePatterns(ignoreFileName){constignorePath=path.resolve(process.cwd(),ignoreFileName);try{constignores=fs.readFileSync(ignorePath,'utf8');returnignores.split(/[\n\r]|\n\r/).filter(pattern=>Boolean(pattern));}catch(e){return[];}}/***根据ignore配置过滤文件列表*@param{*}files*@param{*}ignorePatterns*@param{*}cwd*/functionfilterFilesByIgnore(files,ignorePatterns,ignoreRules,cwd=process.cwd()){constig=ignore().add([...ignorePatterns,...ignoreRules]);constfiltered=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));returnfiltered;}8.基础能力-代码复杂度检测代码复杂度检测(c-complexity)源码:https://github.com/ConardLi/a...代码检测基础包应该具备以下能力:自定义扫描文件夹和类型支持忽略文件定义最小提醒代码Complexity8.1使用npmic-complexity--saveconstcc=require('c-complexity');cc({},10);8.2返回值fileCount:文件数funcCount:函数数result:详细结果funcType:函数类型funcName;functionNameposition:详细位置(行列号)fileName:相对文件路径complexity:代码复杂度advice:重构建议8.3参数scanParam继承自上述代码扫描参数min最小提醒代码复杂度,默认为19.Application-code复杂度检测工具代码复杂度检测(conardcc)源码:https://github.com/ConardLi/a...9.1指定最小提醒复杂度通过命令conardcc默认可以触发提醒的最小复杂度为10--min=5Custom9.2指定扫描参数自定义扫描规则扫描参数继承自上述扫描参数例如:conardcc--defalutIgnore=false10。Application-Codecomplexityreport部分截图来自我们内部的项目质量监控平台,圈子复杂度作为一个重要的指标,对于衡量项目代码的质量起着至关重要的作用。代码复杂度、复杂度变化趋势定时任务爬取每天的代码复杂度、代码行数、代码的函数数,通过每天的数据绘制代码复杂度、代码行数变化趋势折线图。通过【复杂度/代码行数】或【复杂度/函数数】的变化趋势来判断项目的开发是否健康。如果这个比例一直在上升,说明你的代码越来越难理解了。这不仅使我们面临意外功能交互和缺陷的风险,而且由于我们在具有或多或少相关功能的模块中面临过多的认知负荷,因此难以重用代码并对其进行修改和测试。(下图1)如果比例在某个阶段突然发生变化,说明这个时期的迭代质量很差。(下图2)复杂度曲线可以快速帮助你更早发现以上两个问题。发现它们之后,您可能需要重构代码。复杂性趋势对于跟踪代码重构也很有用。复杂性趋势的下降趋势是一个好兆头。这要么意味着您的代码变得更简单(例如,if-else被重构为多态解决方案),要么意味着代码更少(不相关的部分被提取到其他模块中)。(下图3)代码重构后,需要继续探索复杂度变化趋势。经常发生的情况是,我们花费大量时间和精力进行重构,但未能解决根本原因,很快复杂性就会下降。(下图4)您可能认为这是一个例外,但研究表明,在分析了数百个代码库后,这种情况发生的频率很高。因此,需要时刻观察代码复杂度变化的趋势。代码复杂度文件分布计算每个复杂度分布的函数数量。代码复杂度文件详情计算每个函数的代码复杂度,按照从高到低的顺序列出高复杂度文件的分布,并给出重构建议。在实际开发中,并不是所有的代码都需要分析,比如打包产品、静态资源文件等,这些文件往往会误导我们的分析结果。现在分析工具默认会忽略一些规则,比如:.gitignore文件,静态目录等,其实这些规则还是需要根据项目的实际情况不断完善,让分析结果更加准确。