降低代码的圈复杂度——复杂代码的解决之道

欢迎微信关注「SH的全栈笔记

0. 什么是圈复杂

可能你之前没有听说过这个词,也会好奇这是个什么东西是用来干嘛的,在维基百科上有这样的解释。

Cyclomatic complexity is a software metric used to indicate the complexity of a program. It is a quantitative measure of the number of linearly independent paths through a program's source code. It was developed by Thomas J. McCabe, Sr. in 1976.

简单翻译一下就是,圈复杂度是用来衡量代码复杂程度的,圈复杂度的概念是由这哥们Thomas J. McCabe, Sr在1976年的时候提出的概念。

1. 为什么需要圈复杂度

如果你现在的项目,代码的可读性非常差,难以维护,单个函数代码特别的长,各种if else case嵌套,看着大段大段写的糟糕的代码无从下手,甚至到了根本看不懂的地步,那么你可以考虑使用圈复杂度来衡量自己项目中代码的复杂性。

如果不刻意的加以控制,当我们的项目达到了一定的规模之后,某些较为复杂的业务逻辑就会导致有些开发写出很复杂的代码。

举个真实的复杂业务的例子,如果你使用TDDTest-Driven Development)的方式进行开发的话,当你还没有真正开始写某个接口的实现的时候,你写的单测可能都已经达到了好几十个case,而真正的业务逻辑甚至还没有开始写

再例如,一个函数,有几百、甚至上千行的代码,除此之外各种if else while嵌套,就算是写代码的人,可能过几周忘了上下文再来看这个代码,可能也看不懂了,因为其代码的可读性太差了,你读懂都很困难,又谈什么维护性和可扩展性呢?

那我们如何在编码中,CR(Code Review)中提早的避免这种情况呢?使用圈复杂度的检测工具,检测提交的代码中的圈复杂度的情况,然后根据圈复杂度检测情况进行重构。把过长过于复杂的代码拆成更小的、职责单一且清晰的函数,或者是用设计模式解决代码中大量的if else的嵌套逻辑。

可能有的人会认为,降低圈复杂度对我收益不怎么大,可能从短期上来看是这样的,甚至你还会因为动了其他人的代码,触发了圈复杂度的检测,从而还需要去重构别人写的代码。

但是从长期看,低圈复杂度的代码具有更佳的可读性、扩展性和可维护性。同时你的编码能力随着设计模式的实战运用也会得到相应的提升。

2. 圈复杂度度量标准

那圈复杂度,是如何衡量代码的复杂程度的?不是凭感觉,而是有着自己的一套计算规则。有两种计算方式,如下:

节点判定法

点边计算法

判定标准我整理成了一张表格,仅供参考。

圈复杂度 说明
1 - 10   代码是OK的,质量还行  
11 - 15   代码已经较为复杂,但也还好,可以设法对某些点重构一下  
16 - ∞   代码已经非常的复杂了,可维护性很低, 维护的成本也大,此时必须要进行重构  

当然,我个人认为不能够武断的把这个圈复杂度的标准应用于所有公司的所有情况,要按照自己的实际情况来分析。

这个完全是看自己的业务体量和实际情况来决定的。假设你的业务很简单,而且是个单体应用,功能都是很简单的CRUD,那你的圈复杂度即使想上去也没有那么容易。此时你就可以选择把圈复杂度的重构阈值设定为10.

而假设你的业务十分复杂,而且涉及到多个其他的微服务系统调用,再加上各种业务中的corner case的判断,圈复杂度上100可能都不在话下。

而这样的代码,如果不进行重构,后期随着需求的增加,会越垒越多,越来越难以维护。

2.1 节点判定法

这里只介绍最简单的一种,节点判定法,因为包括有的工具其实也是按照这个算法去算法的,其计算的公式如下。

圈复杂度 = 节点数量 + 1

节点数量代表什么呢?就是下面这些控制节点。

if、for、while、case、catch、与、非、布尔操作、三元运算符

大白话来说,就是看到上面符号,就把圈复杂度加1,那么我们来看一个例子。

测试计算圈复杂度

我们按照上面的方法,可以得出节点数量是13,那么最终的圈复杂度就等于13 + 1 = 14,圈复杂度是14,值得注意的是,其中的&&也会被算作节点之一。

2.2 使用工具

对于golang我们可以使用gocognit来判定圈复杂度,你可以使用go get github.com/uudashr/gocognit/cmd/gocognit快速的安装。然后使用gocognit $file就可以判断了。我们可以新建文件test.go。

package main

import (
 "flag"
 "log"
 "os"
 "sort"
)

func main() {
 log.SetFlags(0)
 log.SetPrefix("cognitive: ")
 flag.Usage = usage
 flag.Parse()
 args := flag.Args()
 if len(args) == 0 {
  usage()
 }

 stats := analyze(args)
 sort.Sort(byComplexity(stats))
 written := writeStats(os.Stdout, stats)

 if *avg {
  showAverage(stats)
 }

 if *over > 0 && written > 0 {
  os.Exit(1)
 }
}

然后使用命令gocognit test.go,来计算该代码的圈复杂度。

$ gocognit test.go
6 main main test.go:11:1

表示main包的main方法从11行开始,其计算出的圈复杂度是6

3. 如何降低圈复杂度

这里其实有很多很多方法,然后各类方法也有很多专业的名字,但是对于初了解圈复杂度的人来说可能不是那么好理解。所以我把如何降低圈复杂度的方法总结成了一句话那就是——“尽量减少节点判定法中节点的数量”。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wssxyw.html