【嵌入式解耦很难么】“面向对象”还是“面向过程”?接口说“我全要”

前情回顾

上篇文章《【嵌入式解耦很难么】霸总:你们都要变成我的形状!》,我们围绕温湿度传感器介绍了一下观察者模式。

在前文的例子中,我们使用了  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() ,作为给外界的统一接口。但是通过继承,我们却让我们的原有的功能依赖了父类。单论这程度上来看,它并没有感觉像“动物”之于“人类”一样,明显的逻辑上的包含关系。也并没有什么属性被子类调用。本文的结尾我将通过面向接口的方式进行对该代码进行改造, 带大家体会面向接口开发的优势。

image.png

了解继承的劣势

当一个类中有你想要复用的功能时,下意识的你会想到继承。通过创建一个子类来拓展父类的功能。实际上是复制粘贴了父类,并取了一个新的名字。然后你可以通过写新的方法拓展或重写父类的部件。

Image

这里有个  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 。这时候继承的问题就暴露出来了。

Image

继承的缺点在于和子类发生耦合,父类的结构在子类身上。为了复用其其它方法,我们不得不实现一个没有任何意义的  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 使能功能,从而不需要了解各自需要具体调用哪些函数。有没有既简单又方便能达到抽象这个功能呢?

有的兄弟有的,接下来我们就介绍一下我们的重头戏:接口

Image

另一种抽象:接口

接口不像完整的类,有各种变量和方法。它只是给出一份对象的能力协议。

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 。我不需要知道对方写的是什么对象,只要给我接口类型,后续的代码之间仅仅通过接口类型即可。这就是接口化操作。

Image

接口化改造

现在,让我们把视线拉回到温湿度传感器的例子。让我们来改造这里例子,使用组合替代继承

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  而接口的结构体可以在两方定义。这样,双方无论是面向对象还是面向过程的书写方式,只需要实现接口就可以做到适配。耦合性进一步降低。通过组合代替继承,组合的对象如果发生改变也不会影响到子类。而且组合对象还能组合多个,比多继承好用多了。

Image

结尾

面向接口编程讲究的就是双方通过接口进行松耦合,而不是一个完整的对象。两边可以是面向对象的书写方式,也可以直接是面向过程的书写方式。总之,实现了接口就可以使用。所以这种方式更加受欢迎。

我见到比较完整的写法就是乐鑫的 esp-iot-solution。其中的 lcd 部分是比较个性鲜明的面向对象+面向接口的编程。

Image

在 arm 的开源库 arm2d 中也经常出现

Image

大家对编程范式感兴趣的可以通过 gpt 或者 deepseek 提问。你还了解编程范式有哪些?欢迎评论区讨论。

END

原文:裸机思维

专栏推荐文章

如果你喜欢我的思维,欢迎订阅裸机思维欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。
推荐阅读
关注数
1491
内容数
125
探讨嵌入式系统开发的相关思维、方法、技巧。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息