浅谈前端中的圈复杂度

DevUI是一支兼具设计视角和工程视角的团队,服务于华为云DevCloud平台和华为内部数个中后台系统,服务于设计师和前端工程师。
官方网站:devui.design
Ng组件库:ng-devui(欢迎Star)前端

引言

重构,是咱们开发过程当中不可避免须要进行的一项工做。重构代码,以适配当前模块设计之初未考虑到的多样化场景,并增长模块的可维护性、健壮性、可测试性。那么,如何明确重构的方向,以及量化重构的结果呢?git

代码圈复杂度(Cyclomatic complexity,CC)能够是一个供选择的指标。github

什么是圈复杂度

圈复杂度(Cyclomatic complexity,CC)也称为条件复杂度或循环复杂度,是一种软件度量,是由老托马斯·J·麦凯布(Thomas J. McCabe, Sr.)在1976年提出,用来表示程序的复杂度,其符号为VG或是M。圈复杂度即程序的源代码中线性独立路径的个数。算法

为什么要下降模块(函数)的圈复杂度

下表为模块(函数)圈复杂度与代码情况的一个基本对照表。除了表中给出的代码情况、可测性、维护成本等指标外,圈复杂度高的模块(函数),也对应着高软件复杂度、低内聚、高风险、低可读性。咱们要下降模块(函数)的圈复杂度,就是要下降其软件复杂度、增长内聚性、减小可能出现的软件缺陷个数、加强可测试性、可读性。markdown

圈复杂度前端工程师

代码情况框架

可测性函数

维护成本工具

1 - 10oop

清晰、结构化

10 - 20

复杂

20 - 30

很是复杂

>30

不可读

不可测

很是高

下降软件复杂度

麦凯布提出圈复杂度时,其原始目的之一就是但愿在软件开发过程当中就限制其复杂度。他建议程序设计者需计算其开发模块的复杂度,若一模块的圈复杂度超过10,需再分割为更小的模块。NIST(国家标准技术研究所)的结构化测试方法论已此做法略做调整,在一些特定情形下,模块圈复杂度上限放宽到15会比较合适。此方法论也认可有些特殊情形下,模块的复杂度须要超过上述的上限,其建议为“模块的循环复杂度需在上限范围之内,不然需提供书面数据,说明为什么此模块循环复杂度有必要超过上限。”

增长模块内聚性

优秀的代码模块间老是低耦合,高内聚的。能够预期一个复杂度较高模块的内聚性会比较低,至少不会到功能内聚性的程度。一个有高复杂度及低内聚性的模块中会有许多的决策点,这类的模块多半运行超过一个明肯定义的任务,所以内聚性较低。

减小可能出现的软件缺陷个数

许多研究指出模块(函数)的圈复杂度和其中的缺陷个数有相关性,许多这类研究发现圈复杂度和缺陷个数有高度的正相关:圈复杂度最高的模块及方法,其中的缺陷个数也最多。

加强模块可测试性

一个圈复杂度高的模块(函数),由下文中将描述到的计算方法来看,必然会有更多的运行分支,要对这样的模块进行如单元测试用例的编写,将会十分复杂,而且后期用例维护也是一个问题。

加强代码可读性

代码可读性是大型项目与团队协做间必需要考虑的一个因素。圈复杂度高的模块(函数),随着逻辑复杂度的增长,代码可读性也将下降,不利于成员间相互协做与后期维护。

圈复杂度如何计算

计算方法

一段程序的圈复杂度是其线性独立路径的数量。若程序中没有像IF指令或FOR循环的控制流程,由于程序中只有一个路径,其圈复杂度为1,若程序中有一个IF指令,会有二个不一样路径,分别对应IF条件成立及不成立的情形,所以圈复杂度为2。

数学上,一个结构化程序的圈复杂度是利用程序的控制流图来定义,控制流图是一个有向图,图中的节点为程序的基础模块,若一个模块结束后,可能会运行另外一个模块,则用箭头连接二个模块,并标示可能的运行顺序。圈复杂度M能够用下式定义:

M = E − N + 2P

其中

E 为图中边的个数

N 为图中节点的个数

P 为链接组件的个数

度量工具

CodeMetrics

一款VSCode插件,用于度量TS、JS代码圈复杂度。

ESLint

eslint也能够配置关于圈复杂度的规则,如:

rules: { 
  complexity: [ 
    'error', 
    { 
      max: 15 
    } 
  ] 
}
复制代码

表明了当前每一个函数最高圈复杂度为15,不然eslint将给出错误提示

conard cc

一款开源的代码圈复杂度检测工具(github:github.com/ConardLi/aw…),能够生成当前项目下代码圈复杂度报告。

如何下降模块(函数)圈复杂度

经常使用结构圈复杂度

要下降圈复杂度,咱们就须要了解是哪些语句哪些结构致使了咱们复杂度的增长,如下为常见结构圈复杂度说明。

顺序结构

顺序结构复杂度为1。

例:

function func() {
  let a = 1, b = 1, c;
  c = a * b;
}
复制代码

如上代码,func函数内部为顺序结构,其控制流图以下:

边:1,点:2,连通分支:1,

圈复杂度:

M = 1 - 2 + 2 * 1 = 1
复制代码

if-else-else、switch-case

每增长一个分支,复杂度增长1,&& 、|| 运算也为一个分支。

例:

function func() {
  let a = 1, b = 1, c;
  if (a = 1) {
    c = a + b;
  } else {
    c = a - b;
  }
}
复制代码

边:4,点:4,连通分支:1,

圈复杂度:

M = 4 - 4 + 2 * 1 = 2
复制代码

循环结构

增长一个循环结构,复杂度增长1。、

例:

function func() {
  let a = 1, b = 1, c = 2;
  for (let i = 1; i < 10; i++) {
    b += a;
  }
  c = a + b;
}
复制代码

边:4,点:4,连通分支:1,

圈复杂度:

M = 4 - 4 + 2 * 1 = 2
复制代码

return

从理论上来说,return并不会增长当前模块圈复杂度,但在某些度量工具看来,一条return语句将增长总体程序的一条路径,而且若是提早返回,将增长程序的不肯定性,因此在大多数计算工具中,每增长一条return语句,复杂度将加1。

经常使用下降模块(函数)圈复杂度方法

1. 函数提炼与拆分,单一职责(推荐)

既然是下降一个模块(函数)圈复杂度,那么对于复杂度极高的函数,首先须要进行就是功能的提炼与函数拆分,每一个函数职责要单一。

例:

function add(a, b) {
  let tempA;
  if (a === 10) {
    tempA = 11;
  } else if (a === 12) {
    tempA = 12;
  }
  let tempB;
  if (b === 10) {
    tempB = 13;
  } else if (b === 12) {
    tempB = 12;
  }
  return tempB + tempA;
}
复制代码

重构为:

function add(a, b) {
  return calcA(a) + calcB(b);
}

function calcA(a) {
  if (a === 10) {
    return 11;
  } else if (a === 12) {
    return 12;
  }
}

function calcB(b) {
  if (b === 10) {
    return 13;
  } else if (b === 12) {
    return 12;
  }
}
复制代码

不只下降了add函数圈复杂度,而且代码结构更加清晰,增长了可读性,同时还增长了当前代码可维护性、可测试性。

固然,过犹不及,咱们的目标为提炼函数,保持函数单一职责,不能为了下降圈复杂度而进行暴力拆分。

2. 优化算法(减小没必要要条件、循环分支)

从圈复杂度计算上来看,条件、循环分支均会增长模块圈复杂度。从某些程度上,复杂的条件与循环结构是可优化,减小没必要要结构,从而下降圈复杂度。

例:

let a = 'a', c;
if (a === 'a') {
  c = a + 1;
} else if (a === 'b') {
  c = a + 2;
} else if (a === 'c') {
  c = a + 3;
} else if (a === 'd') {
  c = a + 4;
}
return c;
复制代码

重构为:

let a = 'a', c;
let conditionMap = {
  a: 1,
  b: 2,
  c: 3,
  d: 4
}
c = a + conditionMap[a];
return c;
复制代码

消除了全部条件分支,从而大幅下降了当前函数圈复杂度。

3. 表达式逻辑优化

逻辑计算也将增长圈复杂度,优化一些结构复杂的逻辑表达式,减小没必要要的逻辑判断,也将必定程度上下降圈复杂度。

例:

a && b || a && c
复制代码

可进行简单优化为:

a && (b || c)
复制代码

从而使表达式圈复杂度下降1。

5. 减小提早return(此方法需辩证看待)

单从下降圈复杂度上来看,因为当前大多数圈复杂度计算工具将对return个数进行计算,故若要针对这些工具衡量规则进行优化,减小return语句个数也为一种方式。

例:

let a = 1, b = 1;
if (a = 1) {
  return a + b;
} else {
  return a - b;
}
复制代码

重构为:

let a = 1, b = 1, c;
if (a = 1) {
  c = a + b;
} else {
  c = a - b;
}
return c;
复制代码

圈复杂度将下降1。

总结

圈复杂度(Cyclomatic complexity,CC)高的代码必定不是好代码,对于咱们代码好坏的衡量,圈复杂度能够做为一个参考指标;能够经过控制流图计算圈复杂度;要下降模块(函数)圈复杂度,提炼拆分函数、优化算法、优化逻辑表达式均为能够尝试的方法。

参考

循环复杂度:zh.wikipedia.org/wiki/%E5%BE…

详解圈复杂度:kaelzhang81.github.io/2017/06/18/…

前端代码质量-圈复杂度原理和实践:juejin.cn/post/684490…

加入咱们

咱们是DevUI团队,欢迎来这里和咱们一块儿打造优雅高效的人机设计/研发体系。招聘邮箱:muyang2@huawei.com

文/DevUI 砰砰砰砰

往期文章推荐

手把手教你搭建本身的Angular组件库

《手把手教你使用Vue/React/Angular三大框架开发Pagination分页组件》

《手把手教你搭建一个灰度发布环境》

相关文章
相关标签/搜索