邵国际: 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

相关文章
相关标签/搜索