C語言switch/case循環複雜度最佳化重構

來源:互聯網
上載者:User

標籤:最大   etc   linu   資料   函數   board   new   sub   att   

軟體重構是改善代碼可讀性、可擴充性、可維護性等目的的常見技術手段。循環複雜度作為一項軟體品質度量指標,能從一定程度上反映這些內部品質需求(當然並不是全部),所以循環複雜度往往被很多項目採用作為軟體品質的度量指標之一。

C語言開發的項目中,switch/case代碼塊是一個很容易造成循環複雜度超標的語言特性,所以本文主要介紹下降低switch程式碼片段的重構手段(如)。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, Don’t 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 = funcextern 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循環複雜度最佳化重構

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.