C 語(yǔ)言對(duì)象化設(shè)計(jì)實(shí)例 —— 命令解析器
掃描二維碼
隨時(shí)隨地手機(jī)看文章
之前有朋友問(wèn)面向?qū)ο笙嚓P(guān)例子,這篇文章分享的就是面向?qū)ο蟮膶?shí)例,可以學(xué)一學(xué)。文章出自RTT工程師國(guó)際哥,首發(fā)于Linux閱碼場(chǎng)。
前言
傳統(tǒng)單片機(jī) MCU 編程大多使用過(guò)程式的思維來(lái)組織程序,在單片機(jī)資源少、功能簡(jiǎn)單、代碼規(guī)模小的情況下,「想到啥寫(xiě)啥」的方法也確實(shí)能解決大部分問(wèn)題。但隨著硬件的快速升級(jí),如今的大部分嵌入式工程師已經(jīng)不再需要「掐著內(nèi)存」來(lái)寫(xiě)代碼了。當(dāng)軟件的規(guī)模越發(fā)龐大、復(fù)雜,這時(shí)如何編寫(xiě)可復(fù)用、便于維護(hù)的代碼顯得尤為重要。本文通過(guò)一個(gè)在 51 單片上實(shí)現(xiàn)的簡(jiǎn)單「串口命令解析器」例子,分析如何通過(guò)面向?qū)ο笏枷刖帉?xiě)出「高內(nèi)聚低耦合」的 C 語(yǔ)言程序。
本文是學(xué)習(xí)宋寶華老師的《C語(yǔ)言大型軟件設(shè)計(jì)的面向?qū)ο蟆氛n程(地址:http://edu.csdn.net/course/detail/6496)后的一些收獲。
相關(guān)閱讀:《C語(yǔ)言的面向?qū)ο螅嫦蜉^大型軟件)》ppt分享和ppt注解
C 語(yǔ)言也能面向?qū)ο螅?/span>
在許多年輕人眼里,C 是一門(mén)既「老土」又「古板」的編程語(yǔ)言,更可怕的是,「C 老頭」常年被人貼上「面向過(guò)程」的標(biāo)簽,與 Java、Pyhon 等面向?qū)ο蟮母呒?jí)語(yǔ)言格格不入。
事實(shí)上,面向?qū)ο笾皇且环N思想,與語(yǔ)言無(wú)關(guān)(只不過(guò)C++、Java 在語(yǔ)法形式上天然支持 OO),靈活的 C 語(yǔ)言當(dāng)然也能實(shí)現(xiàn)面向?qū)ο蟮木幊?—— 這些觀點(diǎn)我以前也都聽(tīng)過(guò),但僅僅停留在字面意思的感受。直到看了宋老師的直播中的幾個(gè)實(shí)例,我才加深了對(duì) C 語(yǔ)言面向?qū)ο蟮睦斫?,更進(jìn)一步體會(huì)到 OO 思想的強(qiáng)大。其中課程里提到的「命令解析器」便是典型例子,下面和大家分享一下其中的思想精髓與具體實(shí)現(xiàn),體會(huì)傳統(tǒng)過(guò)程式思維與 OO 思維的差異。
PS:由于筆者真是個(gè)菜雞,個(gè)人理解難免會(huì)有偏差,更多只是拾人牙慧,歡迎指正。
命令解析器
通過(guò)命令操控計(jì)算機(jī)是一件很酷的事情,在 DOS、Linux 系統(tǒng)中也廣泛使用命令行的方式。命令操作的核心便是命令解析器(如 Linux 中的 Shell)。命令解析器實(shí)現(xiàn)接收命令字符串,解析命令并執(zhí)行相應(yīng)操作,在單片機(jī)程序中也常常通過(guò)串口命令為用戶(hù)提供操作接口(如 AT 指令)。
過(guò)程式設(shè)計(jì)
簡(jiǎn)單來(lái)說(shuō),命令解析器的核心功能其實(shí)就是字符串比較,調(diào)用相應(yīng)函數(shù),使用 C 語(yǔ)言的選擇結(jié)構(gòu)便可輕松實(shí)現(xiàn),你甚至能直接想到對(duì)應(yīng)代碼,于是你寫(xiě)出了像這樣的程序:
你非常機(jī)智地采用模塊化編程,每個(gè)子功能都用單獨(dú)的 .c 文件存放。在 cmd.c 中進(jìn)行命令的處理,通過(guò)條件語(yǔ)句比較命令,匹配后調(diào)用 gpio.c、spi.c、i2.c 文件中對(duì)應(yīng)的操作函數(shù),代碼一氣呵成。我的第一反應(yīng)也是這樣寫(xiě),嗯,沒(méi)毛病。
這是典型的過(guò)程式思維 —— 先干什么后干什么,把所有零零散散的操作通過(guò)一根時(shí)間軸串起來(lái),沒(méi)有絲毫拐彎抹角,非常直接。但這樣的過(guò)程式設(shè)計(jì)存在明顯的兩個(gè)問(wèn)題:
1. 命令增加引起跨模塊修改
2. 大量的外部函數(shù),模塊間高耦合
下面來(lái)具體解釋一下遇到的這兩個(gè)問(wèn)題。
1. 命令增加引起跨模塊修改
假設(shè)現(xiàn)在需求變化,要求增加 GPIO翻轉(zhuǎn) 命令產(chǎn)生對(duì)應(yīng)的電平變化。你趕緊在 gpio.c 文件中需要增加一個(gè)電平翻轉(zhuǎn)操作函數(shù) gpio_toggle(),同時(shí)在 cmd.c 的 switch-case 語(yǔ)句內(nèi)部添加新增的命令及函數(shù)……
等等,這不是很怪么?只是增加了 GPIO 相關(guān)功能,命令處理邏輯沒(méi)變(依然只是判斷字符串相等),為什么卻要改動(dòng) cmd.c 的命令處理邏輯?而且還是沒(méi)啥技術(shù)含量地加了一條 case 語(yǔ)句……
改兩個(gè)文件或許咬咬牙就算了,如果工程日益增大,導(dǎo)致每增加一條命令都要像「砌墻」或者「擰螺絲」一樣做一堆機(jī)械重復(fù)的工作,這樣的代碼一點(diǎn)都不酷。
2. 大量的外部函數(shù),模塊間高耦合
如果說(shuō)跨模塊修改只是一個(gè)「麻煩點(diǎn)兒」的問(wèn)題,勤快的人毫不在乎(好吧你們贏了),那模塊間高耦合則直接影響了代碼的復(fù)用性 —— 代碼不通用!這就不是小問(wèn)題了。高復(fù)用性可謂碼農(nóng)的一大追求,誰(shuí)不想只寫(xiě)一次代碼就可以拼湊成各種大項(xiàng)目,輕輕松松躺著賺錢(qián)呢?
某年后,你遇到了一個(gè)新系統(tǒng),其中也需要命令解析器功能模塊,于是你興沖沖把之前寫(xiě)的 cmd.c和 cmd.h 直接拿過(guò)來(lái)用,卻發(fā)現(xiàn)編譯報(bào)錯(cuò)找不到 gpio_high()、gpio_low()、spi_send()……你的內(nèi)心是崩潰的。
由于 gpio_high()、gpio_low() 等函數(shù)都是 gpio.c 中的外部函數(shù),在 cmd.c 中直接通過(guò)函數(shù)名調(diào)用,兩個(gè)文件像纏綿的情侶般高度耦合,這種緊密的聯(lián)系破壞了C 程序設(shè)計(jì)的一個(gè)基本原則 —— 模塊的獨(dú)立性。采用了模塊化編程,然而每個(gè)模塊卻不能獨(dú)立使用,意義何在?
面向?qū)ο笤O(shè)計(jì)
在前面發(fā)現(xiàn)的兩個(gè)問(wèn)題上對(duì)癥下藥,可以得到程序的改進(jìn)目標(biāo):
1. 增加或減少命令不影響 cmd.c
2. 命令的處理函數(shù)要成為 static,去耦合
OO思想
在解決這兩個(gè)問(wèn)題前,讓我們回到思維層面,對(duì)比「面向?qū)ο蟆古c「面向過(guò)程」思想的區(qū)別。當(dāng)我們談?wù)撁嫦蜻^(guò)程思維時(shí),程序員的角色像一個(gè)統(tǒng)治者,掌管一切、什么都要插一手。
舉個(gè)典型例子,要把大象裝到冰箱需要三步:
1. 打開(kāi)冰箱門(mén)
2. 將大象放進(jìn)冰箱
3. 關(guān)閉冰箱門(mén)
這一系列步驟的主動(dòng)權(quán)都牢牢掌握在操作者手里,操作者按部就班地把具體操作與時(shí)間軸綁定起來(lái),是典型的過(guò)程思維。再回到前面匹配命令的 switch-case 語(yǔ)句上,每增加一條新命令都需要程序員手把手地把命令和函數(shù)寫(xiě)死在程序中。于是我們就會(huì)想,能不能讓命令解析器作為一個(gè)主動(dòng)的個(gè)體自己增加命令?
這里就引入了「對(duì)象」的概念,什么是對(duì)象?我們所關(guān)注的一切事物皆為對(duì)象。在「把大象裝到冰箱」問(wèn)題中,把「大象」、「冰箱」這兩個(gè)名詞提取出來(lái),就是兩個(gè)對(duì)象。過(guò)程式思維解決問(wèn)題時(shí)考慮「需要哪些步驟」,而 OO 思想考慮「需要哪些對(duì)象」。
還是這個(gè)例子,要把大象裝到冰箱只需要兩個(gè)對(duì)象:
1. 冰箱
2. 大象
如何描述一個(gè)對(duì)象呢?可以通過(guò)兩個(gè)方面,一是對(duì)象的特征(屬性),二是對(duì)象的行為(方法/函數(shù))。由此可以列舉出描述大象和冰箱的一些屬性和方法:
? 大象的屬性(特征):品種、體形、鼻長(zhǎng)……
? 大象的方法(行為):進(jìn)食、走路、睡覺(jué)……
? 冰箱的屬性(特征):價(jià)格、容量、功耗……
? 冰箱的方法(行為):開(kāi)關(guān)機(jī)、開(kāi)關(guān)門(mén)、除霜去冰……
對(duì)象有如此多的屬性和方法,但實(shí)際上并不都能用得上。不同問(wèn)題涉及到對(duì)象的不同方面,因此可以忽略無(wú)關(guān)的屬性、方法。對(duì)于「把大象裝到冰箱」這個(gè)問(wèn)題,我們只關(guān)心「大象的體形」、「冰箱的容量」、「大象走路(說(shuō)不定能讓大象自己走進(jìn)冰箱)」、「冰箱開(kāi)關(guān)門(mén)」等這些與問(wèn)題相關(guān)的屬性和方法。
于是程序就成了「冰箱開(kāi)門(mén)、大象走進(jìn)冰箱并告訴冰箱關(guān)門(mén)」的模式,將操作的主動(dòng)權(quán)歸還對(duì)象本身時(shí),程序員不再是霸道的統(tǒng)治者,而是扮演管理員的角色,協(xié)調(diào)各對(duì)象基于自身的屬性和方法完成所需功能。
OO 版命令解析器
回歸正題,如何才能解決前面的兩個(gè)問(wèn)題、讓命令解析器更「OO」呢?首先對(duì)最終功能 ——「命令解析器解析命令」這句話(huà)深度挖掘,注意到「命令」、「命令解析器」這兩個(gè)名詞可以抽象成對(duì)象。
命令類(lèi)型的封裝
首先是「命令」本身可以封裝為包含「命令名」和「對(duì)應(yīng)操作」兩個(gè)成員的結(jié)構(gòu)體,前者是屬性,可用字符數(shù)組存儲(chǔ),后者在邏輯上是行為/函數(shù),但由于 C 語(yǔ)言結(jié)構(gòu)體不支持函數(shù),可用函數(shù)指針存儲(chǔ)。這相當(dāng)于把「命令」定義成了新的數(shù)據(jù)類(lèi)型,將命令與操作聯(lián)系起來(lái)。
// 文件名稱(chēng):cmd.h
#define MAX_CMD_NAME_LENGTH 20 // 最大命令名長(zhǎng)度,過(guò)大 51 內(nèi)存會(huì)炸
#define MAX_CMDS_COUNT 10 // 最大命令數(shù),過(guò)大 51 內(nèi)存會(huì)炸
typedef void (*handler)(void); // 命令操作函數(shù)指針類(lèi)型
/* 命令結(jié)構(gòu)體類(lèi)型 */
typedef struct cmd
{
char cmd_name[MAX_CMD_NAME_LENGTH + 1]; // 命令名
handler cmd_operate; // 命令操作函數(shù)
} CMD;
其中宏 MAX_CMD_NAME_LENGTH 表示所存儲(chǔ)命令名的最大長(zhǎng)度,handler 為指向命令操作函數(shù)的指針,所有命令操作函數(shù)均為無(wú)參無(wú)返回值。
命令解析器的封裝
同理,「命令解析器」這一模塊也可以看做一個(gè)對(duì)象,對(duì)功能模塊的封裝已經(jīng)在文件結(jié)構(gòu)上體現(xiàn),就沒(méi)必要用結(jié)構(gòu)體了,我們重點(diǎn)關(guān)注對(duì)象的內(nèi)部(即成員變量與成員函數(shù))。
成員變量
命令解析器要從一堆命令中匹配一個(gè),因此需要一種能存儲(chǔ)命令集合的數(shù)據(jù)結(jié)構(gòu),這里使用數(shù)組實(shí)現(xiàn)線(xiàn)性表:
// 文件名稱(chēng):cmd.h
/* 命令列表結(jié)構(gòu)體類(lèi)型 */
typedef struct cmds
{
CMD cmds[MAX_CMDS_COUNT]; // 列表內(nèi)容
int num; // 列表長(zhǎng)度
} CMDS;
通過(guò)結(jié)構(gòu)體封裝數(shù)據(jù)類(lèi)型定義成員變量類(lèi)型,方便在 cmd.c 中使用:
// 文件名稱(chēng):cmd.c
static xdata CMDS commands = {NULL, 0}; // 全局命令列表,保存已注冊(cè)命令集合
為了簡(jiǎn)化程序,線(xiàn)性表的「增刪改查」等基本操作就不一一獨(dú)立實(shí)現(xiàn)了,而是與命令處理過(guò)程結(jié)合(命令的注冊(cè)與匹配其實(shí)就是插入與查找過(guò)程)。下面考慮對(duì)象的成員函數(shù)。
成員函數(shù)
命令解析器涉及到那些行為呢?首要任務(wù)當(dāng)然是匹配并執(zhí)行指令。其次,要對(duì)外提供增加命令的接口函數(shù),由處理命令功能模塊主動(dòng)注冊(cè)命令,而不是通過(guò)代碼寫(xiě)死,從而就避免了跨模塊修改,硬件無(wú)關(guān)的代碼也提高了程序的可移植性。
編寫(xiě) match_cmd() 函數(shù)實(shí)現(xiàn)命令匹配,該函數(shù)接收一個(gè)待匹配的命令字符串作為參數(shù),對(duì)命令列表進(jìn)行遍歷比較操作:
// 文件名稱(chēng):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();
}
}
}
接著再實(shí)現(xiàn)注冊(cè)命令函數(shù),該函數(shù)接收一個(gè)命令類(lèi)型數(shù)組,插入到命令解析器的命令列表中:
// 文件名稱(chēng):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) // 命令列表未滿(mǎn)
{
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++;
}
}
}
至此,命令解析器便大功告成!通過(guò)調(diào)用兩個(gè)函數(shù)即可完成命令的添加與匹配功能,接下來(lái)編寫(xiě) LED 燈和蜂鳴器的操作函數(shù),測(cè)試命令解析器功能。
命令解析器的使用
注冊(cè)和匹配命令
編寫(xiě) led.c 文件,實(shí)現(xiàn) LED 的亮滅操作函數(shù),在 led_init() 函數(shù)中注冊(cè)命令并初始化硬件:
// 文件名稱(chēng):led.c
static void led_on(void)
{
LED1 = 0;
}
static void led_off(void)
{
LED1 = 1;
}
void led_init(void)
{
/* 填充命令結(jié)構(gòu)體數(shù)組 */
CMD led_cmds[] = {
{"led on", led_on},
{"led off", led_off}
};
/* 注冊(cè)命令 */
register_cmds(led_cmds, ARRAY_SIZE(led_cmds));
/* 初始化硬件 */
led_off();
}
可以看到,命令處理函數(shù) led_on() 和 led_off() 都是 static 修飾的內(nèi)部函數(shù),在其他模塊中不能通過(guò)函數(shù)名直接調(diào)用,而是通過(guò)函數(shù)指針的方式傳遞,實(shí)現(xiàn)了模塊間解耦。再者,使用結(jié)構(gòu)體數(shù)組注冊(cè)命令,大大增加程序擴(kuò)展性。
按照同樣的套路編寫(xiě) beep.c 文件實(shí)現(xiàn)蜂鳴器控制命令。
最后,在主函數(shù) while(1) 循環(huán)中接受串口字符串、解析命令并執(zhí)行:
// 文件名稱(chēng):main.c
void main()
{
unsigned char str[20];
uart_init();
led_init();
beep_init();
while (1)
{
/* 獲取串口命令字符串 */
uart_get_string(str);
/* 匹配命令并執(zhí)行 */
match_cmd(str);
/* 命令回顯 */
uart_send_string(str);
uart_send_byte('\n');
}
}
增加命令
在經(jīng)過(guò)了高度抽象封裝的命令解析器上增加一條命令,如 LED 翻轉(zhuǎn),只需要在 led.c 中增加 led_toggle() 函數(shù),并往待注冊(cè)的命令結(jié)構(gòu)體數(shù)組初始化列表中添加一個(gè)元素,然后……就完了,即使加 100 條新命令也完全不需要?jiǎng)?cmd.c 中的代碼,兩個(gè)模塊彼此獨(dú)立。
// 文件名稱(chēng):led.c
static void led_toggle(void) // 增加 LED 翻轉(zhuǎn)函數(shù)
{
LED1 = ~LED1;
}
void led_init(void)
{
/* 填充命令結(jié)構(gòu)體數(shù)組 */
CMD led_cmds[] = {
{"led on", led_on},
{"led off", led_off},
{"led toggle", led_toggle} // 增加 LED 翻轉(zhuǎn)命令
};
/* 注冊(cè)命令 */
register_cmds(led_cmds, ARRAY_SIZE(led_cmds));
/* 初始化硬件 */
led_off();
}
此外,如果 cmd.c 中改用其他數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)命令集合,也與 led.c 無(wú)關(guān),徹底切斷兩個(gè)文件的強(qiáng)耦合。cmd.c 現(xiàn)已升級(jí)為一個(gè)通用的命令解析器。
總結(jié)
從最初手動(dòng)往 cmd.c 中添加命令代碼,到最后通過(guò)函數(shù)「智能操作」,OO 思想實(shí)現(xiàn)把權(quán)利下放,每個(gè)模塊自己的事自己解決(功能模塊需要命令功能時(shí)自己主動(dòng)注冊(cè)即可),程序員再也不用對(duì)所有細(xì)節(jié)親力親為,而是為每個(gè)對(duì)象賦予該有的能力,然后對(duì)它們說(shuō)上一句:「你辦事我放心」!
工程示例代碼下載:鏈接:http://pan.baidu.com/s/1geKE2ll 密碼:e0ku
(END)
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!