sonar关于认知复杂度的计算:前端如何降低代码认知复杂度?
认知复杂度主要关注的是代码块的嵌套层次和控制流的复杂性。它与圈复杂度(Cyclomatic Complexity)不同,后者更多地关注代码路径的数量。认知复杂度更注重代码的可读性和理解难度。我们的代码认知复杂度为什么很高?嵌套层级太深、else-if 太多。
认知复杂度简介
认知复杂度主要关注的是代码块的嵌套层次和控制流的复杂性。它与圈复杂度(Cyclomatic Complexity)不同,后者更多地关注代码路径的数量。认知复杂度更注重代码的可读性和理解难度。
我们的代码认知复杂度为什么很高?
嵌套层级太深、else-if 太多。
认知复杂度计算方式
认知复杂度的计算主要基于以下因素:
嵌套层级
:每增加一层嵌套,复杂度 +1。
条件分支
:每个 if、else if、else、for、while 复杂度 +1。
逻辑运算符
:每个逻辑运算符(如 &&、||、?),复杂度 +1。
捕获异常
: catch 捕获异常语句 复杂度 +1
中断语句
: continue 或 break 复杂度 +1。
递归循环中的每种方法
: 复杂度 +1。
函数调用
:函数调用本身不增加复杂度,但如果函数内部逻辑复杂,会影响整体复杂度。简单函数调用(如 console.log)不增加复杂度。
注意:
let flag3 = (obj && obj.name) || (obj.age && obj.address); // +3
let flag4 = obj && obj.name && obj.age && obj.address; // +1
function example(value) {
// +1
if (value === "A") {
console.log("Option A");
// +1
} else {
// +1 +1
if (value === "B") {
console.log("Option B");
// +1 +1
} else if (value === "C") {
console.log("Option C");
}
}
}
if -else
对于 if-else if-else
结构,每增加一个条件分支都会增加认知复杂度。具体来说:
- 每个
if
或else if
都会增加 1 点认知复杂度。 - 嵌套的条件语句也会增加认知复杂度。
示例:
function example(value) {
if (value === "A") {
console.log("Option A");
} else if (value === "B") {
console.log("Option B");
} else if (value === "C") {
console.log("Option C");
} else {
console.log("Unknown option");
}
}
在这个例子中,每个 if
和 else if
else
都增加了 1 点认知复杂度,总共增加了 4 点认知复杂度。
switch-case:
在 Sonar 中,if-else if-else
和 switch-case
结构的认知复杂度计算方式有所不同。
以下是它们的主要区别:
虽然 switch-case
也包含多个分支,但 Sonar 对 switch-case
的处理更为宽容,通常不会为每个 case
增加额外的认知复杂度。
相反,整个 switch
语句通常只增加固定的少量复杂度(通常是 1 点)。
示例:
function example(value) {
switch (value) {
case "A":
console.log("Option A");
break;
case "B":
console.log("Option B");
break;
case "C":
console.log("Option C");
break;
default:
console.log("Unknown option");
break;
}
}
在这个例子中,尽管有多个 case
分支,但整个 switch
语句通常只会增加 1 点认知复杂度。
实际影响
-
代码可读性:
switch-case
结构通常比多个if-else if-else
更易于阅读和维护,特别是在处理多个离散值时。
-
认知复杂度:
- 使用
switch-case
可以减少认知复杂度,尤其是在有多个条件分支的情况下。 - 如果你发现
if-else if-else
结构导致较高的认知复杂度,可以考虑使用switch-case
来替代。
- 使用
除了选择合适的控制结构外,还可以通过以下方法进一步降低认知复杂度:
提前返回
通过提前返回减少嵌套层级,这通常被称为“保卫子句”。
// 原始代码 认知复杂度
function processData(data) {
// +2 if+1 && +1
if (data !== null && data !== undefined) {
// +2 if+1 嵌套层级+1
if (data.length > 0) {
// Process data
console.log("Processing data");
}
}
}
// 重构后
function processDataRefactored(data) {
if (!data || data.length === 0) return; //+2
console.log("Processing data");
}
提取函数:
将复杂的逻辑提取到单独的函数中,减少主函数的复杂度。
function processValue(value) {
switch (value) {
case "A":
handleOptionA();
break;
case "B":
handleOptionB();
break;
case "C":
handleOptionC();
break;
default:
handleUnknownOption();
break;
}
}
function handleOptionA() {
console.log("Option A");
}
function handleOptionB() {
console.log("Option B");
}
function handleOptionC() {
console.log("Option C");
}
function handleUnknownOption() {
console.log("Unknown option");
}
使用映射表(Map or Object) :
将条件与对应的处理函数映射到一个对象中,通过查找映射表来调用
相应的处理函数。。
const handlers = {
A: () => console.log("Option A"),
B: () => console.log("Option B"),
C: () => console.log("Option C"),
default: () => console.log("Unknown option"),
};
function processValue(value) {
(handlers[value] || handlers["default"])();
}
const actionMap = new Map([
["login", () => console.log("Processing login...")],
["register", () => console.log("Processing register...")],
["logout", () => console.log("Processing logout...")],
["resetPassword", () => console.log("Processing reset password...")],
]);
function handleUserAction(action) {
const handler =
actionMap.get(action) || (() => console.log("Unknown action"));
handler();
}
// 调用示例
handleUserAction("register"); // 输出: Processing register...
handleUserAction("unknown"); // 输出: Unknown action
优点:
使用 Map 存储逻辑,查找效率高。
代码结构清晰,易于扩展。
认知复杂度:1 点(actionMap.get(action)的隐含条件)
策略模式:
将每个条件分支逻辑封装到一个独立的函数中,然后根据条件调用
相应的函数。
const strategies = {
condition1: function () {
// 处理逻辑1
console.log("Condition 1 met");
},
condition2: function () {
// 处理逻辑2
console.log("Condition 2 met");
},
condition3() {
// 处理逻辑3
console.log("Condition 3 met");
},
// 更多条件...
default: function () {
// 默认处理逻辑
console.log("Default condition met");
},
};
// 根据条件调用策略
function handleCondition(condition) {
const strategy = strategies[condition] || strategies["default"];
strategy();
}
// 示例调用
handleCondition("condition1"); // 输出: Condition 1 met
handleCondition("unknown"); // 输出: Default condition met
优点:
将逻辑分散到对象中,易于扩展和维护。
认知复杂度降低,代码更清晰。
举例分析
原始代码(高认知复杂度)
function handleUserAction(user, action) {
if (user) {
if (user.age >= 18) {
if (action === "login") {
console.log("User logged in");
} else if (action === "register") {
console.log("User registered");
} else if (action === "logout") {
console.log("User logged out");
} else {
console.log("Unknown action");
}
} else {
console.log("User is underage");
}
} else {
console.log("Invalid user");
}
}
问题:
注意:第一个 if 不算嵌套,只有嵌套在内部的 if 才会增加嵌套层级。
- 认知复杂度:1 +2 +3 +1 +1 +1 +1 = 10 || 嵌套:1+1 条件分支:8
- 深层嵌套的
if-else
导致认知复杂度高。 - 代码难以扩展和维护。
方法 1:使用策略模式
将不同 action
的处理逻辑映射到一个对象中,通过键值对直接调用对应的处理函数。
const actions = {
login(user) {
console.log("User logged in");
},
register(user) {
console.log("User registered");
},
logout(user) {
console.log("User logged out");
},
default(user) {
console.log("Unknown action");
},
};
function handleUserAction(user, action) {
if (!user) {
console.log("Invalid user");
return;
}
if (user.age < 18) {
console.log("User is underage");
return;
}
const handler = actions[action] || actions.default;
handler(user);
}
认知复杂度: 3
优点:
- 将逻辑分散到对象中,易于扩展和维护。
- 减少嵌套层级,降低认知复杂度。
方法 2:使用映射表
将条件和对应的处理逻辑存储在一个 Map
中,通过查找表来执行逻辑。
const actionMap = new Map([
["login", (user) => console.log("User logged in")],
["register", (user) => console.log("User registered")],
["logout", (user) => console.log("User logged out")],
]);
function handleUserAction(user, action) {
if (!user) {
console.log("Invalid user");
return;
}
if (user.age < 18) {
console.log("User is underage");
return;
}
const handler =
actionMap.get(action) || (() => console.log("Unknown action"));
handler(user);
}
认知复杂度: 3
优点:
- 使用
Map
存储逻辑,查找效率高。 - 代码结构清晰,易于扩展。
方法 3:使用 switch-case
将 action
的逻辑用 switch-case
替代 if-else
。
function handleUserAction(user, action) {
if (!user) {
console.log("Invalid user");
return;
}
if (user.age < 18) {
console.log("User is underage");
return;
}
switch (action) {
case "login":
console.log("User logged in");
break;
case "register":
console.log("User registered");
break;
case "logout":
console.log("User logged out");
break;
default:
console.log("Unknown action");
}
}
认知复杂度:3
优点:
- 当
action
的条件较多且离散时,switch-case
比if-else
更清晰。
方法 4:分离函数
将不同逻辑拆分为多个小函数,每个函数只负责一个单一的任务。
function validateUser(user) {
if (!user) {
console.log("Invalid user");
return false;
}
if (user.age < 18) {
console.log("User is underage");
return false;
}
return true;
}
function handleLogin(user) {
console.log("User logged in");
}
function handleRegister(user) {
console.log("User registered");
}
function handleLogout(user) {
console.log("User logged out");
}
function handleUserAction(user, action) {
if (!validateUser(user)) return;
if (action === "login") {
handleLogin(user);
} else if (action === "register") {
handleRegister(user);
} else if (action === "logout") {
handleLogout(user);
} else {
console.log("Unknown action");
}
}
认知复杂度:validateUser 2 handleUserAction 5
优点:
- 将逻辑拆分为多个小函数,职责单一。
- 代码更模块化,易于测试和维护。
总结
- 策略模式 和 映射表 适合处理离散的条件逻辑。
- switch-case 适合条件较多且离散的情况。
- 分离函数 适合将复杂逻辑拆分为多个小函数,职责单一。
最重要的是简化控制流
、减少嵌套层级
、提高可读性
!
更多推荐
所有评论(0)