描述:
在项目开发过程中,经常要求圈复杂度不能超过10,有时候写着写着圈复杂度就很大,我在项目代码中见过函数圈复杂度大于100的函数,由于历史的原因,代码越积越多,没人出去重构,导致后面很难懂和维护,所以在编码初期就应该在心中有个要求,就是圈复杂度不能超过10,如果超过10,肯定是代码逻辑写的过于复杂,要回过头来
想想怎么去分解功能,让流程简单易懂。
本文主要通过一些例子来介绍基于Table方式降低圈复杂度的过程。
例子1:一个简单的游戏控制函数
你可能会遇到如下类似的代码:
if(strcmpi(command, "north") == 0) {
if(cur_location->north)
GoToLocation(cur_location->north);
else
Print("Cannot go there");
}
else if(strcmpi(command, "east") == 0) {
if(cur_location->east)
GoToLocation(cur_location->east);
else
Print("Cannot go there");
}
else if(strcmpi(command, "south") == 0) {
if(cur_location->south)
GoToLocation(cur_location->south);
else
Print("Cannot go there");
}
else if(strcmpi(command, "west") == 0) {
if(cur_location->west)
GoToLocation(cur_location->west);
else
Print("Cannot go there");
}
从上面看到该函数的圈复杂度达到了13,包含了很多分支,不容易理解和维护,后续在往里面添加新的特性也容易出错,可以采用如下的方式进行改善。
修改后:
enum SIDE {SIDE_NORTH = 0, SIDE_EAST, SIDE_SOUTH, SIDE_WEST};
struct COMMAND {
const char * name;
SIDE side;
};
static const COMMAND commands[] = {
{"north", SIDE_NORTH},
{"east", SIDE_EAST},
{"south", SIDE_SOUTH},
{"west", SIDE_WEST},
};
for(int i = 0; i < NUM_OF(commands); i++)
if(strcmpi(commands[i].name, command) == 0) {
SIDE d = commands[i].side;
if(cur_location->sides[d])
GoToLocation(cur_location->sides[d]);
else
Print("Cannot go there");
}
上面整改让函数的圈复杂度为5,变得非常清楚和易维护。
例子2:计算租一个CD价钱的函数
double result = 0;
switch(movieType) {
case Movie.REGULAR:
result += 2;
if(daysRented > 2)
result += (daysRented - 2) * 1.5;
break;
case Movie.NEW_RELEASE:
result += daysRented * 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if(daysRented > 3)
result += (daysRented - 3) * 1.5;
break;
}
修改后的版本:
enum MovieType {Regular = 0, NewRelease = 1, Childrens = 2};
// Regular NewRelease Childrens
const double initialCharge[] = {2, 0, 1.5};
const double initialDays[] = {2, 0, 3};
const double multiplier[] = {1.5, 3, 1.5};
double price = initialCharge[movie_type];
if(daysRented > initialDays[movie_type])
price += (daysRented - initialDays[movie_type]) * multiplier[movie_type];
其实也可以采用继承的方式来解决上面的许多switch case分支,用一个类来介绍 regular 价格, 另外一个类 new releases价格, 一个类计算 children's movie价格。
例子3:判断字符和数字
有时需要判断一个字符是否为数字或者大小写,我们经常会使用如下的方法:
int isalnum(int ch) {
return 'a' <= ch && ch <= 'z' ||
'A' <= ch && ch <= 'Z' ||
'0' <= ch && ch <= '9';
}
static const unsigned char properties[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 16, 16, 16, 16, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
16,160,160,160,160,160,160,160,160,160,160,160,160,160,160,160,
204,204,204,204,204,204,204,204,204,204,160,160,160,160,160,160,
160,202,202,202,202,202,202,138,138,138,138,138,138,138,138,138,
138,138,138,138,138,138,138,138,138,138,138,160,160,160,160,160,
160,201,201,201,201,201,201,137,137,137,137,137,137,137,137,137,
137,137,137,137,137,137,137,137,137,137,137,160,160,160,160, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
#define islower(ch) (properties[ch] & 0x01)
#define isupper(ch) (properties[ch] & 0x02)
#define isdigit(ch) (properties[ch] & 0x04)
#define isalnum(ch) (properties[ch] & 0x08)
#define isspace(ch) (properties[ch] & 0x10)
#define ispunct(ch) (properties[ch] & 0x20)
#define isxdigit(ch) (properties[ch] & 0x40)
#define isgraph(ch) (properties[ch] & 0x80)
如果需要存储更少的信息,可以采用位数组,但是需要更多的操作去检索该值,如下:
inline int isalnum(int ch) {
static const unsigned int alnum[] = {
0x0, 0x3ff0000, 0x7fffffe, 0x7fffffe, 0x0, 0x0, 0x0, 0x0,
};
return (alnum[ch >> 5]) & (1 << (ch & 31));
}
从例子3中可以看出,善于利用数组可以有效减少程序的复杂度,有时候还能提高执行效率,不过在开发过程中还是要以可维护性和可理解性为主,除非是在关键路径上需要考虑性能指标。