描述:
在项目开发过程中,经常要求圈复杂度不能超过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'; }
但是在C的运行库中判断是否为数字和 字母采用了如下的方式,如ctype.h中
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中可以看出,善于利用数组可以有效减少程序的复杂度,有时候还能提高执行效率,不过在开发过程中还是要以可维护性和可理解性为主,除非是在关键路径上需要考虑性能指标。
时间: 2024-10-18 01:18:23