跳至主要內容
圈复杂度介绍

圈复杂度介绍

大约 9 分钟工程化前端质量工程化前端质量规范

圈复杂度介绍

圈复杂度(Cyclomatic complexity)是一种代码复杂度的衡量标准,在1976年由Thomas J. McCabe, Sr. 提出。在软件测试的概念里,圈复杂度用来衡量一个模块判定结构的复杂程度,数量上表现为线性无关的路径条数,即合理的预防错误所需测试的最少路径条数。圈复杂度大说明程序代码可能质量低且难于测试和维护,根据经验:

程序的可能错误和高的圈复杂度有着很大关系。

圈复杂度衡量标准

圈复杂度代码状况可测性维护成本
0 - 5良好
5 - 10良好中等中等
10 - 20较差
20 - 30很低很高

圈复杂度计算

计算公式

计算公式1

V(G)=e-n+2p。其中,e表示控制流图中边的数量,n表示控制流图中节点的数量,p图的连接组件数目(图的组件数是相连节点的最大集合)。因为控制流图都是连通的,所以p为1.

open in new window

计算公式2

V(G)=区域数=判定节点数+1。其实,圈复杂度的计算还有更直观的方法,因为圈复杂度所反映的是“判定条件”的数量,所以圈复杂度实际上就是等于判定节点的数量再加上1,也即控制流图的区域数。

对于多分支的CASE结构或IF-ELSEIF-ELSE结构,统计判定节点的个数时需要特别注意一点,要求必须统计全部实际的判定节点数,也即每个ELSEIF语句,以及每个CASE语句,都应该算为一个判定节点。

open in new window

计算公式3

计算公式3:V(G)=R。其中R代表平面被控制流图划分成的区域数。

open in new window圈复杂度

针对程序的控制流图计算open in new window圈复杂度V(G)时,最好还是采用第一个公式,也即V(G)=e-n+2;而针对模块的控制流图时,可以直接统计判定节点数,这样更为简单;针对复杂的控制流图是,使用区域计算公式V(G)=R更为简单。

推荐使用第一种计算方法。

圈复杂度示例

典型的控制流程,如if-else,While,until和正常的流程顺序:

圈复杂度的计算还有更直观的方法,因为圈复杂度所反映的是“判定条件”的数量,所以圈复杂度实际上就是等于判定节点的数量再加上1,也即控制流图的区域数,对应的计算公式为:

V (G) = P + 1

  1. if语句
  2. while语句
  3. for语句
  4. case语句
  5. catch语句
  6. and和or布尔操作
  7. ?:三元运算符

示例:

function sort(A: number[]): void {
  let i = 0
  const n = 4
  let j = 0
  while (i < n - 1) {
      j = i + 1
      while (j < n) {
          if (A[i] < A[j]) {
              const temp = A[i]
              A[i] = A[j]
              A[j] = temp
          }
      }
      i = i + 1
  }
}

使用点边计算法绘出控制流图:

其圈复杂度为:V(G) = 9 - 7 + 2 = 4

圈复杂度的检测工具

项目sonarqubeeslintcodemetrics
圈复杂度度量标准支持支持支持
检测效率
精度
支持的编程语言一般一般
对圈复杂度高的代码的指导性

sonarqube

  1. 安装SonarQube服务器

SonarQube服务器可以通过下载和安装来获取,也可以在云上进行部署。你可以从SonarQube的官方网站上下载并安装相应的版本。安装完成后,需要启动SonarQube服务器。

  1. 配置SonarQube服务器

安装完成后,需要在SonarQube服务器中进行一些配置,例如配置数据库和LDAP等。你可以参考SonarQube的官方文档来进行相应的配置。

  1. 安装SonarQube扫描器

SonarQube扫描器可以安装在本地开发机器或者CI/CD服务器上。你需要下载并安装相应的扫描器,然后配置扫描器与SonarQube服务器的连接。

  1. 配置项目

在SonarQube服务器中,你需要为每个项目进行相应的配置。你可以在SonarQube界面中手动创建项目,也可以使用SonarQube API进行自动化配置。在配置项目时,需要设置项目名称、语言类型、代码仓库地址等信息。

  1. 运行SonarQube扫描器

在配置完成后,你需要使用SonarQube扫描器对代码进行扫描。在扫描时,你需要指定要扫描的代码路径、扫描器的参数等。扫描器将会把扫描结果上传到SonarQube服务器中。


eslint

使用ESLint检测圈复杂度的步骤:

  1. 安装ESLint

在命令行中执行以下命令安装ESLint:

npm install eslint --save-dev
  1. 安装eslint-plugin-complexity插件

在命令行中执行以下命令安装eslint-plugin-complexity插件:

npm install eslint-plugin-complexity --save-dev
  1. 配置ESLint

在项目根目录下创建.eslintrc.js文件,配置ESLint和eslint-plugin-complexity插件。示例如下:

module.exports = {
  env: {
    browser: true,
    es6: true,
  },
  extends: [
    'eslint:recommended',
  ],
  plugins: [
    'complexity',
  ],
  rules: {
    'complexity': ['error', { 'max': 10 }],
  },
};

其中,"max"表示允许的最大圈复杂度,上述配置将检测每个函数的圈复杂度是否大于10。

  1. 运行ESLint

在命令行中执行以下命令来运行ESLint:

npx eslint yourfile.js

其中,"yourfile.js"为待检测的JavaScript文件。

运行结果将会显示每个函数的圈复杂度是否超过了配置的最大值,如果超过了,ESLint将会给出相应的警告和建议。

codeMetrics

插件商店直接搜索codeMetrics,直接安装既可。

检测结果。

如何保障代码质量

  1. 单一职责原则

单一职责原则是指每个类或方法应该只有一个责任。如果一个方法或类的职责过于复杂,那么它就很容易产生高圈复杂度。通过将复杂的方法或类拆分为多个小方法或类,每个方法或类只关注一个特定的任务,可以有效地降低圈复杂度。

before:

class User {
  login(username: string, password: string): boolean {
    // 验证用户身份
    // ...
    return true;
  }

  getProfile(userId: number): object {
    // 获取用户信息
    // ...
    return {};
  }
}

after:

class Authenticator {
  login(username: string, password: string): boolean {
    // 验证用户身份
    // ...
    return true;
  }
}

class UserProfile {
  getProfile(userId: number): object {
    // 获取用户信息
    // ...
    return {};
  }
}
  1. 开闭原则

开闭原则是指软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。通过采用开闭原则,可以使我们的代码更加容易扩展,从而避免出现复杂的控制流程和高圈复杂度。

before

function quickSort(data: number[]) {
  // 快速排序算法的实现
}

function mergeSort(data: number[]) {
  // 归并排序算法的实现
}

after:

interface SortStrategy {
  sort(data: number[]): number[];
}

class QuickSortStrategy implements SortStrategy {
  sort(data: number[]) {
    // 快速排序算法的实现
  }
}

class MergeSortStrategy implements SortStrategy {
  sort(data: number[]) {
    // 归并排序算法的实现
  }
}

class Sorter {
  private strategy: SortStrategy;

  constructor(strategy: SortStrategy) {
    this.strategy = strategy;
  }

  sort(data: number[]) {
    return this.strategy.sort(data);
  }
}
  1. 去除重复代码

重复的代码是代码复杂度的一种来源。通过去除重复的代码,可以将代码块中的控制流程减少到最小,从而降低圈复杂度。

  1. 提炼函数

将复杂的代码块提炼到一个单独的函数中,可以将控制流程减少到最小,从而降低圈复杂度。

before:

// 原始代码
function calculateTotalPrice(products) {
  let totalPrice = 0;
  for (let i = 0; i < products.length; i++) {
    const product = products[i];
    totalPrice += product.price * product.quantity;
    if (product.isOnSale) {
      totalPrice -= product.discount;
    }
  }
  return totalPrice;
}

after:

// 重构后的代码
function calculateTotalPrice(products) {
  let totalPrice = 0;
  for (let i = 0; i < products.length; i++) {
    const product = products[i];
    totalPrice += calculateProductPrice(product);
  }
  return totalPrice;
}

function calculateProductPrice(product) {
  let productPrice = product.price * product.quantity;
  if (product.isOnSale) {
    productPrice -= product.discount;
  }
  return productPrice;
}
  1. 引入多态

引入多态可以避免复杂的条件语句,从而使代码更加简洁和易于理解。多态是一种在运行时根据对象类型选择方法的机制,它可以避免使用复杂的条件语句,从而减少代码块中的控制流程,降低圈复杂度。

// 抽象的动物类
abstract class Animal {
  abstract makeSound(): void;
}

// 具体的狗类
class Dog extends Animal {
  makeSound(): void {
    console.log("汪汪汪!");
  }
}

// 具体的猫类
class Cat extends Animal {
  makeSound(): void {
    console.log("喵喵喵!");
  }
}

// Animal 类型的数组
const animals: Animal[] = [new Dog(), new Cat()];

// 遍历数组,调用不同的 makeSound 方法
animals.forEach(animal => animal.makeSound());
  1. 提前返回

通过提前返回可以避免过多的条件语句和嵌套,从而减少圈复杂度。使用break和return提前返回。

function calculateBonus(salary: number, level: string) {
  if (salary <= 0) {
    return 0;
  }
  
  let bonus = 0;
  switch (level) {
    case 'A':
      bonus = salary * 0.2;
      break;
    case 'B':
      bonus = salary * 0.1;
      break;
    case 'C':
      bonus = salary * 0.05;
      break;
    default:
      break;
  }
  
  return bonus;
}
  1. 使用多个小函数

使用多个小函数可以使代码更加模块化,从而降低圈复杂度。每个小函数只需要关注一个具体的任务,从而避免出现复杂的控制流程。

before:

// 大函数
function processItems(items: any[]) {
  const results = [];
  for (let i = 0; i < items.length; i++) {
    const item = items[i];
    // 执行一系列操作...
    if (item.isValid) {
      results.push(item.value * 2);
    }
    // 执行一系列操作...
    if (item.isValid && item.value > 10) {
      results.push(item.value * 3);
    }
    // 执行一系列操作...
  }
  return results;
}

after:

// 使用多个小函数
function processItems(items: any[]) {
  const results = [];
  for (let i = 0; i < items.length; i++) {
    const item = items[i];
    const result = processItem(item);
    if (result !== null) {
      results.push(result);
    }
  }
  return results;
}

function processItem(item: any): any {
  const result1 = processItemPart1(item);
  const result2 = processItemPart2(item);
  if (result1 !== null && result2 !== null) {
    return result1 * 2 + result2 * 3;
  }
  return null;
}

function processItemPart1(item: any): any {
  // 执行一系列操作...
  if (item.isValid) {
    return item.value;
  }
  return null;
}

function processItemPart2(item: any): any {
  // 执行一系列操作...
  if (item.isValid && item.value > 10) {
    return item.value;
  }
  return null;
}
  1. 使用函数式编程

函数式编程是一种通过函数组合来构建复杂程序的编程范式,它可以避免出现复杂的控制流程和高圈复杂度。函数式编程中的函数通常都是纯函数,即给定相同的输入,始终返回相同的输出,因此不会受到外部环境的影响。

function sum(array) {
  let result = 0;
  for (let i = 0; i < array.length; i++) {
    result += array[i];
  }
  return result;
}

after:

function sum(array) {
  return array.reduce((acc, val) => acc + val, 0);
}
上次编辑于:
贡献者: uniqueli