C語言switch/case圈複雜度優化重構

軟件重構是改善代碼可讀性、可擴展性、可維護性等目的的常見技術手段。圈複雜度做爲一項軟件質量度量指標,能從必定程度上反映這些內部質量需求(固然並非所有),因此圈複雜度每每被不少項目採用做爲軟件質量的度量指標之一。html

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種方法各有所長和不足,可根據自身須要進行選擇。.net

編譯期表構建

問題背景

boot啓動支持3種啓動方式,每種啓動方式的用戶菜單流程也不盡相同。啓動菜單支持輸入檢查、存儲、菜單回退等功能。原有設計中函數設計臃腫,菜單項經過switch/case來進行選擇處理,有十幾個函數圈複雜度超過40,最大的圈複雜度爲147,代碼維護困難。設計

重構方法

boot啓動用戶菜單本質是一個優先狀態機,每一個菜單項是其中一個狀態。抽象菜單項對象T_PROMT,其包含提示打印、輸入檢查、存儲、狀態跳轉等成員。構建T_PROMT aPromtArray[]菜單表描述全部菜單項對象,經過MenuFsm實現狀態機的控制:經過對象T_PROMT的jumpto接口實現狀態的跳轉,經過check接口實現輸入檢查,經過setvalue接口實現存儲,經過parent實現菜單回退到上級菜單(由於上級菜單是動態變化的,沒法靜態初始化,因此在jumpto中進行動態賦值)。示例代碼以下:htm

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;

}
相關文章
相關標籤/搜索