代码质量评价维度,很多都是些主观性的评价维度,需要有专门的人员去查看评判代码,对于审核的人员代码能力要求比较高,而且有时候往往不同的人审核会得出不同的结论,会有争议。然而也有些对代码客观的分析方式可以帮助我们识别代码质量,节省大量人力去分析代码。比如代码复杂度的分析。
判断代码复杂度有多种不同的方式,下面介绍一些其中比较重要的判断代码复杂度的方式:
一 圈复杂度(Cyclomatic complexity,V(G))
1.1 圈复杂度介绍
圈复杂度(Cyclomatic complexity)也称为条件复杂度或循环复杂度,是一种软件度量,是由Thomas J. McCabe在1976年提出,用来表示程序的复杂度,其符号为VG或是M。圈复杂度是用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数,
即合理的预防错误所需测试的最少路径条数,圈复杂度大说明程序代码可能质量低且难于测试和维护,经验表明,程序的可能错误和高的圈复杂度有着很大关系。独立路径组成的集合称为基本路径集合,独立路径数就是指基本路径集合中路径的数量。
基本路径集合不是唯一的,独立路径数也就不唯一。因此,圈复杂度是最大独立路径数。
1.2 控制流图
圈复杂度的计算一般都是需要根据控制流图来进行计算,控制流图(Control Flow Graph, CFG)是一个过程或程序的抽象表现,代表了一个程序执行过程中会遍历到的所有路径。流图中有3个概念:节点、边、区域。流图中的圆称为节点,一个节点代表一条或多条语句;
流图中的箭头线称为边,代表控制流;由边和节点围成的部分称为区域。
下面是从网上借鉴了一个程序流程图转换成控制流图的示意图,如下所示:
其中,2、3和4、5节点都是对应程序流程图中的两个语句,但它们都是在同一条路径上,所以在流图中只是一个节点。
1.3 圈复杂度计算
圈复杂度计算一般有以下几计算方式,下面根据流图再来看一下圈复杂度计算方式:
1.通过流图中点边进行计算: V(G) = E−N+2
其中E是流图中边的条数,N是流图中节点数。所以使用这种方式计算,上图的V(G) =11 - 9 +2 = 4
2. 通过流图中判定节点计算: V(G) = P+1
其中P是流图中判定节点的数目。在流图中当一个节点分出两条或多条边指向其他节点时,这个节点就是一个判定节点。上面流图中p为3 所以上图的 V(G) = 3+1 = 4
3. 通过流图中区域计算: V(G) = R
其中R是区域数。计算区域数时不仅包括由边和节点围起来的区域,也包括图外部未被围起来的那个区域。上面流图中R为4 所以上图的V(G) = 4
4. 通过条件表达式计算:
上面的常规计算方法需要画出控制流图有些麻烦,我们可以粗暴一点根据判定条件节点计算法粗略估计,其实也就是上图中的第二个计算方式。
一般圈复杂度中的判定节点是指下面这些条件语句:
条件语句: if语句、while语句(包含do...while...语句)、for语句(包含foreach语句)、switch case语句。
条件表达式(二元或多元):&& 表达式、|| 表达式、三元运算符。
注: 这里强调下圈复杂度是不包括try/catch的,网上凡是带有try/catch的文章,写的都是错的。
我们计算时,正常的顺序代码,圈复杂度加1,然后遇到上面的判定节点就加1。比如现在有段代码,先把圈复杂度初始为1,然后遇到一个if语句就加1,遇到一个for语句就加1 ,那这段代码圈复杂度就是3。
需要注意的是,判定节点计算法对于多分支的 case 结构或多个 if - else 结构,每个case或者 else 都需要统计。
圈复杂度是合理的预防错误所需测试的最少路径条数。圈复杂度大说明程序代码的判断逻辑复杂,程序代码可能质量低且难于测试和维护,根据经验,程序的可能错误和高的圈复杂度有着很大关系。
二 认知复杂度(Cognitive Complexity, Cogc)
前面介绍了圈复杂度,但是圈复杂度是有一些缺陷的,以下引用自sonar官方白皮书翻译:
Thomas J. McCabe 的圈复杂度长期以来一直是衡量方法控制流复杂度的事实标准。它最初的目的是“识别难以测试或维护的软件模块”[1],但虽然它准确计算了完全覆盖一个方法所需的最少测试用例数,但它并不是一个令人满意的可理解性度量。这是因为具有相同圈复杂度的方法不一定给维护者带来相同的难度,导致测量通过高估某些结构而低估其他结构的“假象”。
同时,圈复杂度不再是全面的。它于 1976 年在 Fortran 环境中制定,不包括现代语言结构,如try/catch和lambdas。最后,因为每个方法的最小圈复杂度分数为 1,所以不可能知道任何具有高聚合圈复杂度的给定类是属于大型的、易于维护的类还是具有复杂控制流的小类。除了类级别之外,人们普遍认为应用程序的圈复杂度分数与其代码行总数相关。换句话说,圈复杂度在方法级别之上几乎没有用处。
简单来说就是,圈复杂度已经有些过时并不能涵盖现有程序语言结构,而且也并不能衡量代码的可理解性,那么有什么方式解决圈复杂度的这些问题呢?
针对这点,我们可以使用认知复杂度:
为了解决这些问题,我们制定了认知复杂度(Cognitive Complexity)用于解决现代语言结构,并在类和应用程序级别产生有意义的值。更重要的是,它脱离了基于数学模型评估代码的实践,因此它可以产生与程序员对理解这些流程所需的心理或认知相对应的控制流评估。
认知复杂度打破了使用数学模型来评估软件可维护性的做法。它使用人类判断来评估应该如何计算结构,并决定应该将什么添加到整个模型中。因此,它产生的方法复杂性分数让程序员觉得比以前的模型更公平的可理解性相对评估。此外,由于认知复杂度
不收取方法的“入门成本”,因此它不仅在方法级别,而且在类和应用程序级别都会产生更公平的相对评估。
如果你需要识别代码的理解难度,那就应该使用认知复杂度。比如下面我引用下sonar官方的一个例子,下面这段代码,圈复杂度都是4,但是我们很明显的可以看出来,两边代码的可理解性是不相同的
我们再来看看如果用认知复杂度,结果是什么
通过这个例子,可以比较明显的看出,相比圈复杂度,认知复杂度更适合我们对代码可理解性的进行量化评估。
我们再来看下认知复杂是怎么计算的,下面引用下认知复杂度基本规则评估方式:
认知复杂度分数根据三个基本规则进行评估:
1. 忽略允许将多个语句易于理解地简写成一个的情况。
2. 在代码线性流程中的每一次中断都增加(+1)(复杂度)。
3. 断流结构嵌套时增加(复杂度)
简单来说,认知复杂度中,多个语句简写为一个语句,复杂度不会增加,如果线性代码逻辑中,逻辑被一些条件语句或条件表达式打断一次,复杂度就会加一,如果打断逻辑的语句是一个嵌套的语句,那么复杂度需要额外再加上嵌套所带来的复杂度。
下面再举个例子说明下:
认知复杂度更多相关内容,可以参考sonar官方白皮书,详见以下链接:
https://www.sonarsource.com/docs/CognitiveComplexity.pdf
三 基本复杂度(Essential Complexity, ev(G))
基本复杂度是用来衡量程序非结构化程度的,非结构成分降低了程序的质量,增加了代码的维护难度,使程序难于理解。因此,基本复杂度高意味着非结构化程度高,难以模块化和维护。实际上,消除了一个错误有时会引起其他的错误。
将圈复杂度图中的结构化部分简化成一个点,计算简化以后流程图的圈复杂度就是基本复杂度。
四 模块设计复杂度(Module Design Complexity, iv(G))
模块设计复杂度是用来衡量模块判定结构,即模块和其他模块的调用关系。软件模块设计复杂度高意味模块耦合度高,这将导致模块难于隔离、维护和复用。模块设计复杂度是从模块流程图中移去那些不包含调用子模块的判定和循环结构后得出的圈复杂度,因此模块设计复杂度不能大于圈复杂度,通常是远小于圈复杂度。
除了上面介绍的一些复杂度之外,还有些其他复杂度比如设计复杂度,集成复杂度,规范化复杂度等等,就不再展开。
五 复杂度计算工具
上面复杂度计算都有些麻烦,平时如果自己去计算工作量不小,不过目前都已经有专门的工具用来计算这些复杂度,比如Metrics Reloaded,SonarLint等等,这些工具都有相应的idea插件,平时开发时使用这些工具就可以做到代码实时检测分析代码复杂度,可以有效
的帮我们识别代码复杂度。至于使用方法,这里也不再展开了。
下面这个是常见的复杂度标准,以供参考:
六 如何降低复杂度
降低代码复杂度的方式比较多,复杂度主要是条件语句和条件表达式引起的,所以降低复杂度的方法,也就是减少这些语句,一般有以下常用方法:
1.将方法拆分,抽出部分独立逻辑组成小的方法。
2.尽量简化、合并条件表达式,比如合并多个同义的if语句,使用多元运算符代替if else等。
3.减少else,比如可以调整表达式顺序,使用卫语句等等
4.使用lambda代替部分逻辑。lambda虽然也会增加复杂度,但有些场景下会比条件表达式复杂度低