cover_image

前端代码质量规范测量

李国靖 好未来技术
2023年07月13日 10:00

点击蓝字,关注我们

前端代码质量规范测量

1

圈复杂度介绍

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

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

2

圈复杂度衡量标准

圈复杂度

代码状况

可测性

维护成本

0 - 5

良好

5 - 10

良好

中等

中等

10 - 20

较差

20 - 30

很低

很高

3

圈复杂度计算

计算公式

计算公式1


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

图片

计算公式2

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

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

图片

计算公式3

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

图片

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

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

圈复杂度示例

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

图片

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

        V (G) = P + 1

  1. 1. if语句

  2. 2. while语句

  3. 3. for语句

  4. 4. case语句

  5. 5. catch语句

  6. 6. and和or布尔操作

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

4

圈复杂度的检测工具

项目

sonarqube

eslint

codemetrics

圈复杂度度量标准

支持

支持

支持

检测效率

精度

支持的编程语言

一般

一般

对圈复杂度高的代码的指导性

sonarqube

1.安装SonarQube服务器

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


2.配置SonarQube服务器

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


3.安装SonarQube扫描器

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


4.配置项目

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


5.运行SonarQube扫描器

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

eslint

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

1.安装ESLint

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

npm install eslint --save-dev

 

2.安装eslint-plugin-complexity插件


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

npm install eslint-plugin-complexity --save-dev


3.配置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。


4.运行ESLint


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

npx eslint yourfile.js

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

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

codeMetrics

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

图片

检测结果。

图片

5

如何保障代码质量

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 {};  }}


2.开闭原则

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

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


3.去除重复代码

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


4.提炼函数

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

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


5.引入多态

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

// 抽象的动物类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());


6.提前返回

通过提前返回可以避免过多的条件语句和嵌套,从而减少圈复杂度。使用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;}


7.使用多个小函数

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

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


8.使用函数式编程

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

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

当代码复杂度遇到 Copilot chat

当代码复杂度过高时,可以通过Copilot chat来进行优化。

图片
图片

- 也许你还想看 -

给非前端伙伴的前端知识学习引导

Python并发编程入门

基于双模检测的通话录音质检解决方案


我知道你“在看”哟!~

继续滑动看下一个
好未来技术
向上滑动看下一个