前情回顾
上篇文章《【嵌入式解耦很难么】霸总:你们都要变成我的形状!》,我们围绕温湿度传感器介绍了一下观察者模式。
在前文的例子中,我们使用了 observer 类作为父类;其子类为 ble 类和 display 类,代码如下:
typedef struct _observer_t
{
void (*update)(struct _observer_t*, int, int);
struct _observer_t *next;
} observer_t;
void observer_init(observer_t *obs)
{
obs->update = NULL;
obs->next= NULL;
}
void observer_update(observer_t *obs, int temp, int hum)
{
if (obs->update != NULL)
{
obs->update(obs, temp, hum);
}
}
typedef struct _display_t
{
observer_t obs;
} display_t;
void display_show(observer_t *obs, int temp, int hum);
// 静态构造函数
void display_init(display_t *display)
{
observer_init(&display->obs);
display->obs.update = display_show;
}
// 模拟屏幕显示
void display_show(observer_t *obs, int temp, int hum)
{
printf("temp:%d\n", temp);
printf("hum:%d\n", hum);
}
typedef struct _ble_t
{
observer_t obs;
} ble_t;
void ble_send(observer_t *obs, int temp, int hum);
// 静态构造函数
void ble_init(ble_t* ble)
{
observer_init(&ble->obs);
ble->obs.update = ble_send;
}
// 模拟蓝牙发送
void ble_send(observer_t *obs, int temp, int hum)
{
printf("send temp:%d\n", temp);
printf("send hum:%d\n", hum);
}
从父子类的关系上来看。他们仅仅用到了父类的 update() ,作为给外界的统一接口。但是通过继承,我们却让我们的原有的功能依赖了父类。单论这程度上来看,它并没有感觉像“动物”之于“人类”一样,明显的逻辑上的包含关系。也并没有什么属性被子类调用。本文的结尾我将通过面向接口的方式进行对该代码进行改造, 带大家体会面向接口开发的优势。
了解继承的劣势
当一个类中有你想要复用的功能时,下意识的你会想到继承。通过创建一个子类来拓展父类的功能。实际上是复制粘贴了父类,并取了一个新的名字。然后你可以通过写新的方法拓展或重写父类的部件。
这里有个 MCU 的类,它用来表示 MCU 所具有的功能。它有属性 name,device,frequency。还有一个虚函数 usb_enable()。子类继承了父类的方法,同时对 usb_enable() 的实现各不相同。调用 usb_enable() 的时候,虽然函数都一样,但是实际上都是调用各自的重写版本。
typedef struct _MCU_t
{
const char* name;
int device;
int frequency;
int (*usb_enable)(struct _MCU_t *MCU);
...
}MCU_t ;
void muc_init(MCU_t *MCU)
{
MCU->usb_enable = NULL;
...
}
typedef struct _stm32f103_t
{
MCU_t MCU;
...
}stm32f103_t;
int stm32f103_usb_enable(MCU_t *MCU)
{
// USB 初始化相关操作
return 0;
}
void stm32f103_init(stm32f103_t *stm32f103)
{
muc_init(&stm32f103->MCU)
stm32f103->MCU.usb_enable = stm32f103_usb_enable;
...
}
typedef struct _esp32_t
{
MCU_t MCU;
...
}esp32_t;
int esp32_usb_enable(esp32_t *esp32)
{
// USB 初始化相关操作
return 0;
}
void esp32_init(esp32_t *esp32)
{
muc_init(&esp32->MCU)
esp32->MCU.usb_enable = esp32_usb_enable;
...
}
现在,我想创建一个 stm32f030 的 MCU 子类。这款芯片是一款性价比较高的芯片,但是它没有 USB 。这时候继承的问题就暴露出来了。
继承的缺点在于和子类发生耦合,父类的结构在子类身上。为了复用其其它方法,我们不得不实现一个没有任何意义的 usb_enable() 。我们让这个方法返回 NOT_SUPPORT 的错误。
#define NOT_SUPPORT -1
typedef struct _stm32f030_t
{
MCU_t MCU;
...
}stm32f030_t;
int stm32f030_usb_enable(MCU_t *MCU)
{
return NOT_SUPPORT; // 通过返回值可以知道这个对象不支持USB
}
void stm32f030_init(stm32f030_t *stm32f030)
{
muc_init(&stm32f030->MCU)
stm32f030->MCU.usb_enable = stm32f030_usb_enable;
...
}
为了实现复用 MCU 的其它方法。我们需要将 usb_enable 从父类中踢出去。创建一个新的类,叫做 mcu_interface。放入这个方法:
typedef struct _mcu_interface_t
{
int (*usb_enable)(struct _MCU_t *MCU);
}mcu_interface_t ;
不过这样就破坏了我们的 esp32 和 stm32f103 的类。当父类被修改的时候,我们必须要修改所有的类。这样的重构费时费力,这就是继承最大的劣势。当项目需求增加时,也会发生这种问题。最后只能不断地重构相关子类。修改就是继承最大的敌人。因为继承会自然而然的让你把所有共有的部分放到一个父类,但是很快你就会发现这是个特例,然后需要修改这个父类。所以在设计原则中有一个非常重要的规则:组合优于继承
组合优于继承
何为组合?其实我们在不知不觉中一直在使用组合。不通过继承复用代码就是组合。如果有两个类想复用一段代码,那么直接使用就是组合。
对上述例子进行继承转组合的改造:
// 剔除通用性不够强的虚函数
typedef struct _MCU_t
{
const char* name;
int device;
int frequency;
...
}MCU_t ;
void muc_init(MCU_t *MCU)
{
...
}
typedef struct _stm32f030_t
{
MCU_t *mcu_info;
...
}stm32f030_t;
void stm32f030_init(stm32f030_t *stm32f030)
{
...
}
// 这里stm32f030类和MCU类相互独立,如果需要MCU则将其对象传入
// 内部的MCU_t *mcu_info;只是为了保存该对象方便后续无需传入使用
void stm32f030_set_mcu_info(stm32f030_t *stm32f030, MCU_t *MCU)
{
stm32f030->mcu_info = MCU;
}
组合和继承的区别在于继承是直接在子类的开头写上父类,而组合则是通过指针关联一个独立的对象。在继承中,子类会继承父类的所有方法。而在组合中,需要分别对组合的不同对象进行操作。在通过方法将他们组合在一起。
继承的有趣之处在于它结合了两种不同的能力于一身。它的继承能够复用代码。它的多态可以抽象代码。所谓的抽象,指的是你可以在父类中实现一系列的“空壳”函数(虚函数),而我们的子类需要完善这些函数的具体实现。对于调用者来说,这部分的功能被我们抽象化成父类相同的方法,而不需要知道具体实现的细节。
那么对应的组合来说,因为 MCU 对象和 stm32f030 对象是相互独立的,最终通过 set 函数结合在一起。所以我们完全可以配置一个新的 MCU 对象给 esp32 对象,一个新的 MCU 对象给 stm32f103 对象。他们公共部分也实现了复用。
对于 esp32 类和 stm32f103 类来说之前的继承可以通过 usb_enable() 方法来抽象他们的 usb 使能功能,从而不需要了解各自需要具体调用哪些函数。有没有既简单又方便能达到抽象这个功能呢?
有的兄弟有的,接下来我们就介绍一下我们的重头戏:接口
另一种抽象:接口
接口不像完整的类,有各种变量和方法。它只是给出一份对象的能力协议。
typedef struct _MCU_t
{
const char* name;
int device;
int frequency;
...
}MCU_t ;
// 我们将剔除的虚函数单独的声明成一个类
// 它表示能对MCU进行的操作,即 usb_enable()
typedef struct _mcu_interface_t
{
int (*usb_enable)(void *self);
void* self; // 用于保存对象
}mcu_interface_t ;
// 主要简化了usb_enable的调用
void mcu_usb_enable(mcu_interface_t *mcu_interface)
{
mcu_interface->usb_enable(mcu_interface->self);
}
typedef struct _stm32f103_t
{
MCU_t *MCU;
...
}stm32f103_t;
// stm32f103的usb使能函数
static int stm32f103_usb_enable(void *MCU)
{
stm32f103_t *stm32f103 = (stm32f103_t *)MCU;
...
}
// stm32f103的接口对象传入进去被初始化。
void stm32f103_init(stm32f103_t *stm32f103)
{
...
}
// 组合 MCU 对象
void stm32f103_set(stm32f103_t *stm32f103, MCU_t *MCU)
{
stm32f103->MCU = MCU;
...
}
// 放出内部的接口
void stm32f103_get_interface(stm32f103_t *stm32f103, mcu_interface_t *mcu_interface)
{
mcu_interface->usb_enable = stm32f103_usb_enable; // 将接口返回出去
mcu_interface->self = stm32f103;
}
typedef struct _esp32_t
{
MCU_t *MCU;
...
}esp32_t;
int esp32_usb_enable(void *MCU)
{
esp32_t *esp32= (esp32_t *)MCU;
...
}
// 组合 MCU 对象
void esp32_init(esp32_t *esp32, MCU_t *MCU)
{
esp32->MCU = MCU;
...
}
// 传进去接受esp32的接口。
void esp32_get_interface(esp32_t *esp32, mcu_interface_t* mcu_interface)
{
mcu_interface->usb_enable = esp32_usb_enable; // 将接口返回出去
mcu_interface->self = esp32;
}
对于维护者来说,原有的代码上多写一个接口类型和函数,即可修改好。对于使用者来说,关注的是 mcu_interface_t 。我不需要知道对方写的是什么对象,只要给我接口类型,后续的代码之间仅仅通过接口类型即可。这就是接口化操作。
接口化改造
现在,让我们把视线拉回到温湿度传感器的例子。让我们来改造这里例子,使用组合替代继承
typedef struct _observer_interface_t
{
void (*update)(void*, int, int);
void* self;
} observer_interface_t;
// 主要简化了update的调用
void observer_update(observer_interface_t* observer_interface)
{
observer_interface->update(observer_interface->self);
}
typedef struct _display_t
{
...
} display_t;
// 静态构造函数
void display_init(display_t *display)
{
...
}
// 模拟屏幕显示
static void display_show(void* device, int temp, int hum)
{
// 这里转换是为了后续可能用到对象里面的属性
display_t* display = (display_t*)device;
printf("temp:%d\n", temp);
printf("hum:%d\n", hum);
...
}
// 获取屏幕的接口
void display_get_interface(display_t *display, observer_interface_t *interface)
{
interface->update = display_show;
interface->self= display;
}
typedef struct _ble_t
{
...
} ble_t;
// 静态构造函数
void ble_init(ble_t* ble)
{
...
}
// 模拟蓝牙发送
static void ble_send(void *device, int temp, int hum)
{
// 这里转换是为了后续可能用到对象里面的属性
ble_t* ble = (ble_t*)device;
printf("send temp:%d\n", temp);
printf("send hum:%d\n", hum);
...
}
// 获取屏幕的接口
void ble_get_interface(ble_t *ble, observer_interface_t *interface)
{
interface->update = ble_send;
interface->self = ble ;
}
接下来我们要改造温湿度传感器部分:
typedef struct _interface_list_t
{
observer_interface_t *interface;
struct _observer_list_t *next;
} interface_list_t;
typedef struct _sensor_t
{
int temp;
int hum;
interface_list_t* interfaces;
} sensor_t;
// 静态构造函数
void sensor_init(sensor_t *sensor)
{
sensor->temp = 0;
sensor->hum = 0;
sensor->obs = NULL;
}
// 添加接口列表
void sensor_attach(sensor_t *sensor, observer_interface_t *observer_interface)
{
interface_list_t *interface = (interface_list_t *)malloc(sizeof(interface_list_t))
if (interface == NULL) return;
interface->next = NULL;
interface->interface = observer_interface;
if (!sensor->interfaces) {
sensor->interfaces= interface;
} else {
interface_list_t *cur = sensor->interfaces;
while (cur->next) cur = cur->next;
cur->next = interface;
}
}
// 删除接口列表
void sensor_detach(sensor_t *sensor, observer_interface_t *observer_interface)
{
interface_list_t **indirect = &sensor->interface; // 使用二级指针简化操作
while (*indirect) {
if (*indirect->interface == observer_interface) {
interface_list_t *temp = *indirect;
*indirect = *indirect->interface->next; // 绕过目标节点
free(temp);
return;
}
indirect = &(*indirect)->next;
}
}
// 调用接口列表
void sensor_notify(sensor_t *sensor, int temp, int hum)
{
interface_list_t *interface_list = sensor->interface_list;
while(interface_list)
{
observer_update(interface_list->interface);
interface_list = interface_list->next;
}
}
// 模拟温湿度传感器读取数值
void sensor_get(sensor_t *sensor, int temp, int hum)
{
sensor->temp = temp;
sensor->hum = hum;
sensor_notify(sensor, temp, hum);
}
经过改造我们发现现在温湿度传感器类和 ble、display 之间的依赖仅仅是 observer_interface_t 而接口的结构体可以在两方定义。这样,双方无论是面向对象还是面向过程的书写方式,只需要实现接口就可以做到适配。耦合性进一步降低。通过组合代替继承,组合的对象如果发生改变也不会影响到子类。而且组合对象还能组合多个,比多继承好用多了。
结尾
面向接口编程讲究的就是双方通过接口进行松耦合,而不是一个完整的对象。两边可以是面向对象的书写方式,也可以直接是面向过程的书写方式。总之,实现了接口就可以使用。所以这种方式更加受欢迎。
我见到比较完整的写法就是乐鑫的 esp-iot-solution。其中的 lcd 部分是比较个性鲜明的面向对象+面向接口的编程。
在 arm 的开源库 arm2d 中也经常出现
大家对编程范式感兴趣的可以通过 gpt 或者 deepseek 提问。你还了解编程范式有哪些?欢迎评论区讨论。
END
原文:裸机思维
专栏推荐文章
- 【嵌入式解耦很难么】霸总:你们都要变成我的形状!
- 超级嵌入式系统“性能/时间”工具箱
- 当 DeepSeek 接管操作系统:智能体(Agent)真能让程序员提前退休?
- 【为宏正名】for的妙用你想不到
- 超越 JLINK?这是 DAPLink 的最后一块拼图
如果你喜欢我的思维,欢迎订阅裸机思维欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。