关于代码质量, 每个人有每个人的评价标准。但有一点是可以肯定的:清晰简单的代码,代码质量较高;扩展性较好的代码,代码质量较高。自从计算机产生之时,人们就不停的研究如何客观评价代码质量的方法,即某种可定性分析代码是否存在风险的东西。令人高兴的是,现在已经有了不少方法可定性的分析代码质量,本实用经验要讨论的圈复杂度就是一个代码复杂度的定性计算方法。
所谓圈复杂度是一种代码复杂度的衡量标准,中文名称叫做圈复杂度,简称CC,其由 Thomas McCabe于1975年定义。
在软件测试的概念里,圈复杂度“用来衡量一个模块判定结构的复杂程度,数量上表现为独立现行路径条数,即合理的预防错误所需测试的最少路径条数,圈复杂度大说明程序代码可能质量低且难于测试和维护,根据经验,程序的可能错误和高的圈复杂度有着很大关系”。所以我们可得出这样的结论,圈复杂度高的函数其产生错误的概率也会很高。
说明
- 简单的说函数的圈复杂度就是统计一个函数有多少个分支(if, while, for,等等),如果没有分支的话,其复杂度为1,每增加一个分支复杂度加1。这儿需要注意的是:无论这些分支是并列还是嵌套,统统都是加1。
- 从一个非常简单的角度来理解,一个函数的圈复杂度就相当于至少需要多少个测试用例才能对这个函数做到全路径覆盖。
一般而言,圈复杂度用来评价代码复杂度,以函数为单位,数值越大表示代码的逻辑分支越多,理解起来也更复杂。圈复杂度可以成为编码及重构的重要参考指标,以指导撰写可读性高的代码。有关圈复杂度的计算,《代码大全2》给出了下述的方法:
(1)圈复杂度CC由1计数,一直往下通过程序;
(2)一旦遇到以下关键字或其同类的词,CC就加1:if,while,repeat,for,and,or
(3)Switch-case语句的每一种情况都加1。
条件语句、循环语句都是逻辑分支语句。if else、switch-case、for、while都代表逻辑分支。一个逻辑分支,就代表两条可能执行路径。可能执行路径数目的增长,相对于逻辑分支的个数来说,是指数增长。比如一个method有n个逻辑分支,那么可能执行路径数目就是2的n次方。
降低函数的复杂度是提供软件质量的一个重要手段。一般情况下有9种技术可直接降低函数的圈复杂度。他们是提炼函数,替换算法,分节条件表达式,合并条件式,合并重复的条件片段,移除控制标记,将查询函数和修改函数分离,令函数携带参数,已明确函数取代参数。
关于这9种的降低函数复杂度的方法,你可以阅读其他资料获取详细描述,这儿仅简单介绍2种。首先介绍的提炼函数方法,假设你有这么一段代码可被组织并独立出来。这段代码以一个函数呈现,给人感觉比了凌乱。且圈复杂度也较高。
void PrintOwing(double dPreviousAmount)
{
Enumeration e = Orders.elements();
double outstanding = dPreviousAmount * 1.2;
// 打印信息
printf("**************************");
printf("********Customer Owes*****");
printf("**************************");
while (e.hsMoreElents())
{
Order each = (Order)e.nextElement();
outstanding += each.getAmount();
}
// 打印详情
printf("name:%s", szName);
printf("amount:%d",outstanding);
}
我们使用提炼函数降低函数圈复杂度。将这段代码放进一个独立函数中,并让函数名称解释函数的用途。具体代码实现如下述:
void PrintOwing(double dPreviousAmount)
{
PrintBanner();
double outstanding = dPreviousAmount * 1.2;
outstanding = GetOutstanding(outstanding);
PrintDetails(outstanding);
}
void PrintfBanner()
{
// 打印信息
printf("**************************");
printf("********Customer Owes*****");
printf("**************************");
}
double GetOutstanding(double initialValue)
{
double result = initialValue;
Enumeration e = Orders.elements();
while (e.hsMoreElents())
{
Order each = (Order)e.nextElement();
result += each.getAmount();
}
return result;
}
void PrintDetails(double outstanding)
{
// 打印详情
printf("name:%s", szName);
printf("amount:%d",outstanding);
}
对比两者实现,我想与第一种相比,第二种会给你一种清晰简单的感觉。第一种会给你鱼龙混杂的感觉。
接着在介绍一种以明确函数取得参数降低圈复杂度的方法。我们看下面这段代码,此函数实现完全取决于参数值而采取不同的反应:
// 设置键值
void SetValue(string strName, int iValue)
{
if (strName == "height")
{
m_iHeight = iValue;
}
else
{
m_iWidth = iValue;
}
}
如果采用针对该参数每个可能值,建立一个独立函数方式。我们看下面的实现,这种实现明显比上述实现看起来舒服多了:
void SetHeight(int arg)
{
m_iHeight = arg;
}
void SetWidth(int arg)
{
m_iWidth = arg;
}
最后,在介绍一种依靠C++语言多态取代条件式,降低函数圈复杂度的实现方式。此方法的精髓在于依靠C++的语言机制降低复杂度。这应该是我们大力提倡的一种实现机制。
// 获得鸟的运行速度。
double GetSpeed()
{
switch(nType)
{
case EUROPEAN: // 欧洲速度,计算方法
return GetBaseSpeed();
break;
case AFRICAN: // 非洲速度,计算方法
return GetBaseSpeed() - GetLoadFactor();
break;
case NORWEGIAN_BLUE:
return GetBaseSpeed()*0.7;
break;
}
}
上面这段代码是通过条件判断实现,根据不同的类型选择不同的行为,最终实现鸟的运动速度。如果将整个条件式的每个分支放进一个子类的重载方法中,然后将原始函数声明为抽象方法。我们看下述的多态继承关系类图6-5描述。这种机制将if判断转化为子类的函数重载,简化了代码的实现,同时也降低了函数的复杂度。图6-5展示了通过多态方式实现鸟的运动速度。
请谨记
- 降低函数的圈复杂度是提供编码质量的一个重要手段,掌握降低函数圈复杂度的9种实现方式。
- 在降低函数圈复杂度的所有方法中,我们推荐依靠类函数重载多态机制降低函数圈复杂度的方法。