邵國際: C 語言對象化設計實例 —— 命令解析器

本文系轉載,著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。linux

做者: 邵國際程序員

來源: 微信公衆號linux閱碼場(id: linuxdev)編程

file


內容簡介segmentfault

單片機工程師經常疑惑爲何 Linux 驅動框架要搞那麼複雜的一套,卻不知這種「複雜」纔是面向對象設計的精髓。對代碼的高度抽象和封裝可大大提升軟件的複用性、可維護性。本文從一個簡單例子 —— 51 單片機上的串口命令解析器程序出發,對比過程式與對象式思惟差別,分享本身對 OO 的一點淺薄見解。數組

做者介紹微信

邵國際,計算機專業學生,擅長動手,熱衷物聯網。用技術表達自我,雖然是個玩過單片機的渣渣,但一直想作出好玩有趣的東西(軟/硬件),並享受其中的樂趣。目前在深圳增加見識、學習嵌入式開發技術中。數據結構

file


前言

傳統單片機 MCU 編程大多使用過程式的思惟來組織程序,在單片機資源少、功能簡單、代碼規模小的狀況下,「想到啥寫啥」的方法也確實能解決大部分問題。但隨着硬件的快速升級,現在的大部分嵌入式工程師已經再也不須要「掐着內存」來寫代碼了。當軟件的規模愈加龐大、複雜,這時如何編寫可複用、便於維護的代碼顯得尤其重要。本文經過一個在 51 單片上實現的簡單「串口命令解析器」例子,分析如何經過面向對象思想編寫出「高內聚低耦合」的 C 語言程序。框架

本文是學習宋寶華老師的《C語言大型軟件設計的面向對象》課程(地址:http://edu.csdn.net/course/de...) 後的一些收穫。編程語言

相關閱讀:《C語言的面向對象(面向較大型軟件)》ppt分享和ppt註解
https://mp.weixin.qq.com/s?__...模塊化

C 語言也能面向對象?

在許多年輕人眼裏,C 是一門既「老土」又「古板」的編程語言,更可怕的是,「C 老頭」常年被人貼上「面向過程」的標籤,與 Java、Pyhon 等面向對象的高級語言格格不入。

事實上,面向對象只是一種思想,與語言無關(只不過C++、Java 在語法形式上自然支持 OO),靈活的 C 語言固然也能實現面向對象的編程 —— 這些觀點我之前也都聽過,但僅僅停留在字面意思的感覺。直到看了宋老師的直播中的幾個實例,我才加深了對 C 語言面向對象的理解,更進一步體會到 OO 思想的強大。其中課程裏提到的「命令解析器」即是典型例子,下面和你們分享一下其中的思想精髓與具體實現,體會傳統過程式思惟與 OO 思惟的差別。

PS:因爲筆者真是個菜雞,我的理解不免會有誤差,更多隻是拾人牙慧,歡迎指正。

命令解析器

file

經過命令操控計算機是一件很酷的事情,在 DOS、Linux 系統中也普遍使用命令行的方式。命令操做的核心即是命令解析器(如 Linux 中的 Shell)。命令解析器實現接收命令字符串,解析命令並執行相應操做,在單片機程序中也經常經過串口命令爲用戶提供操做接口(如 AT 指令)。

過程式設計

簡單來講,命令解析器的核心功能其實就是字符串比較,調用相應函數,使用 C 語言的選擇結構即可輕鬆實現,你甚至能直接想到對應代碼,因而你寫出了像這樣的程序:

file

你很是機智地採用模塊化編程,每一個子功能都用單獨的 .c 文件存放。在 cmd.c 中進行命令的處理,經過條件語句比較命令,匹配後調用 gpio.c、spi.c、i2.c 文件中對應的操做函數,代碼一鼓作氣。個人第一反應也是這樣寫,嗯,沒毛病。

這是典型的過程式思惟 —— 先幹什麼後幹什麼,把全部零零散散的操做經過一根時間軸串起來,沒有絲毫拐彎抹角,很是直接。但這樣的過程式設計存在明顯的兩個問題:

  1. 命令增長引發跨模塊修改
  2. 大量的外部函數,模塊間高耦合

下面來具體解釋一下遇到的這兩個問題。

1. 命令增長引發跨模塊修改

假設如今需求變化,要求增長 GPIO翻轉 命令產生對應的電平變化。你趕忙在 gpio.c 文件中須要增長一個電平翻轉操做函數 gpio_toggle(),同時在 cmd.c 的 switch-case 語句內部添加新增的命令及函數……

等等,這不是很怪麼?只是增長了 GPIO 相關功能,命令處理邏輯沒變(依然只是判斷字符串相等),爲何卻要改動 cmd.c 的命令處理邏輯?並且仍是沒啥技術含量地加了一條 case 語句……

改兩個文件或許咬咬牙就算了,若是工程日益增大,致使每增長一條命令都要像「砌牆」或者「擰螺絲」同樣作一堆機械重複的工做,這樣的代碼一點都不酷。

2. 大量的外部函數,模塊間高耦合

若是說跨模塊修改只是一個「麻煩點兒」的問題,勤快的人絕不在意(好吧大家贏了),那模塊間高耦合則直接影響了代碼的複用性 —— 代碼不通用!這就不是小問題了。高複用性可謂碼農的一大追求,誰不想只寫一次代碼就能夠拼湊成各類大項目,輕輕鬆鬆躺着賺錢呢?

某年後,你遇到了一個新系統,其中也須要命令解析器功能模塊,因而你興沖沖把以前寫的 cmd.c和 cmd.h 直接拿過來用,卻發現編譯報錯找不到 gpio_high()、gpio_low()、spi_send()……你的心裏是崩潰的。

因爲 gpio_high()、gpio_low() 等函數都是 gpio.c 中的外部函數,在 cmd.c 中直接經過函數名調用,兩個文件像纏綿的情侶般高度耦合,這種緊密的聯繫破壞了C 程序設計的一個基本原則 —— 模塊的獨立性。採用了模塊化編程,然而每一個模塊卻不能獨立使用,意義何在?

面向對象設計

在前面發現的兩個問題上對症下藥,能夠獲得程序的改進目標:

  1. 增長或減小命令不影響 cmd.c
  2. 命令的處理函數要成爲 static,去耦合

OO思想

在解決這兩個問題前,讓咱們回到思惟層面,對比「面向對象」與「面向過程」思想的區別。當咱們談論面向過程思惟時,程序員的角色像一個統治者,掌管一切、什麼都要插一手。

舉個典型例子,要把大象裝到冰箱須要三步:

  1. 打開冰箱門
  2. 將大象放進冰箱
  3. 關閉冰箱門

這一系列步驟的主動權都緊緊掌握在操做者手裏,操做者循序漸進地把具體操做與時間軸綁定起來,是典型的過程思惟。再回到前面匹配命令的 switch-case 語句上,每增長一條新命令都須要程序員手把手地把命令和函數寫死在程序中。因而咱們就會想,能不能讓命令解析器做爲一個主動的個體本身增長命令?

這裏就引入了「對象」的概念,什麼是對象?咱們所關注的一切事物皆爲對象。在「把大象裝到冰箱」問題中,把「大象」、「冰箱」這兩個名詞提取出來,就是兩個對象。過程式思惟解決問題時考慮「須要哪些步驟」,而 OO 思想考慮「須要哪些對象」。

仍是這個例子,要把大象裝到冰箱只須要兩個對象:

  1. 冰箱
  2. 大象

如何描述一個對象呢?能夠經過兩個方面,一是對象的特徵(屬性),二是對象的行爲(方法/函數)。由此能夠列舉出描述大象和冰箱的一些屬性和方法:

• 大象的屬性(特徵):品種、體形、鼻長……

• 大象的方法(行爲):進食、走路、睡覺……

• 冰箱的屬性(特徵):價格、容量、功耗……

• 冰箱的方法(行爲):開關機、開關門、除霜去冰……

對象有如此多的屬性和方法,但實際上並不都能用得上。不一樣問題涉及到對象的不一樣方面,所以能夠忽略無關的屬性、方法。對於「把大象裝到冰箱」這個問題,咱們只關心「大象的體形」、「冰箱的容量」、「大象走路(說不定能讓大象本身走進冰箱)」、「冰箱開關門」等這些與問題相關的屬性和方法。

因而程序就成了「冰箱開門、大象走進冰箱並告訴冰箱關門」的模式,將操做的主動權歸還對象自己時,程序員再也不是霸道的統治者,而是扮演管理員的角色,協調各對象基於自身的屬性和方法完成所需功能。

OO 版命令解析器

迴歸正題,如何才能解決前面的兩個問題、讓命令解析器更「OO」呢?首先對最終功能 ——「命令解析器解析命令」這句話深度挖掘,注意到「命令」、「命令解析器」這兩個名詞能夠抽象成對象。

命令類型的封裝

首先是「命令」自己能夠封裝爲包含「命令名」和「對應操做」兩個成員的結構體,前者是屬性,可用字符數組存儲,後者在邏輯上是行爲/函數,但因爲 C 語言結構體不支持函數,可用函數指針存儲。這至關於把「命令」定義成了新的數據類型,將命令與操做聯繫起來。

// 文件名稱: cmd.h
 
#define     MAX_CMD_NAME_LENGTH     20    // 最大命令名長度,過大 51 內存會炸
#define     MAX_CMDS_COUNT          10    // 最大命令數,過大 51 內存會炸
 
typedef void (*handler)(void);        // 命令操做函數指針類型
 
/* 命令結構體類型 */
typedef struct cmd
{
    char cmd_name[MAX_CMD_NAME_LENGTH + 1];   // 命令名 
    handler cmd_operate;                      // 命令操做函數
} CMD;

其中宏 MAX_CMD_NAME_LENGTH 表示所存儲命令名的最大長度,handler 爲指向命令操做函數的指針,全部命令操做函數均爲無參無返回值。

命令解析器的封裝

同理,「命令解析器」這一模塊也能夠看作一個對象,對功能模塊的封裝已經在文件結構上體現,就不必用結構體了,咱們重點關注對象的內部(即成員變量與成員函數)。

成員變量

命令解析器要從一堆命令中匹配一個,所以須要一種能存儲命令集合的數據結構,這裏使用數組實現線性表:

// 文件名稱: cmd.h
 
/* 命令列表結構體類型 */
typedef struct cmds
{
    CMD cmds[MAX_CMDS_COUNT];  // 列表內容
    int num;                   // 列表長度
} CMDS;

經過結構體封裝數據類型定義成員變量類型,方便在 cmd.c 中使用:

// 文件名稱: cmd.c
 
static xdata CMDS commands = {NULL, 0};  // 全局命令列表,保存已註冊命令集合

爲了簡化程序,線性表的「增刪改查」等基本操做就不一一獨立實現了,而是與命令處理過程結合(命令的註冊與匹配其實就是插入與查找過程)。下面考慮對象的成員函數。

成員函數

命令解析器涉及到那些行爲呢?首要任務固然是匹配並執行指令。其次,要對外提供增長命令的接口函數,由處理命令功能模塊主動註冊命令,而不是經過代碼寫死,從而就避免了跨模塊修改,硬件無關的代碼也提升了程序的可移植性。

編寫 match_cmd() 函數實現命令匹配,該函數接收一個待匹配的命令字符串做爲參數,對命令列表進行遍歷比較操做:

// 文件名稱: cmd.c
 
void match_cmd(char *str)
{
    int i;
 
    if (strlen(str) > MAX_CMD_NAME_LENGTH)
    {
        return;
    }
 
    for (i = 0; i < commands.num; i++)  // 遍歷命令列表
    {
        if (strcmp(commands.cmds[i].cmd_name, str) == 0)
        {
            commands.cmds[i].cmd_operate();
        }
    }
}

接着再實現註冊命令函數,該函數接收一個命令類型數組,插入到命令解析器的命令列表中:

// 文件名稱: cmd.c
 
void register_cmds(CMD reg_cmds[], int length)
{
    int i;
 
    if (length > MAX_CMDS_COUNT)
    {
        return;
    }
 
    for (i = 0; i < length; i++)
    {
        if (commands.num < MAX_CMDS_COUNT)  // 命令列表未滿
        {
            strcpy(commands.cmds[commands.num].cmd_name, reg_cmds[i].cmd_name);
            commands.cmds[commands.num].cmd_operate = reg_cmds[i].cmd_operate;
            commands.num++;
        }  
    }  
}

至此,命令解析器便大功告成!經過調用兩個函數便可完成命令的添加與匹配功能,接下來編寫 LED 燈和蜂鳴器的操做函數,測試命令解析器功能。

命令解析器的使用

註冊和匹配命令

編寫 led.c 文件,實現 LED 的亮滅操做函數,在 led_init() 函數中註冊命令並初始化硬件:

// 文件名稱: led.c
 
static void led_on(void)
{
    LED1 = 0;
}
 
static void led_off(void)
{
    LED1 = 1;
}
 
void led_init(void)
{
    /* 填充命令結構體數組 */
    CMD led_cmds[] = {
        {"led on", led_on},
        {"led off", led_off}
    };
 
    /* 註冊命令 */
    register_cmds(led_cmds, ARRAY_SIZE(led_cmds)); 
 
    /* 初始化硬件 */
    led_off();
}

能夠看到,命令處理函數 led_on() 和 led_off() 都是 static 修飾的內部函數,在其餘模塊中不能經過函數名直接調用,而是經過函數指針的方式傳遞,實現了模塊間解耦。再者,使用結構體數組註冊命令,大大增長程序擴展性。

按照一樣的套路編寫 beep.c 文件實現蜂鳴器控制命令。

最後,在主函數 while(1) 循環中接受串口字符串、解析命令並執行:

// 文件名稱: main.c
 
void main()
{
    unsigned char str[20];
 
    uart_init();
    led_init();
    beep_init();
 
    while (1)
    {  
        /* 獲取串口命令字符串 */
        uart_get_string(str);
 
        /* 匹配命令並執行 */
        match_cmd(str);
 
        /* 命令回顯 */
        uart_send_string(str);
        uart_send_byte('\n');                  
    }
}

增長命令

在通過了高度抽象封裝的命令解析器上增長一條命令,如 LED 翻轉,只須要在 led.c 中增長 led_toggle() 函數,並往待註冊的命令結構體數組初始化列表中添加一個元素,而後……就完了,即便加 100 條新命令也徹底不須要動 cmd.c 中的代碼,兩個模塊彼此獨立。

// 文件名稱: led.c

 

static void led_toggle(void)  // 增長 LED 翻轉函數

{

    LED1 = ~LED1;

}

 

void led_init(void)

{

    /* 填充命令結構體數組 */

    CMD led_cmds[] = {

        {"led on", led_on},

        {"led off", led_off},

        {"led toggle", led_toggle}  // 增長 LED 翻轉命令

    };

 

    /* 註冊命令 */

    register_cmds(led_cmds, ARRAY_SIZE(led_cmds)); 

 

    /* 初始化硬件 */

    led_off();

}

此外,若是 cmd.c 中改用其餘數據結構存儲命令集合,也與 led.c 無關,完全切斷兩個文件的強耦合。cmd.c 現已升級爲一個通用的命令解析器。

實驗效果

file

總結

從最初手動往 cmd.c 中添加命令代碼,到最後經過函數「智能操做」,OO 思想實現把權利下放,每一個模塊本身的事本身解決(功能模塊須要命令功能時本身主動註冊便可),程序員不再用對全部細節親力親爲,而是爲每一個對象賦予該有的能力,而後對它們說上一句:「你辦事我放心」!

工程示例代碼下載:連接:http://pan.baidu.com/s/1geKE2ll 密碼:e0ku

相關文章
相關標籤/搜索