软件架构的可维护性指标——代码圈复杂度

1、目的

区别于常规的高内聚、低耦合、抽象、封装这种定性的指标,我想通过对软件架构可维护性的可量化的指标的分享,帮助大家在日常的开发工作中,有一个更为广阔的视角去审视我们编写的代码。即编写代码时,如果相关量化指标数据升高且超过一定的阈值,那么反问自己一下,当前的编码逻辑方便阅读吗?


2、前言

软件架构是软件开发和维护过程中的一个重要制品,是软件需求和设计、实现之间的桥梁。软件架构的开发和维护是基于架构软件软件生命周期中的重要环节,与之相关的步骤包括导出架构需求、架构开发、架构文档化、架构分析、架构实现和架构维护。软件架构的维护与演化密不可分,维护需要对软件架构的演化过程进行追踪和控制,以保障软件架构的演化过程能够满足需求(亦有说法将架构维护作为架构演化的一个部分)

那么如何衡量软件架构的可维护性呢?官方给出了六个指标

其中圈复杂度 (CCN) 度量整个架构的独立执行路径的条数,该结果值即为待评估架构的最终度量结果;而对于扇入扇出度 (FFC) 、模块间耦合度 (CBO) 、模块的响应 (RFC) 、紧内聚度 (TCC) 、松内聚度 (LCC) 个度量指标,它们针对每个组件进行度量,则待评估架构的最终度量结果为所有组件结果的平均值。


3、简介

由于在组件图中组件是独立的,每个组件代表一个系统或子系统中的封装单位,封装了完整的事务处理行为,组件图能够通过组件之间的控制依赖关系来体现整个系统的组成结构。对架构的组件图进行圈复杂度的度量,可以对整个系统的复杂程度做出初步评估,在设计早期发现问题和做出调整,并预测待评估系统的测试复杂度,及早规避风险,提高软件质量。圈复杂度高的程序往往是最容易出现错误的程序,实践表明程序规模以 CCN小于等于10 为宜。

圈复杂度 = 程序控制流图中分支节点数 + 1,其它计算方法不做过多说明。


4、案例

以项目中的代码片段为例

圈复杂度为37的方法

圈复杂度为10的方法

圈复杂度为5的方法

我们不难发现,圈复杂度越高的代码,往往方法也越长,在阅读代码逻辑时,过长的方法(一屏装不下)和流程细节很影响阅读体验。


5、降低

于其说是降低代码圈复杂度的方法,不如说是常见的代码优化手段:

  • 将条件判定提炼出独立函数
  • 将大函数拆成小函数
  • 以明确函数取代参数
  • 多态方式替代条件式
  • 移除控制标记

更多优化方法可以参考《重构》、《代码整洁之道》等书籍

优化的本质是,该抽象抽象,该封装封装,在面向对象语言(Java)的大背景下,代码首先应该考虑如何自下而上面向对象的设计,而不是考虑如何自上而下的流程编码。


6、插件

metricsreloaded

Method metrics:

  1. ev(G)(Essential Complexity (ev(G)):基本复杂度是用来衡量程序非结构化程度的,非结构成分降低了程序的质量,增加了代码的维护难度,使程序难于理解。因此,基本复杂度高意味着非结构化程度高,难以模块化和维护。实际上,消除了一个错误有时会引起其他的错误。
  2. Iv(G)(Module Design Complexity (iv(G))):模块设计复杂度是用来衡量模块判定结构,即模块和其他模块的调用关系。软件模块设计复杂度高意味模块耦合度高,这将导致模块难于隔离、维护和复用。模块设计复杂度是从模块流程图中移去那些不包含调用子模块的判定和循环结构后得出的圈复杂度,因此模块设计复杂度不能大于圈复杂度,通常是远小于圈复杂度。
  3. v(G)(Cyclomatic Complexity (v(G))):衡量圈复杂度,数量上表现为独立路径的条数,即合理的预防错误所需测试的最少路径条数,圈复杂度大说明程序代码可能质量低且难于测试和维护,经验表明,程序的可能错误和高的圈复杂度有着很大关系。

Class metrics:

  1. OCavg:代表类的方法的平均循环复杂度
  2. WMC:总循环复杂度

Package metrics:包复杂度

  1. v(G)avg:圈复杂度
  2. v(G)tot:总复杂度

Module metrics:模块复杂度

  • 同上

Project metrics:项目复杂度

  • 同上

7、总结

再次说明我们的目的不是去追求降圈复杂度的代码,因为过低的圈代码复杂度也不是最佳实践。而是要以有利架构维护的前提下,尽可能的提高代码的可读性。在开头的分享目的中提到,圈复杂度这一量化指标的提出,是帮助我们去审视编写的代码是否具有可读性。那么如何审视呢?

  1. 编写新业务流程时,当我们意识到方法过长或者圈复杂度过高时,应该考虑到需要对执行的流程进行抽象,至于抽象的方法是否需要满足xx设计、是否需要考虑未来的扩展,如果开发时间不允许,完全可以定义为private方法,而这个方法大概率也只有你自己会用。换个思路,将流程式的代码片段放在主逻辑中,也没有想要给给别人用。
  2. 维护业务时,当我们意识到新增的业务流程会影响原流程的圈复杂度,并且圈复杂度超过了可接受的值,那么维护相关的代码则需要考虑是否需要抽象,抽象程度同理

屎山代码往往不是一个人造成的,而我们能做的也只有做好自己



测试驱动的开发较低圈复杂度值 之间存在着紧密联系。

因为在编写测试用例时,开发人员会首先考虑代码的可测试性,从而倾向编写简单的代码(因为复杂的代码难以测试)。

一个好的测试用例设计经验是:创建数量与被测代码圈复杂度值相等的测试用例,以此提升测试用例对代码的分支覆盖率。