关于设计模式,有很多软件工程师认为,设计模式都是高级编程中的事情,并且多少有点被玩烂了。在嵌入式C语言中,设计模式是非常过时且没有实用价值的东西。笔者认为,设计模式在嵌入式编程中其实还是有很多用武之地的。用好了可以在小小的单片机上很好地实现你的需求。
笔者的有关的设计模式的内容来源于《Head First 设计模式》。使用UML2.0建模,并在模型中体现设计模式,再用C代码进行实现。
UML2.0的标准可以参考这个链接:UML Specification
我用的硬件是基于嵌入式单片机或MCU的,比如常见的STM32F系列MCU。软件平台可以考虑KEIL,STM32CUBEIDE,SEGGER STUDIO都可以。
下面先从单件模式开始介绍,比较传统的平铺式和单件模式封装式的差别。然后在单件模式的基础上发展策略模式。
在嵌入式系统中,往往需要对资源进行封装管理。例如,将一组LED灯封装成警报灯,将传感器封装为一个模块。这就会用到单件模式。
单件模式是指:
单件模式确保一个类只有一个实例,并提供一个全局访问点。——《Head First 设计模式》
就报警灯为例,一种常见的做法是设计leds.h和leds.c两个文件,在头文件中声明相关的函数,在源文件中实现这些函数。这两个文件的代码如下所示。
/* leds.h*/
#ifndef _LEDS_H_
#define _LEDS_H_
#include
typedef enum Led_ColourKind_Def{
LED_NONE,
LED_YELLOW,
LED_RED,
LED_BOTH,
}Led_ColourKind;
void leds_init(void);
void leds_setColour(Led_ColourKind);
...
#endif
/*leds.c*/
#include "leds.h"
void leds_init(void);
void leds_setColour(Led_ColourKind);
void leds_init(void){
;/*Code Implementation*/
}
void leds_setColour(Led_ColourKind Colour){
;/*Code Implementation*/
}
...
这种写法在抛开做具体项目,只是做个简单的硬件测试的时候是没有问题的。但是如果要做项目,这种办法不能直观得体现UML建模。如果事先使用UML建模,你会得到一个如下所述的模型。
就这个模型而言,前面的代码不能很直观地体现该模型,尽管从功能上可以实现。但是随着模型的成长,这种平铺式的代码就会显得比较乏力。这里给出单件模式的写法。
/*leds.h*/
#ifndef _LEDS_H_
#define _LEDS_H_
#include
#include
typedef enum Led_ColourKind_Def{
LED_NONE,
LED_YELLOW,
LED_RED,
LED_BOTH,
}Led_ColourKind;
typedef struct _Leds_TypeDef{
void (*init)(void);
void (*setColour)(Led_ColourKind);
...
}Leds_TypeDef;
extern const Leds_TypeDef leds;
#endif
/*leds.c*/
#include "leds.h"
extern const Leds_TypeDef leds;
static void init (void);
static void setColour (Led_ColourKind);
const Leds_TypeDef leds = {
.init = init ,
.setColour = setColour ,
};
/*省略init和setColour的实现,因为和前面的一样*/
void init(void){
/*初始化代码*/
}
void setColour (Led_ColourKind colour){
/*设置颜色有关代码*/
}
这样设计,则整个系统中只有一个类,及Leds_TypeDef。这个类只有一个实例,就是leds。leds同时也是唯一的全局访问点。这样的设计就是单件模式的实现。应用举例,通过下面的代码就可以在main函数中改变灯的颜色为红色。
.../*其他的代码*/
void main(void){
leds.init();
leds.sesColour(LED_RED);
}
.../*其他的代码*/
这里要说明一点,leds对象被定义为const类型的原因是,在大部分MCU中,ROM都是比RAM要大的。所以尽可能把大的数据定义成const类型,这样编译后就可以被放到ROM上。
根据需求变更,为了迎合“多用组合,少用继承”和“针对接口编程,不针对实现编程”的面向对象原则,我们的模型发展成下面的样子。
策略模式的定义为:
策略模式定义了算法族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。 ——《Head First 设计模式》
虽然在Java中有interface关键字,也有很好的机制去实现各种设计模式以及各种UML语法,但是在C语言中,主要还是靠C语言本身的语法去部分地实现UML概念,并以C语言特色的方法去实现有关的设计模式。策略模式要求C语言中实现interface。
图片来源:OMG® Unified Modeling Language® (OMG UML®) v2.5.1
这里给出interface在UML2.0中的词法定义
An Interface is a kind of Classifier that represents a declaration of a set of public Features and obligations that together constitute a coherent service. An Interface specifies a contract; any instance of a Classifier that realizes the Interface shall fulfill that contract.
——OMG® Unified Modeling Language® (OMG UML®) v2.5.1
参考interface的词法定义和上面的用UML描述的interface在该语言中的定义,可以理解为只定义了标识而不去实现它,它的实现放在它的类或者数据结构中做。如果是个C++类,那么就相当于是虚基类、虚函数。放到C语言中,可以是个结构体指针,或者是函数指针。这样,概括来说,包含了这个接口的类、结构体或其他标识,
本例中,我们的Leds_TypeDef包含了一个接口叫IEmergency,这个接口有一个行为叫blink(),有三个实现分别是_Emergency_Low、_Emergency_Normal和_Emergency_High。笔者考虑使用一个间接的函指针blink_emergency和函数列表blink_emergency_list[]来实现。具体代码如下所示。
/*leds.h*/
#ifndef _LEDS_H_
#define _LEDS_H_
#include
#include
typedef enum Led_ColourKind_Def{
LED_NONE,
LED_YELLOW,
LED_RED,
LED_BOTH,
}Led_ColourKind;
typedef enum Led_EmergencyKind_Def{
LED_EMERGENCY_HIGH,
LED_NORMAL,
LED_LOW,
}Led_EmergencyKind;
typedef struct _Leds_TypeDef{
void (*init)(void);
void (*setColour)(Led_ColourKind);
void (*blink)(bool);
void (*set_Emergency)(Led_EmergencyKind);
...
}Leds_TypeDef;
extern const Leds_TypeDef leds;
#endif
/*leds.c*/
#include "leds.h"
extern const Leds_TypeDef leds;
static void init (void);
static void setColour (Led_ColourKind);
static void blink (bool);
static void set_Emergency (Led_EmergencyKind);
static void _blink_emergency_high (void);
static void _blink_emergency_normal (void);
static void _blink_emergency_low (void);
static void (*_blink_emergency_list[])(void) = {
_blink_emergency_high ,
_blink_emergency_normal ,
_blink_emergency_low ,
}
static void (*blink_emergency)(void) = _blink_emergency_low;
const Leds_TypeDef leds = {
.init = init ,
.setColour = setColour ,
.blink = blink ,
.set_Emergency = set_Emergency ,
};
/*省略init和setColour的实现,因为和前面的一样*/
void blink(bool state){
if(state){
blink_emergency();
}
else{
/*关闭闪烁*/
}
}
void set_Emergency(Led_EmergencyKind emg){
blink_emergency = _blink_emergency_list[emg];
}
在外部通过以下的调用方法来改变接口的实现,并执行接口的行为改成高紧急度,并启动闪烁。
void main(void){
/*其他的代码*/
leds.set_Emergency(LED_EMERGENCY_HIGH);
leds.blink(true);
/*其他的代码*/
}
本文中笔者使用结构体、函数指针和函数列表为实现手段,以UML2.0为建模语言为建模方法,以单件模式和策略模式为例实现了动态改变leds的接口行为和执行。可以看出,
因此,在嵌入式下使用设计模式是可行并且有效的。只是在使用的时候,要考虑在当前的编译环境下语法的特点;要摆脱思维定式,在理解了UML的词法和语法后用恰当的C语言表达。就可以在实际项目中发挥设计模式的优势,做出高效、高可维护性的软件编码设计。