圈复杂度介绍
圈复杂度(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.
计算公式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
- if语句
- while语句
- for语句
- case语句
- catch语句
- and和or布尔操作
- ?:三元运算符
示例:
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
圈复杂度的检测工具
项目 | sonarqube | eslint | codemetrics |
---|---|---|---|
圈复杂度度量标准 | 支持 | 支持 | 支持 |
检测效率 | 高 | 中 | 低 |
精度 | 高 | 中 | 低 |
支持的编程语言 | 多 | 一般 | 一般 |
对圈复杂度高的代码的指导性 | 高 | 中 | 低 |
sonarqube
- 安装SonarQube服务器
SonarQube服务器可以通过下载和安装来获取,也可以在云上进行部署。你可以从SonarQube的官方网站上下载并安装相应的版本。安装完成后,需要启动SonarQube服务器。
- 配置SonarQube服务器
安装完成后,需要在SonarQube服务器中进行一些配置,例如配置数据库和LDAP等。你可以参考SonarQube的官方文档来进行相应的配置。
- 安装SonarQube扫描器
SonarQube扫描器可以安装在本地开发机器或者CI/CD服务器上。你需要下载并安装相应的扫描器,然后配置扫描器与SonarQube服务器的连接。
- 配置项目
在SonarQube服务器中,你需要为每个项目进行相应的配置。你可以在SonarQube界面中手动创建项目,也可以使用SonarQube API进行自动化配置。在配置项目时,需要设置项目名称、语言类型、代码仓库地址等信息。
- 运行SonarQube扫描器
在配置完成后,你需要使用SonarQube扫描器对代码进行扫描。在扫描时,你需要指定要扫描的代码路径、扫描器的参数等。扫描器将会把扫描结果上传到SonarQube服务器中。
eslint
使用ESLint检测圈复杂度的步骤:
- 安装ESLint
在命令行中执行以下命令安装ESLint:
npm install eslint --save-dev
- 安装eslint-plugin-complexity插件
在命令行中执行以下命令安装eslint-plugin-complexity插件:
npm install eslint-plugin-complexity --save-dev
- 配置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。
- 运行ESLint
在命令行中执行以下命令来运行ESLint:
npx eslint yourfile.js
其中,"yourfile.js"为待检测的JavaScript文件。
运行结果将会显示每个函数的圈复杂度是否超过了配置的最大值,如果超过了,ESLint将会给出相应的警告和建议。
codeMetrics
插件商店直接搜索codeMetrics,直接安装既可。
检测结果。
如何保障代码质量
- 单一职责原则
单一职责原则是指每个类或方法应该只有一个责任。如果一个方法或类的职责过于复杂,那么它就很容易产生高圈复杂度。通过将复杂的方法或类拆分为多个小方法或类,每个方法或类只关注一个特定的任务,可以有效地降低圈复杂度。
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 {};
}
}
- 开闭原则
开闭原则是指软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。通过采用开闭原则,可以使我们的代码更加容易扩展,从而避免出现复杂的控制流程和高圈复杂度。
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);
}
}
- 去除重复代码
重复的代码是代码复杂度的一种来源。通过去除重复的代码,可以将代码块中的控制流程减少到最小,从而降低圈复杂度。
- 提炼函数
将复杂的代码块提炼到一个单独的函数中,可以将控制流程减少到最小,从而降低圈复杂度。
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;
}
- 引入多态
引入多态可以避免复杂的条件语句,从而使代码更加简洁和易于理解。多态是一种在运行时根据对象类型选择方法的机制,它可以避免使用复杂的条件语句,从而减少代码块中的控制流程,降低圈复杂度。
// 抽象的动物类
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());
- 提前返回
通过提前返回可以避免过多的条件语句和嵌套,从而减少圈复杂度。使用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;
}
- 使用多个小函数
使用多个小函数可以使代码更加模块化,从而降低圈复杂度。每个小函数只需要关注一个具体的任务,从而避免出现复杂的控制流程。
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;
}
- 使用函数式编程
函数式编程是一种通过函数组合来构建复杂程序的编程范式,它可以避免出现复杂的控制流程和高圈复杂度。函数式编程中的函数通常都是纯函数,即给定相同的输入,始终返回相同的输出,因此不会受到外部环境的影响。
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);
}