sonar关于认知复杂度的计算:前端如何降低代码认知复杂度?

认知复杂度简介

认知复杂度主要关注的是代码块的嵌套层次和控制流的复杂性。它与圈复杂度(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 结构,每增加一个条件分支都会增加认知复杂度。具体来说:

  • 每个 ifelse 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");
  }
}

在这个例子中,每个 ifelse if else都增加了 1 点认知复杂度,总共增加了 4 点认知复杂度。

switch-case

在 Sonar 中,if-else if-elseswitch-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 点认知复杂度。

实际影响

  1. 代码可读性

    • switch-case 结构通常比多个 if-else if-else 更易于阅读和维护,特别是在处理多个离散值时。
  2. 认知复杂度

    • 使用 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-caseif-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 适合条件较多且离散的情况。
  • 分离函数 适合将复杂逻辑拆分为多个小函数,职责单一。

最重要的是简化控制流减少嵌套层级提高可读性

你可能感兴趣的:(前端,javascript,开发语言)