百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

如何降低前端代码圈复杂度?

bigegpt 2024-09-17 12:41 4 浏览

作者 | 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循环、三元运算符等等,都属于一个判定节点,例如下面的代码

  1. 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 。

  1. 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 :

  1. const

    eslint = require('eslint');

    const{ CLIEngine} = eslint;

    const cli = newCLIEngine({

    parserOptions: {

    ecmaVersion: 2018,

    },

    rules: {

    complexity: [

    'error',

    { max: 0}

    ]

    }

    });

使用 executeOnFiles 对指定文件进行扫描,并获取结果,过滤出所有 complexity 的 message 信息。

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);

}

}

}

5.3 提取message

通过 eslint 的检测结果将有用的信息提取出来,先测试几个不同类型的函数,看看 eslint 的检测结果:

  1. 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.

可以发现,除了前面的函数类型,以及后面的复杂度,其他都是相同的。

函数类型:

Function :普通函数

Arrowfunction :箭头函数

Method :类方法

Asyncfunction :异步函数

截取方法类型:

  1. 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;

}

除了 message ,还有其他的有用信息:

函数位置:获取 messages 中的 line 、 column 即函数的行、列位置

当前文件名称:reports 结果中可以获取当前扫描文件的绝对路径 filePath ,通过下面的操作获取真实文件名:

filePath.replace(process.cwd, '').trim

复杂度等级,根据函数的复杂度等级给出重构建议:

架构设计

将代码复杂度检测封装成基础包,根据自定义配置输出检测数据,供其他应用调用。

上面的展示了使用 eslint 获取代码复杂度的思路,下面我们要把它封装为一个通用的工具,考虑到工具可能在不同场景下使用,例如:网页版的分析报告、cli版的命令行工具,我们把通用的能力抽象出来以 npm包 的形式供其他应用使用。

在计算项目代码复杂度之前,我们首先要具备一项基础能力,代码扫描,即我们要知道我们要对项目里的哪些文件做分析,首先 eslint 是具备这样的能力的,我们也可以直接用 glob 来遍历文件。但是他们都有一个缺点,就是 ignore 规则是不同的,这对于用户来讲是有一定学习成本的,因此我这里把手动封装代码扫描,使用通用的 npm ignore 规则,这样代码扫描就可以直接使用 .gitignore这样的配置文件。另外,代码扫描作为代码分析的基础能力,其他代码分析也是可以公用的。

  • 基础能力

  • 代码扫描能力

  • 复杂度检测能力

  • ...

  • 应用

  • 命令行工具

  • 代码分析报告

  • ...

基础能力 - 代码扫描

本文涉及的 npm 包和 cli命令源码均可在我的开源项目 awesome-cli中查看。

awesome-cli 是我新建的一个开源项目:有趣又实用的命令行工具,后面会持续维护,敬请关注,欢迎 star。

代码扫描( c-scan)源码:https://github.com/ConardLi/awesome-cli/tree/master/conard

代码扫描是代码分析的底层能力,它主要帮助我们拿到我们想要的文件路径,应该满足我们以下两个需求:

  • 我要得到什么类型的文件

  • 我不想要哪些文件

7.1 使用

  1. npm i c-scan --save

    const scan = require('c-scan');

    scan({

    extensions:'**/*.js',

    rootPath:'src',

    defalutIgnore:'true',

    ignoreRules:[],

    ignoreFileName:'.gitignore'

    });

7.2 返回值

符合规则的文件路径数组:

7.3 参数

extensions

扫描文件扩展名

默认值:**/*.js

rootPath

扫描文件路径

默认值:.

defalutIgnore

是否开启默认忽略( glob规则)

glob ignore规则为内部使用,为了统一 ignore规则,自定义规则使用 gitignore规则

默认值:true

默认开启的 glob ignore 规则:

  1. const

    DEFAULT_IGNORE_PATTERNS = [

    'node_modules/**',

    'build/**',

    'dist/**',

    'output/**',

    'common_build/**'

    ];

ignoreRules

自定义忽略规则( gitignore规则)

默认值:

ignoreFileName

自定义忽略规则配置文件路径( gitignore规则)

默认值:.gitignore

指定为 则不启用 ignore配置文件

7.4 核心实现

基于 glob ,自定义 ignore 规则进行二次封装。

  1. /**

    * 获取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;

    }

基础能力 - 代码复杂度检测

代码复杂度检测( c-complexity)源码:https://github.com/ConardLi/awesome-cli/tree/master/code-complexity

代码检测基础包应该具备以下几个能力:

自定义扫描文件夹和类型

支持忽略文件

定义最小提醒代码复杂度

8.1 使用

  1. npm i c-complexity --save

    const cc = require('c-complexity');

    cc({},10);

8.2 返回值

  • fileCount:文件数量

  • funcCount:函数数量

  • result:详细结果

  • funcType:函数类型

  • funcName;函数名称

  • position:详细位置(行列号)

  • fileName:文件相对路径

  • complexity:代码复杂度

  • advice:重构建议

8.3 参数

scanParam

继承自上面代码扫描的参数

min

最小提醒代码复杂度,默认为1

应用 - 代码复杂度检测工具

代码复杂度检测( c-complexity)源码:https://github.com/ConardLi/awesome-cli/blob/master/conard/lib/cc.js

9.1 指定最小提醒复杂度

可以触发提醒的最小复杂度。

默认为 10

通过命令 conard cc--min=5 自定义

9.2 指定扫描参数

自定义扫描规则

扫描参数继承自上面的 scan param

例如:conard cc--defalutIgnore=false

应用 - 代码复杂度报告

部分截图来源于我们内部的项目质量监控平台,圈复杂度作为一项重要的指标,对于衡量项目代码质量起着至关重要的作用。

代码复杂复杂度变化趋势

定时任务爬取代码每日的代码复杂度、代码行数、函数个数,通过每日数据绘制代码复杂度和代码行数变化趋势折线图。

通过 [ 复杂度 / 代码行数 ] 或 [ 复杂度 / 函数个数 ] 的变化趋势,判断项目发展是否健康。

比值若一直在上涨,说明你的代码在变得越来越难以理解。这不仅使我们面临意外的功能交互和缺陷的风险,由于我们在具有或多或少相关功能的模块中所面临的过多认知负担,也很难重用代码并进行修改和测试。(下图1)

若比值在某个阶段发生突变,说明这段期间迭代质量很差。(下图2)

复杂度曲线图可以很快的帮你更早的发现上面这两个问题,发现它们后,你可能需要重构代码。复杂性趋势对于跟踪你的代码重构也很有用。复杂性趋势的下降趋势是一个好兆头。这要么意味着您的代码变得更简单(例如,把 if-else 被重构为多态解决方案),要么代码更少(将不相关的部分提取到了其他模块中)。(下图3)

代码重构后,你还需要继续探索复杂度变化趋势。经常发生的事情是,我们花费大量的时间和精力来重构,无法解决根本原因,很快复杂度又滑回了原处。(下图4)你可能觉得这是个例,但是有研究标明,在分析了数百个代码库后,发现出现这种情况的频率很高。因此,时刻观察代码复杂度变化趋势是有必要的。

代码复杂度文件分布

统计各复杂度分布的函数数量。

代码复杂度文件详情

计算每个函数的代码复杂度,从高到低依次列出高复杂度的文件分布,并给出重构建议。

实际开发中并不一定所有的代码都需要被分析,例如打包产物、静态资源文件等等,这些文件往往会误导我们的分析结果,现在分析工具会默认忽略一些规则,例如:.gitignore文件、static目录等等,实际这些规则还需要根据实际项目的情况去不断完善,使分析结果变得更准确。

参考

加推研发质量与规范实战

codescene

圈复杂度那些事儿-前端代码质量系列文章(二)

代码质量管控 -- 复杂度检测

详解圈复杂度

文章开头小丑图片来源于网络,如有侵权请联系我删除,其余图片均为本人原创图片。

声明:本文系作者独立观点,不代表CSDN立场。

【End】

相关推荐

方差分析简介(方差分析通俗理解)

介绍方差分析(ANOVA,AnalysisofVariance)是一种广泛使用的统计方法,用于比较两个或多个组之间的均值。单因素方差分析是方差分析的一种变体,旨在检测三个或更多分类组的均值是否存在...

正如404页面所预示,猴子正成为断网元凶--吧嗒吧嗒真好吃

吧嗒吧嗒,绘图:MakiNaro你可以通过加热、冰冻、水淹、模塑、甚至压溃压力来使网络光缆硬化。但用猴子显然是不行的。光缆那新挤压成型的塑料外皮太尼玛诱人了,无法阻挡一场试吃盛宴的举行。印度政府正...

Python数据可视化:箱线图多种库画法

概念箱线图通过数据的四分位数来展示数据的分布情况。例如:数据的中心位置,数据间的离散程度,是否有异常值等。把数据从小到大进行排列并等分成四份,第一分位数(Q1),第二分位数(Q2)和第三分位数(Q3)...

多组独立(完全随机设计)样本秩和检验的SPSS操作教程及结果解读

作者/风仕在上一期,我们已经讲完了两组独立样本秩和检验的SPSS操作教程及结果解读,这期开始讲多组独立样本秩和检验,我们主要从多组独立样本秩和检验介绍、两组独立样本秩和检验使用条件及案例的SPSS操作...

方差分析 in R语言 and Excel(方差分析r语言例题)

今天来写一篇实际中比较实用的分析方法,方差分析。通过方差分析,我们可以确定组别之间的差异是否超出了由于随机因素引起的差异范围。方差分析分为单因素方差分析和多因素方差分析,这一篇先介绍一下单因素方差分析...

可视化:前端数据可视化插件大盘点 图表/图谱/地图/关系图

前端数据可视化插件大盘点图表/图谱/地图/关系图全有在大数据时代,很多时候我们需要在网页中显示数据统计报表,从而能很直观地了解数据的走向,开发人员很多时候需要使用图表来表现一些数据。随着Web技术的...

matplotlib 必知的 15 个图(matplotlib各种图)

施工专题,我已完成20篇,施工系列几乎覆盖Python完整技术栈,目标只总结实践中最实用的东西,直击问题本质,快速帮助读者们入门和进阶:1我的施工计划2数字专题3字符串专题4列表专题5流程控制专题6编...

R ggplot2常用图表绘制指南(ggplot2绘制折线图)

ggplot2是R语言中强大的数据可视化包,基于“图形语法”(GrammarofGraphics),通过分层方式构建图表。以下是常用图表命令的详细指南,涵盖基本语法、常见图表类型及示例,适合...

Python数据可视化:从Pandas基础到Seaborn高级应用

数据可视化是数据分析中不可或缺的一环,它能帮助我们直观理解数据模式和趋势。本文将全面介绍Python中最常用的三种可视化方法。Pandas内置绘图功能Pandas基于Matplotlib提供了简洁的绘...

Python 数据可视化常用命令备忘录

本文提供了一个全面的Python数据可视化备忘单,适用于探索性数据分析(EDA)。该备忘单涵盖了单变量分析、双变量分析、多变量分析、时间序列分析、文本数据分析、可视化定制以及保存与显示等内容。所...

统计图的种类(统计图的种类及特点图片)

统计图是利用几何图形或具体事物的形象和地图等形式来表现社会经济现象数量特征和数量关系的图形。以下是几种常见的统计图类型及其适用场景:1.条形图(BarChart)条形图是用矩形条的高度或长度来表示...

实测,大模型谁更懂数据可视化?(数据可视化和可视化分析的主要模型)

大家好,我是Ai学习的老章看论文时,经常看到漂亮的图表,很多不知道是用什么工具绘制的,或者很想复刻类似图表。实测,大模型LaTeX公式识别,出乎预料前文,我用Kimi、Qwen-3-235B...

通过AI提示词让Deepseek快速生成各种类型的图表制作

在数据分析和可视化领域,图表是传达信息的重要工具。然而,传统图表制作往往需要专业的软件和一定的技术知识。本文将介绍如何通过AI提示词,利用Deepseek快速生成各种类型的图表,包括柱状图、折线图、饼...

数据可视化:解析箱线图(box plot)

箱线图/盒须图(boxplot)是数据分布的图形表示,由五个摘要组成:最小值、第一四分位数(25th百分位数)、中位数、第三四分位数(75th百分位数)和最大值。箱子代表四分位距(IQR)。IQR是...

[seaborn] seaborn学习笔记1-箱形图Boxplot

1箱形图Boxplot(代码下载)Boxplot可能是最常见的图形类型之一。它能够很好表示数据中的分布规律。箱型图方框的末尾显示了上下四分位数。极线显示最高和最低值,不包括异常值。seaborn中...