C语言switch/case圈复杂度优化重构
软件重构是改善代码可读性、可扩展性、可维护性等目的的常见技术手段。圈复杂度作为一项软件质量度量指标,能从一定程度上反映这些内部质量需求(当然并不是全部),所以圈复杂度往往被很多项目采用作为软件质量的度量指标之一。
C语言开发的项目中,switch/case代码块是一个很容易造成圈复杂度超标的语言特性,所以本文主要介绍下降低switch/case圈复杂度的重构方法(如下图)。switch圈复杂度优化重构可分为两部分:程序块的重构和case的重构。程序块重构是对代码的局部优化,而case重构是对代码的整体设计,所涉及的重构手段也各不相同。
程序块重构
程序块重构指的是每个case内的代码段重构。Martin Fowler 的《重构——改善既有代码的设计》(电子版)书中总结了80多种重构方法。书中针对每种技术都给出了示例说明,另外这里、这里还提供了其他语言的示例和进一步介绍。因为存在大量示例,所以本文针对这些方法不再给出示例,有兴趣的同学可以通过上面几种途径了解学习。不过这些技术中有些是改善代码的可读性,有些是改善代码的可扩展性,并不是每项技术都能有效减低圈复杂度。其中可以降低圈复杂度的方法有如下几种:
- 提炼函数(Extract Method)。你有一段代码可以被组织在一起并独立出来。将这段代码放进一个独立函数中,并将函数名称解释该函数的用途。
- 分解条件表达式(Decompose Conditional)。你有一个复杂的条件(if-then-else)语句。从if、then、else三分段落中分别提炼出独立函数。
- 合并条件表达式(Consolidate Conditional Expression)。你有一系列条件测试,都得到相同结果。将这些测试合并为一个条件表达式,并将这个条件表达式提炼成为一个独立函数。
- 合并重复的条件片段(Consolidate Duplicate Conditional Fragments)。在条件表达式的每个分支上有着相同的一段代码。将这段重复的代码搬移到条件表达式之外。
- 移除控制标记(Remove Control Flag)。在一系列布尔表达式中,某个变量带有“控制标记”的作用。以break语句或return语句取代控制标记。
这些重构方法除了降低圈复杂度外,还有如下好处:
- 满足单一职责设计原则,提高代码可读性。
- 去除重复冗余代码。你可以删除大量相同的条件语句。
- 满足“Tell, Dont Ask”原则,告诉对象需要做什么,而不是怎么做。
case重构
对于一个switch有几十个case的情况,其圈复杂度往往上百,程序块重构显然已不能解决其本质复杂度。如果要降低其圈复杂度,必然需要对代码进行重新设计。
C语言的switch/case语言特性本质是描述一种查表逻辑,其中表结构和表的控制(即查表)都通过软件来表达。表通过代码来描述,这显然不是一种最佳的实现方式。我们需要做的就是,避免控制中的复杂性,将精力集中在数据的组织上,以反映所模拟世界的真实结构,并将数据与控制进行分离。
表的设计由两部分组成:对象(表项)的抽象和表的构建。对象如何抽象,对象粒度如何划分,对象间的关系如何设计?这些问题涉及抽象思维能力的训练,而且也与具体业务逻辑强相关,不是本文重点。读者可阅读《计算机程序的构造和解释》来进一步了解软件抽象等相关技术细节。
表的构建方法是本文的重点,其可分为编译期构建、链接期构建和运行时构建。3种方法各有所长和不足,可根据自身需要进行选择。
编译期表构建
问题背景
boot启动支持3种启动方式,每种启动方式的用户菜单流程也不尽相同。启动菜单支持输入检查、存储、菜单回退等功能。原有设计中函数设计臃肿,菜单项通过switch/case来进行选择处理,有十几个函数圈复杂度超过40,最大的圈复杂度为147,代码维护困难。
重构方法
boot启动用户菜单本质是一个优先状态机,每个菜单项是其中一个状态。抽象菜单项对象T_PROMT,其包含提示打印、输入检查、存储、状态跳转等成员。构建T_PROMT aPromtArray[]菜单表描述所有菜单项对象,通过MenuFsm实现状态机的控制:通过对象T_PROMT的jumpto接口实现状态的跳转,通过check接口实现输入检查,通过setvalue接口实现存储,通过parent实现菜单回退到上级菜单(因为上级菜单是动态变化的,无法静态初始化,所以在jumpto中进行动态赋值)。示例代码如下:
typedef struct prompt
{
WORD32 type;
CHAR *name;/*env name*/
CHAR *prompt;/*prompt info to user*/
WORD32 (*check)(CHAR *src);/*check func for user's input*/
struct prompt* (*jumpto)(struct prompt*, WORD32);
struct prompt *parent;
VOID (*setvalue)(CHAR *name);
}T_PROMT; static T_PROMT aPromtArray[] =
{
/* env name prompt string check func jump func parent set func */
{TYPE_NORMAL, ENV_LOCAL_IP, "Local IP:", CheckIpAddr, LocalIpJump ,NULL, SetCltIpAddr },
{TYPE_NORMAL, ENV_SERVER_IP, "Server IP:", CheckIpAddr, ServeripJump ,NULL, SetSerIpAddr },
/* 共 22 个表项,以下略 */
}; static SWORD32 MenuFsm(struct prompt *menu)
{
SWORD32 dwRet = BSP_OK;
WORD32 dwIndex; while(menu != NULL) {
if (menu == GetPrompt(ENV_NULL)) {
dwRet = MODE_MENU_BACK;
break;
} dwIndex= PrintPromptAndGetUserInput(menu);
if (dwIndex != NORMAL_MENU_BACK ) {
menu = menu->jumpto(menu, dwIndex);
} else {
menu = menu->parent;
}
} return dwRet;
} static struct prompt* GetPrompt(char *name)
{
WORD32 i = 0;
struct prompt *pt = NULL; WORD32 dwSize = sizeof(aPromtArray)/sizeof(aPromtArray[0]); for (i = 0; i < dwSize; i++) {
if (strcmp(name, aPromtArray[i].name) == 0) {
pt = &aPromtArray[i];
break;
}
} return pt;
}
运行时表构建
问题背景
内核模块通过ioctl对外部提供接口,而此模块ioctl控制码有84个,原ioctl函数通过switch/case完成ioctl的分发和处理,此实现方案导致函数代码长度达767行,圈复杂度达124,难以维护,不满足项目软件质量要求(函数圈复杂度在12以下)。
重构方法
抽象ioctl接口对象ctrl_operations并实例化;通过bsp_iocmds_init构建字典(哈希表),实现ioctl控制码到ioctl接口的映射;在board_dev_init模块初始化中完成哈希表的初始化;在boardctrl_do_ioctl中通过哈希查表接口bsp_dict_get获取ioctl控制码的处理接口。
示例代码
struct ctrl_operations {
SWORD32 (*board_init)(struct board *bd);
SWORD32 (*board_exit)(struct board *bd);
/* 共 92 个表项,以下略 */
}; struct ctrl_operations ioctl_ops = {
.inherits = &extern_ops,
.epld_op = bsp_epld_op,
.epldrw = bsp_epld_rw,
/* 共 84 个字段,以下略 */
}; void bsp_iocmds_init(struct board *bd, pt_bsp_dict pdict)
{
bsp_dict_add(pdict, BSP_IOCMD_ROV_WR, bd->ops->rov_wr);
bsp_dict_add(pdict, BSP_IOCMD_TCAM_INFO, bd->ops->tcam_info);
/* 共 84 个key,以下略 */ } static SWORD32 __init board_dev_init(void)
{
struct board *bd = get_board();
/* 删除无关代码 */
bd->iocmds = bsp_dict_new(DICT_HINT, bsp_cmp, bsp_hash);
bsp_iocmds_init(bd, bd->iocmds); return BSP_OK;
} WORD32 boardctrl_do_ioctl(unsigned int cmd, void *pParam)
{
WORD32 dwIoNum = _IOC_NR(cmd);
struct board *bd = get_board();
WORD32 dwRet = BSP_E_BRDCTRL_NOTSUPPORT;
PT_OPS_FUNC ops; ops = bsp_dict_get(bd->iocmds, dwIoNum); if(likely(ops)) {
dwRet = ops(bd, pParam);
} return dwRet; }
当然除了使用哈希表,也可以使用链表等数据结构来组织数据。
链接期表构建
问题背景
编译期表构建和运行时表构建2种方法,能优化设计,降低圈复杂度,但有一件事情没有做完美:新增一个表项时,必须修改公共的静态表(编译期表构建,如需要修改aPromtArray)或注册函数(运行时表构建,如需要修改bsp_iocmds_init),无法做到完全满足“开发封闭原则”。
链接期表构建方法则可以解决这个问题。
重构方法
通过gcc的section属性,把所有(ioctl控制码,接口)数据对(即元组)定义在同一个section数据段中。在链接阶段,链接器会构建初始化此section数据段,话句话说,连接器帮助我们完成了这个对象数组的初始化和构建。然后利用gcc导出的__start_ctrl_op_section和__stop_ctrl_op_section符号,boardctrl_do_ioctl即可完成对section数据表的查表操作。
此项技术在u-boot、Linux kernel中大量使用。当添加一个新表项时,只需要添加一句ctrl_op_init,不需要修改任何公共代码或数据。
示例代码:
typedef void (*ctrl_op)(struct board *bd); #define _init __attribute__((section("ctrl_op_section")))
#define ctrl_op_init(num, func) ctrl_op __no_##func _init = (ctrl_op)num; \
ctrl_op __fn_##func _init = func extern ctrl_op __start_ctrl_op_section;
extern ctrl_op __stop_ctrl_op_section; ctrl_op_init(BSP_IOCMD_ROV_WR, bsp_rov_wr);
ctrl_op_init(BSP_IOCMD_TCAM_INFO, bsp_tcam_info);
/* 共 84 个ctrl_op_init,以下略 */ WORD32 boardctrl_do_ioctl(unsigned int cmd, void *pParam)
{
WORD32 dwIoNum = _IOC_NR(cmd);
struct board *bd = get_board();
WORD32 dwRet = BSP_E_BRDCTRL_NOTSUPPORT;
ctrl_op * ptr = &__start_ctrl_op_section; do {
if((WORD32)*ptr == dwIoNum) {
ptr++;
if(likely(ptr))
return (*ptr)(bd, pParam);
}
ptr += 2;
} while (ptr < &__stop_ctrl_op_section); return dwRet; }
C语言switch/case圈复杂度优化重构的更多相关文章
- python中Switch/Case实现
学习Python过程中,发现没有switch-case,过去写C习惯用Switch/Case语句,官方文档说通过if-elif实现.所以不妨自己来实现Switch/Case功能. 方法一 通过字典实现 ...
- c语言基础表达式, 关系运算符, 逻辑运算符, 位运算符, 数据的取值范围, 分支结构(if...else, switch...case)
1.表达式: 表达式的判断是有无结果(值), 最简单的表达式是一个常量或变量, 如:12, a, 3 + 1, a + b, a + 5 都是表达式 2.BOOL(布尔)数据类型: c语言中除了基本数 ...
- go语言选择语句 switch case
根据传入条件的不同,选择语句会执行不同的语句.下面的例子根据传入的整型变量i的不同而打印不同的内容: switch i { case 0: fmt.Printf("0") case ...
- 李洪强漫谈iOS开发[C语言-040]-switch case
李洪强漫谈iOS开发[C语言-039]-switch case 补充:
- J S 脚本语言 if() { if { } else { } } var a =100; switch { case ( ) break ; } 基础详解 , 最下面有例子
注释语法 注释语法// 多行注释/ JS输出语句 JS样式尽量靠最下面写 <script type="text/javascript">//嵌入JS开始代码 //ale ...
- 如何优化代码中大量的if/else,switch/case?
前言 随着项目的迭代,代码中存在的分支判断可能会越来越多,当里面涉及到的逻辑比较复杂或者分支数量实在是多的难以维护的时候,我们就要考虑下,有办法能让这些代码变得更优雅吗? 正文 使用枚举 这里我们简单 ...
- 选择语言之switch case
程序语言-选择语言之switch case 多选一,类似if else if else if else 模版: Switch(选择条件) { Case(条件一)//相当于if Conso ...
- Go语言 - 流程控制 if else | for | switch case
流程控制 流程控制是每种编程语言控制逻辑走向和执行次序的重要部分,流程控制可以说是一门语言的“经脉”. Go语言中最常用的流程控制: if | for | switch | goto// switch ...
- c语言学习笔记 多级else if 和switch case有什么区别
; ) { dosth(); } ) { dosth2(); } else if(opion==) { dosth3(); } else dosth4(); 如果给option的一个值是2的话,那么程 ...
随机推荐
- UESTC 251 最长上升子序列O(nlgn)
O(n^2)过不了.必须要用一个额外的数组保存当前长度最小值,然后lgn查表 AC代码 #include<cstdio> #include<algorithm> using n ...
- Vue中method与computed的区别
为了说明method与computed的区别,在此我想先来看看computed属性在vue官网中的说法:模板内的表达式是非常便利的,但是它们实际上只用于简单的运算.在模板中放入太多的逻辑会让模板过重且 ...
- 借鉴mini2440的usb-wifi工具集在Beagleboard上移植无线网卡
配置minicom: sudo yum install minicom sudo minicom -s 选择Serial port setup,此时所示光标在"Change which se ...
- 如何更改Ubuntu的root密码
安装Ubuntu系统时,只提示了设定用户密码,该密码可用于普通用户暂时获取root的权限,执行一些需要root权限的操作,而没有要求我们设置root密码,在需要用到root密码时,却想不起来,很尴尬啊 ...
- GetWindowRect、GetClientRect、ScreenToClient与ClientToScreen
GetWindowRect是取得窗口在屏幕坐标系下的RECT坐标(包括客户区和非客户区),这样可以得到窗口的大小和相对屏幕左上角(0,0)的位置. GetClientRect取得窗口客户区(不包括非客 ...
- Android可以拖动位置的ListVeiw
参考网址: 1.https://github.com/bauerca/drag-sort-listview 2.http://www.tuicool.com/articles/jyA3MrU
- Solution for link error:Cannot Open File 'python27_d.lib'
引自:http://guangboo.org/2013/01/17/solution-link-errorcannot-open-file-python27_dlib 感谢原作者 使用C调用Pytho ...
- python学习之字典(Dictionary)练习
Python字典是另一种可变容器模型,且可存储任意类型对象,如字符串.数字.元组等其他容器模型 字典中分为键值对 , key 类型需要时被哈希. value 类型可以是 字符串.数字.元组等其他容器模 ...
- Linux中挂载Windows共享出来的目录
Windows中1.新建文件夹,创建共享目录 右键文件夹,共享 2.设置访问方式 Everyone,添加确认 Linux-ubuntu中1.安装cifs#sudo apt-get install ci ...
- Rational Rose_2007的下载、安装与破解--UML建模软件
一.下载Rational.Rose_2007安装包与破解文件 对于Rational.Rose_2007,您可以到我的百度网盘计算机相关专业所用软件---百度云链接下载下载,另外附上安装需要的通行证(破 ...