最近遇到一个课题 当我拿到一段静态代码,如何更好地衡量出这段源代码的复杂性,也就是说如何从静态(不运行起来地情况下)去衡量一段函数或者代码片段的所含信息和所含结构复杂的程度。
这个其实属于代码度量
领域,跟代码的维护性息息相关。
1 各种度量
通过搜集资料,发现其实从各种角度都可以去衡量代码复杂度,简单例如:
- 代码行数:通常情况下,代码行数越多,程序复杂度越高。
- 程序结构:程序结构的复杂度可以通过层数、循环、分支等结构来衡量。
- 变量的数量和类型:变量的数量和类型越多,程序复杂度越高。
- 函数的数量和复杂度:函数的数量和复杂度越高,程序复杂度越高。
- 对象的数量和关系:如果程序中使用了面向对象编程,那么对象的数量和关系也是衡量复杂度的一个重要指标。
- 控制流程的复杂度:控制流程的复杂度可以通过程序的控制流图来衡量,控制流程的复杂度越高,程序的复杂度也越高。
- 数据结构的复杂度:数据结构的复杂度可以通过数据结构的类型、数量、层级等来衡量
然而,我希望能拥有一个公式,将上述的多个项目都用起来,这就引出了 MI 指数( Maintainability)
,也就是 可维护性指数 。
2 可维护性指数 MI
MI指数计算代码度量的经典公式,如下所示:
Maintainability Index = 171 - 5.2 * ln(Halstead Volume) - 0.23 * (Cyclomatic Complexity) - 16.2 * ln(Lines of Code)
其中的三个参数是:
- Halstead Volume(代码容量)
- Cyclomatic Complexity(圈复杂度)
- Lines of Code(代码行数)
值得注意的是,使用此公式意味着值的范围是从 171 到无限负数。
当代码趋向于 0 时,显然很难维护代码,并且值为 0 的代码和为某个负值的代码之间的差异没什么用处。 由于负数的用处很低,并且我们希望让指标尽可能清晰,我们决定将所有 0 或更小的指数都视为 0,然后重新定义 171 或定义成更小的范围,即 0 到 100。
出于此原因,公式可以修改为:
Maintainability Index = MAX(0,(171 - 5.2 * ln(Halstead Volume) - 0.23 * (Cyclomatic Complexity) - 16.2 * ln(Lines of Code))*100 / 171)
写成python代码就是
mi = max(float(0), (171 - 5.20 * np.log(hv) - 0.23 * cc - 16.20 * np.log(lloc)) * 100.0 / 171.0)
3 示例函数
这里写一段计算mi用的核心python函数
注意:前面需要让源代码经过解析器解析,拿到代码行数、条件语句数、操作符、操作数等信息,这里我用的是将代码转成AST抽象语法树再处理)
def mi_val_calcu(lloc, node_list, operator_dict, operands_dict):
""" calculate MI value of function frags
lloc: 逻辑代码行数,一句完整的代码语句,才是一行
node_list:通过代码解析器得到的AST的每个结点的列表
operator_dict:存储了这段代码里的操作符operator的种类和个数 {'return':1, '+':2,'printf':3,...}
operands_dict:存储了这段代码里的操作数operands的种类和个数 {’a‘:1,'0':2,'ans':1,...}
"""
# 1. 逻辑代码行数 LOC
# 这里的lloc通过cpp代码的“;”分号来计算
# [1,...] 使用max是为了避免log(lloc)趋近负无穷
lloc = max(1, lloc)
# 2. 圈复杂度 CC
# 计算条件节点个数加1, 即 cc = P + 1
# 这里是在数if elseif switch-case的数目
cc = len([n for n in node_list if n in if_statements]) + 1
# 3. HV体积
# 计算操作符和操作数
# n = n1 + n2 Program Vocabulary
n = len(operator_dict.keys()) + len(operands_dict.keys())
# N = N1 + N2 Program length
N = sum(operator_dict.values()) + sum(operands_dict.values())
# Halstead Volume
# [1,...] 使用max避免log(lloc)趋近负无穷
hv = N * np.log2(n) if n * N != 0 else 1
# MI 公式 结果范围在(0,100)
mi = max(float(0), (171 - 5.20 * np.log(hv) - 0.23 * cc - 16.20 * np.log(lloc)) * 100.0 / 171.0)
return mi
谢谢!
参考资料来源: