龙空技术网

谈一谈代码复杂度的那些事

IT民工冯老师 306

前言:

而今朋友们对“delphihtml解析”大体比较注意,你们都想要知道一些“delphihtml解析”的相关内容。那么小编在网上网罗了一些对于“delphihtml解析””的相关内容,希望同学们能喜欢,我们一起来了解一下吧!

代码复杂度的那些事


前言

大家在工作时,经常听到周围有同事说"这个代码实现的好复杂啊"、"这个代码写的太复杂了,单元测试工作量太大了"、"这个业务用代码实现起来比较复杂,工作量需要再加XXX人月"….

那么代码的复杂程度到底是怎么衡量的呢?有什么工具能检查出来代码是否复杂吗?下面我们引入一个概念----代码的圈复杂度。


圈复杂度的定义

圈复杂度(Cyclomatic complexity)是一种代码复杂度的衡量标准,软件代码某部分的圈复杂度就是这部分代码中的线性无关路径数量。换句话说,圈复杂度就是在完全覆盖代码的情况下,需要编写的单元测试用例的最小数量。

备注:线性无关路径即代码里所有不可再分割的逻辑分支,比如'A且B'、'A或B'组合逻辑都需要按照两个分支进行计算。


圈复杂度计算方法

圈复杂度主要与分支语句(if、else、switch 、case、default等)的个数成正相关,可以在下图中看到常用到的几种语句的控制流图(表示程序执行流程的有向图),代码中每多一个分支语句,其代码的圈复杂度值就会增加一至多个。

各判断语句的控制流图

计算步骤:

· 从1开始,一直往下通过程序。

· 如果遇到以下关键字,或者其它同类的词,就加1:if,else,else if…

· 如果遇到以下关键字,或者其它同类的词,就根据实际代码逻辑加1至多个数值:while,for,and,or。

· 给case语句中的每一种情况都加1。

· else和default也进行圈复杂度计数。

· try语句中的每个catch/except,圈复杂度值加1。


圈复杂度的意义

以圈复杂度评价代码复杂度带来的风险。

圈复杂度个数与程序风险程度的关联性说明

建议:每个函数的圈复杂度最好都能保持在≤10的范围内。


采用圈复杂度去衡量代码有哪些好处呢?

1.通过圈复杂度分析出极复杂模块或方法,并对其进行代码细化,可以使得代码更加容易阅读和维护。

2.限制程序逻辑长度,降低程序质量风险。圈复杂度数值越高,测试用例越复杂,风险也越高。

3.方便制定测试计划,确定需要重点测试的函数。许多研究发现圈复杂度和模块或者方法的缺陷个数有正相关的关系,圈复杂度最高的模块及方法,其中的缺陷个数也最多,在测试时需要做重点测试。


开发流程对圈复杂度的影响

前面咱们了解到圈复杂度是代码复杂度的度量标准和计算方法,有些同学此时会有疑问,代码复杂度和软件开发流程具体有哪些关系呢?哪些开发阶段会影响代码的复杂程度?代码的复杂程度又怎么影响的程序质量呢?

为了方便描述,下面咱们就以常见的V型开发流程进行相关的介绍说明:

V型开发流程图

为了方便大家理解,下面用表的形式简单介绍一下每个开发阶段的输入、输出、执行目的(不同项目输出内容会存在一些差异):

各开发阶段职责说明

从上面的开发流程介绍可以看出来,上游阶段(编码前的所有开发阶段)全是为了代码实现做的设计,而下游阶段(编码后的所有开发阶段)全是为了验证编码与设计和需求的一致性。换句话说,就是上游的设计阶段决定代码编写的复杂度、准确度、全面性,下游的测试阶段保证代码编写与设计的一致性。

所以说,如果系统设计中接口调用越多、接口的参数和返回值判断越多,代码实现起来就越复杂;详细设计中函数实现的功能越复杂,函数的入参越多、函数内部的判断语句越多、类的继承深度越多、类的耦合程度越高,代码实现起来就越复杂…

代码的复杂度越高,单元测试用例编写的越多、单元测试耗时越多、程序的质量风险越大…


降低代码复杂度的方法

从前面的开发流程分析得知,想要降级代码的复杂度,我们需要从SD/PD和DD进行设计优化。

程序设计的基本思想,应该考虑低耦合、高内聚、根据可能的变化设计软件、合理的职责划分、使用接口而不是继承、封装变化、面向接口而不是实现、优先使用组合而非继承,代码设计时更需要考虑可读性、复用性、可维护性和易变更性。

降低代码复杂度主要有如下几个方法:

1.参考一些经典的设计原则进行代码设计,比如单一职责、开闭原则、依赖倒置原则、接口隔离原则…

2.提炼函数。把代码分割成细粒度的函数,然后给这些函数起一个恰当的名字,这是最简单最有效的做法。写代码的时候可以先写成一个函数,然后在过程中,不断地把"不相干的子问题"抽取出来,变成一个单独的函数。做到极致的效果就是,每个函数,别人只要看到名字和参数列表,就能知道这个函数是干什么的,而且可以自己实现出来。这样一来,代码的复杂度显然就降了下来;读代码的时候,其实大部分时候都是在读函数调用,都是在了解算法的逻辑,而不是细节。

3.减少逻辑嵌套,降低代码复杂度。深度嵌套的代码会大大降低代码可读性,并且容易出错。以下三条准则能帮助你在代码中尽量减少嵌套:

· 条件语句块尽量的简短。

· 当循环和分支超过两层时,考虑重构。

· 将嵌套逻辑移动到单独的函数中。例如,如果你需要遍历每个包含列表的对象列表(例如带有重复字段的协议缓冲区),则可以定义一个函数来处理每个对象,而不是使用双重嵌套循环。

4.大扇入小扇出

扇入:是指直接调用该模块的上级模块的个数(该模块被调用的次数)。扇入大表示模块的复用性好。

扇出:是指该模块直接调用的下级模块的个数(该模块调用其他模块的个数)。扇出大表示模块的业务逻辑复杂,需要控制和协调过多的下级模块;扇出过大一般是因为缺乏中间层次,应该适当增加中间层次的模块。扇出过小(例如总是1)也不好。扇出过小时可以把下级模块进一步分解成若干个子功能模块,或者合并到它的上级模块中去。

设计良好的软件结构,通常顶层扇出比较大,中间扇出小,底层模块则有大扇入。

5.简化条件表达式,比如逆向表达、分解或合并条件表达式、以多态取代条件式。把复杂的条件表达式拆成多条单独的条件表达式或单独的子函数中进行实现,可以降低条件表达式的耦合程度。

6.优化算法或替换算法。把代码实现比较复杂的算法进行优化,或换个逻辑相对简单一些的算法,不仅可以提高代码的安全性,代码也更容易阅读和维护。


圈复杂度检查工具

为了方便大家统计代码的圈复杂度,给大家推荐一款目前使用比较广泛的免费代码度量工具---SourceMonitor,其具有以下特点:

· 免费使用(官方下载网站: )

· 多语言支持:C++, C, C#, VB.NET, Java, Delphi, Visual Basic (VB6) or HTML

· Windows下GUI界面,界面友好,操作简单

· 支持命令行方式调用,可以集成到IDE或持续集成工具中

· 功能强大,可保存用于比较的基线(Baseline)

· 可导出XML、CSV文件,进一步统计分析


SourceMonitor度量对象:

· 产品

· 模块

· 单个源文件

· 单个方法


SourceMonitor主要功能:

· 计算平均圈复杂度,最大圈复杂度

· 所有源文件的圈复杂度

· 所有函数的圈复杂度

· 排序,找到最复杂的源文件、函数

· 其他指标,如嵌套深度,扇出数,函数大小


SourceMonitor用法:

· 参见官网下载安装包自带的帮助文档

· 支持命令行方式调用,可以集成到IDE或持续集成工具中


SourceMonitor操作范例:

使用windows版工具对测试代码(代码内容参考文章最后的附录部分)进行圈复杂度检查

测试文件的整体度量结果

测试文件的度量细节

测试文件内所有函数的度量结果

备注:度量信息说明,

· Complexity:当前函数的圈复杂度

· Statements:函数内部代码行数(不统计空行和花括号单独占用的行)

· Maximum Depth:函数内部逻辑嵌套深度(比如if…elseif…、if分支里面还有if判断:if(xx){ if(xx){}})…)

· Calls:当前函数调用的子函数个数


总结

在程序开发过程中,我们可以结合圈复杂度的检测工具/插件(SourceMonitor、PMD、checkstyle…),将高复杂度的代码进行适当的拆分、优化,可以大大提高代码整体的质量,减少潜在bug存在。

------------------------------------------------


附录

编写test.cpp代码用于进行圈复杂度检查

1. //test.cpp 测试圈复杂度的简单代码

2. #include <iostream>

3. using namespace std;

4.

5. int fun_A(int a, int b)

6. {

7. return (a + b);

8. }

9.

10. int fun_B(int a, int b)

11. {

12. if(0 == a)

13. {

14. return 0;

15. }

16. else if(0 == b)

17. {

18. return -1;

19. }

20. else

21. {

22. return (int)(a / b);

23. }

24. }

25.

26. int fun_C(int a)

27. {

28. if(0 == a)

29. {

30. return -1;

31. }

32.

33. return a;

34. }

35.

36. int fun_D(int a)

37. {

38. int sum = 0;

39. for(int i = 0; i < a; i++)

40. {

41. sum = sum + i * a;

42. }

43.

44. return sum;

45. }

46.

47. int fun_E(int a, int b)

48. {

49. if((0==a)&&(0==b))

50. {

51. return 0;

52. }

53.

54. return a * b;

55. }

56.

57. int main()

58. {

59. int a = 1;

60. int b = 2;

61.

62. int ret =fun_A(a,b);

63. cout<<ret<<endl;

64.

65. ret =fun_B(a,b);

66. cout<<ret<<endl;

67.

68. ret =fun_C(a);

69. cout<<ret<<endl;

70.

71. ret =fun_D(a);

72. cout<<ret<<endl;

73.

74. ret =fun_E(a,b);

75. cout<<ret<<endl;

76.

77. return 0;

78. }

标签: #delphihtml解析