概述
定义
圈复杂度 (Cyclomatic complexity) 是一种代码复杂度的衡量标准,也称为条件复杂度或循环复杂度,它可以用来衡量一个模块判定结构的复杂程度,数量上表现为独立现行路径条数,也可理解为覆盖所有的可能情况最少使用的测试用例数。简称 CC 。其符号为 VG 或是 M 。
圈复杂度 在 1976 年由 Thomas J. McCabe, Sr. 提出。
圈复杂度大说明程序代码的判断逻辑复杂,可能质量低且难于测试和维护。程序的可能错误和高的圈复杂度有着很大关系。
看一个案例
public String parseValue(String val){ String res = null; if(val != null){ res == "hello"; } return res; } ....某处调用逻辑 parseValue("你好").length();
此方法在测试时,只用一个case就能实现代码覆盖率100%,但是还是会有空指针异常的情况。
因为方法的圈复杂度是2,所以至少有两个独立的case才能完全覆盖,所以圈复杂度简单理解就是需要多少个独立的case才能覆盖所有可能路径。
计算方法
有一个简单的计算方法,圈复杂度实际上就是等于判定节点的数量再加上1。向上面提到的:if else
、switch case
、 for
循环、三元运算符,&&,|| 等等,都属于一个判定节点,例如下面的代码:
function testComplexity(*param*) {
let result = 1;
if (param > 0) {
result--;
}
for (let i = 0; i < 10; i++) {
result += Math.random();
}
switch (parseInt(result)) {
case 1:
result += 20;
break;
case 2:
result += 30;
break;
default:
result += 10;
break;
}
return result > 20 ? result : result;
}
上面的代码中一共有1
个if
语句,一个for
循环,两个case
语句,一个三元运算符,所以代码复杂度为 4+1+1=6
。
质量标准
代码复杂度低,代码不一定好,但代码复杂度高,代码一定不好。
圈复杂度 | 代码状况 | 可测性 | 维护成本 |
1-10 | 清晰,结构化 | 高 | 低 |
10~20 | 复杂 | 中 | 中 |
20~30 | 非常复杂 | 低 | 高 |
>30 | 不可读 | 不可测 | 非常高 |
所以我们的目标是把代码圈复杂度控制在10以内。
度量插件
IDEA可以安装MetricsReloaded插件
使用方法:
选中一个模块,点击analyze -> calculate metrics。
重点关注Method metrics结果。
在分析结果中可以看到ev, iv, v这几栏,分别代指基本复杂度(Essential Complexity (ev(G))、模块设计复杂度(Module Design Complexity (iv(G)))、Cyclomatic Complexity (v(G))圈复杂度。
ev(G)基本复杂度是用来衡量程序非结构化程度的,非结构成分降低了程序的质量,增加了代码的维护难度,使程序难于理解。因此,基本复杂度高意味着非结构化程度高,难以模块化和维护。实际上,消除了一个错误有时会引起其他的错误。
Iv(G)模块设计复杂度是用来衡量模块判定结构,即模块和其他模块的调用关系。软件模块设计复杂度高意味模块耦合度高,这将导致模块难于隔离、维护和复用。模块设计复杂度是从模块流程图中移去那些不包含调用子模块的判定和循环结构后得出的圈复杂度,因此模块设计复杂度不能大于圈复杂度,通常是远小于圈复杂度。
v(G)是用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数,即合理的预防错误所需测试的最少路径条数,圈复杂度大说明程序代码可能质量低且难于测试和维护,经验表明,程序的可能错误和高的圈复杂度有着很大关系。
改进方案
改进方案的目的是为了提高代码质量,而不是单单为了降低圈复杂度,一切以提高代码质量为原则。
技巧一、提炼函数
把子程序的一部分提取成另一个子程序,不会降低整个程序的复杂度,只是把决策点移到其他地方,但是这样做可以降低你在同一时间必须关注的复杂度水平。由于重点是要降低你需要在头脑中同时考虑的项目的数量,所以降低一个给定程序的复杂度是有价值的。
public void test(Integer number){
if(number < Integer.MIN_VALUE)
{
number = Integer.MIN_VALUE;
}
for(int i = 0; i < number; i++){
//some code
}
}
//替换成下面
public void
test(Integer number){
number = getMin(number);
for(int i = 0; i < number; i++){
//some code
}
}
function getMin(number){
if(number < Integer.MIN_VALUE){
return Integer.MIN_VALUE;
}
return number
}
技巧二、替换算法
if(str == 'China'){
result = '中国人';
}
else if(str == 'US'){
result = '美国人';
}
else if(str == 'France'){
result = '法国人';
}
//替换成下面
people = [
'China' => '中国人',
'US' => '美国人',
'France' => '法国人'
];
result = people[str];
技巧三、Optional
public void test(User user){
if(user != null){
String userName = user.getUserName();
Integer age = user.getAge();
if(userName != null){
userName = userName.toUpperCase();
user.setUserName(userName);
}
if(age != null){
age += 10;
user.setAge(age);
}
....
}
}
//变成下面
public void test(User user){
Optional<User> optional = Optional.ofNullable(user);
optional.map(u -> {
u.setUserName(u.getUserName().toUpperCase());
u.setAge(u.getAge() + 10);
});
}
技巧四、逆向表达
if((条件1 && 条件2) || !条件1){
return true;
}
else{
return false;
}
//变成
if(条件1 && !条件2){
return false;
}
return true;
技巧五、合并条件(将这些判断合并为一个条件式,并提取成独立函数)
if($x < 1) return 0;
if($y > 10) return 0;
if($z != 0) return 0;
//to
if(get_result($x,$y,$z)) return 0;
技巧六、移除控制标识(可以使用break和return取代控制标记。)
boolean flag = false;
for(arr : arrs){
if(!flag){
if(arr == 1){
someFunction();
bool = true;
}
if(arr == 2){
someFunction();
bool = true;
}
}
}
//to
for(arr : arrs){
if(arr == 1 || arr == 2){
someFunction();
}
break;
}
技巧七、以多态取代条件式(将整个条件式的每个分支放进一个子类的重载方法中,然后将原始函数声明为抽象方法。
switch (cat){
case ‘fish’:
eatFish();
case ‘moss’:
eatMoss();
}
public void eatFish() {
sys.out "Whale eats fish";
}
public void eatMoss() {
sys.out "Whale eat moss";
}
//to
interface Eat {
public eatFish();
public eatMoss();
}
class Whale implements Eat {
public void eatFish() {
sys.out "Whale eats fish";
}
public void eatMoss() {
sys.out "Whale eat moss";
}
}
最后
以上就是俊逸烧鹅为你收集整理的代码圈复杂度的全部内容,希望文章能够帮你解决代码圈复杂度所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复