背景
常听开发同学吐槽xx代码复杂度高,难以理解。"难以理解"是一种主观感受,为了解决这个问题,我们需要将主观感受量化成客观指标。因此我们引入圈复杂度 和认知复杂度两个概念,帮助我们量化代码复杂度。
概念
圈复杂度
圈复杂度,是一种代码复杂度的衡量标准。它可以用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径条数 ,也可理解为覆盖所有的可能情况最少使用的 测试用例 数。圈复杂度大说明程序代码的判断逻辑复杂,质量低且难于测试和维护。
计算方法
方法一
V(G) = E - N + 2
Where
E = the number of edges of the graph.
N = the number of nodes of the graph.
下面是一些典型的控制流程,如if-else,While,until和正常的流程顺序。
Below are some typical control flows, such as if-else, while, until, and normal sequential flows.

方法二 | Method Two
V(G) = P + 1
Where
P = the number of edges of the decision node.
圈复杂度等于控制流中判定节点的数量加一。常见的判定节点包括:
Cyclomatic complexity is equal to the number of decision nodes in the control flow plus one. Common decision nodes include:
if
for
switch case
select case
and / or
一个例子 | Example
css
A = 10
IF B > C THEN
A = B
ELSE
A = C
ENDIF
Print A
Print B
Print C
以上代码的控制流为:
The control flow of the above code is:

用方法一计算 :V(G) = 7 - 7 + 2 = 2
用方法二计算:V(G) = 1 + 1 = 2
圈复杂度的缺陷
圈复杂度只评估了代码的结构复杂性,对代码可读性的衡量不够准确。以下两段代码圈复杂度都是6,但是代码块1的可读性和可维护性明显比代码块2更好。
go
// code snippet 1
// Cyclomatic Complexity = 6
func getWeight(i int) string { // +1
switch {
case i <= 0: // +1
return "no weight"
case i < 10: // +1
return "light"
case i < 20: // +1
return "medium"
case i < 30: // +1
return "heavy"
case i < 40: // +1
return "very heavy"
default:
return "super heavy"
}
}
// code snippet 2 // Cyclomatic Complexity = 6
func sumOfNonPrimes(limit int) int { // +1
var bAdd bool = false
var sum int = 0
for i := 0; i < limit; i++ { // +1
if i <= 2 { // +1
continue
}
for j := 2; j < i; j++ { // +1
if i%j == 0 { // +1
bAdd = false break
}
bAdd = true
}
if bAdd { // +1
sum += i
} else {
sum -= 1
}
}
return sum
}
认知复杂度 | Cognitive Complexity
为了弥补圈复杂度的不足,我们引入认知复杂度概念。与圈复杂度主要考虑代码的结构复杂性不同,认知复杂度更关注代码的可读性、嵌套程度以及理解代码逻辑时的认知负担。
计算方法 | How to Caculate
认知复杂度的计算基于以下规则:
- 线性代码:不会增加认知复杂度。
- 分支结构(如
if
,else if
,switch
,else
):增加1的认知复杂度。 - 循环结构(如
while
,for
):增加1的认知复杂度。 - 嵌套结构:每增加一层嵌套,复杂度额外增加1。
- 逻辑运算符:连续
&&
或||
不额外增加认知复杂度
注:不同统计工具的规则略有不同
两者主要区别 | Differences Between the Two Types of Complexity
- 圈复杂度对每一个
switch case
加1分,但是认知复杂度将switch
整体视为1分 - 认知复杂度增加了对嵌套深度的计分,嵌套结构越深,得分越高
- 认知复杂度增加了对
else
的计分 - 圈复杂度对每一个
&&
和||
计分,认知复杂度仅对一连串不同的逻辑运算符计分 - 回顾以下两个代码块,两者圈复杂度都是6,但是认知复杂度分别为1和10,认知复杂度得分更好得衡量了代码的可读性
go
// code snippet 1
// Cognitive Complexity = 1
func getWeight(i int) string {
switch { // +1
case i <= 0:
return "no weight"
case i < 10:
return "light"
case i < 20:
return "medium"
case i < 30:
return "heavy"
case i < 40:
return "very heavy"
default:
return "super heavy"
}
}
// code snippet 2
// Cognitive Complexity = 10
func sumOfNonPrimes(limit int) int {
var bAdd bool = false
var sum int = 0
for i := 0; i < limit; i++ { // +1
if i <= 2 { // +2
continue
}
for j := 2; j < i; j++ { // +2
if i%j == 0 { // +3
bAdd = false break
}
bAdd = true
}
if bAdd { // +2
sum += i
} else { // +1
sum -= 1
}
}
return sum
}
如何使用指标 | How to Use this Metrics
圈复杂度和认知复杂度是对代码质量的一种简单直观的衡量。圈复杂度低的代码不一定是好代码,但是圈复杂度高的代码必定是质量低的。因此,为代码仓库的圈复杂度和认知复杂度划定上限,是提升代码质量的有效方法。
本地统计工具
以go语言为例,可以用
-
Cyclomatic complexity
- gocyclo
-
Cognitive complexity
- gocognit
bash
# install
go install github.com/fzipp/gocyclo/cmd/gocyclo@latest
go install github.com/uudashr/gocognit/cmd/gocognit@latest
# Usage:
gocyclo [flags] <Go file or directory> ...
gocognit [flags] <Go file or directory> ...