「这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战」
引子
在# Githook实践以代码规范检测插件golangci-lint为例 中我们提到了使用githook + golangci-lint来做在commit前对代码的质量进行检测。其中有一项代码复杂度检测的项目, 可以用来对项目中代码的复杂度做检测, 如果代码过于复杂则拒绝此次提交。使用代码复杂度检测,可以避免写出不可维护的代码。
说起复杂度,经常刷Leetcode的同学可能会立即联想到时间复杂度和空间复杂度,但实际上代码规范检测软件没办法做这么细致的检查。因此,代码规范检查软件通常使用了圈复杂度这一个概念对代码的复杂程度进行判断。
那么什么是圈复杂度呢?
简单来说,就是如果一段代码中流程控制语句的分支很多,就会让测试妹子的工作量超级加倍。
举个例子, 早期的《太吾绘卷》代码被反编译之后发现代码中存在大量的if语句,引来的大量讨论,这样做最显著的问题就是导致可维护性降低。(作者只学了一个月,要求不能太高,但是作为专业人士我们是要避免这种情况的滴)
业界实践
cyclop
cyclop是以插件的形式引入golangci-lint中,其代码复杂度判断方式较为简单(粗暴), 该插件会遍历代码中所有的函数, 然后遍历函数的语法树(AST)的所有结点,只要遇到以下节点则对代码复杂度+1
ast.FuncDecl在函数中定义了函数,可能是目标代码实现了闭包或者在函数内起了协程ast.IfStmt每写一个if, 圈复杂度加+1ast.ForStmt,ast.RangeStmt每写一个for, 圈复杂度+1ast.CaseClauseswitch 语句每多一个case语句,圈复杂度+1ast.CommClauseswitch 语句每多一个从channel接收数据的case, 圈复杂度+1ast.BinaryExpr每个表达式中如果存在and或者or逻辑判断,圈复杂度+1 核心逻辑如下所示
type complexityVisitor struct {
Complexity int
}
func (v *complexityVisitor) Visit(n ast.Node) ast.Visitor {
switch n := n.(type) {
case *ast.FuncDecl, *ast.IfStmt, *ast.ForStmt, *ast.RangeStmt, *ast.CaseClause, *ast.CommClause:
v.Complexity++
case *ast.BinaryExpr:
if n.Op == token.LAND || n.Op == token.LOR {
v.Complexity++
}
}
return v
}
评价: 以上方法统计圈复杂度虽然简单粗暴,可能存在错杀无辜的情况,但根据实际经验来看大多数情况下还是有起到防止“怀味道”代码的作用, 唯一例外的情况就是如果你需要使用switch-case来实现状态机此时就需要调整圈复杂度上限,或者在代码上加上//nolint。
gocyclo
gocyclo同样也是以插件的形式引入golangci-lint中的, 其对代码复杂度判断更加细致一点
- 遇到
if,for语句复杂度+1 - 遇到
switch-case语句复杂度+1,遇到default分支则不加+1 - 遇到
&&或者||运算符,则复杂度+1
type complexityVisitor struct {
// complexity is the cyclomatic complexity
complexity int
}
// Visit implements the ast.Visitor interface.
func (v *complexityVisitor) Visit(n ast.Node) ast.Visitor {
switch n := n.(type) {
case *ast.IfStmt, *ast.ForStmt, *ast.RangeStmt:
v.complexity++
case *ast.CaseClause:
if n.List != nil { // ignore default case
v.complexity++
}
case *ast.CommClause:
if n.Comm != nil { // ignore default case
v.complexity++
}
case *ast.BinaryExpr:
if n.Op == token.LAND || n.Op == token.LOR {
v.complexity++
}
}
return v
}
总结
以上开源实现的代码圈复杂度的检测软件核心思想都是通过遍历函数的抽象语法树(AST)的节点来对圈复杂度进行统计,如果遇到特定的节点则对代码圈复杂度+1.
如何降低圈复杂度
- 拆分大函数, 如果一个if语句/for循环过多,并且不存在上下相关联的情况则可以进行拆分
- 使用
map存储 条件到结果的映射,消除过多的if判断 - 优化你的算法😀