接口设计原则:高内聚低耦合、接口稳定性、可移植性、可测试性
各位同学,咱们今天聊点实在的。接口设计这件事,说白了就是定规矩。你想想看,一个RTOS里少说几十个驱动,多则上百个模块,如果没有一套统一的接口规范,那代码写起来就是各玩各的,最后集成的时候哭都来不及。
我个人习惯,在设计HAL接口之前,先把四个核心原则刻在脑子里。这四个原则就像房子的四根承重柱,缺一根都不行。
2.1 高内聚低耦合
什么叫高内聚?就是一个模块只干一件事,而且把这件事干到极致。低耦合呢?模块之间尽量少说话,说话也只通过定义好的接口。
我在项目中遇到过这样一个坑:有个同事把GPIO控制和定时器配置写在同一个函数里。当时看着挺方便,后来要换MCU平台,好家伙,改一个功能牵扯出一堆依赖。这就是典型的耦合太高。
举个例子,一个高内聚的GPIO接口应该长这样:
// 高内聚:只做GPIO相关操作
hal_status_t hal_gpio_init(gpio_t *gpio, gpio_config_t *config);
hal_status_t hal_gpio_set_pin(gpio_t *gpio, uint8_t pin, gpio_level_t level);
hal_status_t hal_gpio_get_pin(gpio_t *gpio, uint8_t pin, gpio_level_t *level);
hal_status_t hal_gpio_deinit(gpio_t *gpio);
你看,每个函数都只做一件事。初始化、设置电平、读取电平、反初始化。清清楚楚,明明白白。
低耦合怎么体现?接口参数里不要传其他模块的句柄。比如GPIO接口里出现timer_handle_t,那就是耦合的信号。
2.2 接口稳定性
接口一旦发布,就不要轻易改。为什么?因为你的接口会被多个任务、多个驱动调用。改一个接口名或者参数顺序,可能引发连锁反应。
我有个习惯:接口设计阶段多花点时间想清楚,后面就能少挨骂。稳定性不是说不让改,而是改的时候要有策略。
| 变更类型 | 影响范围 | 处理方式 |
|---|---|---|
| 新增接口 | 无影响 | 直接添加,保留旧接口 |
| 修改参数 | 所有调用方 | 废弃旧接口,提供新接口 |
| 删除接口 | 所有调用方 | 标记为废弃,至少保留一个版本 |
| 修改返回值 | 所有调用方 | 必须同步更新,建议用枚举扩展 |
你看这个表,新增接口是最安全的。所以我建议,设计初期宁可多定义几个接口,也别想着"先凑合用,后面再改"。
#define HAL_GPIO_API_VERSION 2,这样上层代码可以根据版本号做适配。
2.3 可移植性
可移植性,说白了就是换芯片不换接口。你从STM32换到GD32,甚至换到NXP,上层应用代码一行都不改,这才叫可移植。
怎么做到?把硬件相关的部分全部藏在HAL层下面。上层只跟HAL接口打交道,不直接操作寄存器。
// 不可移植的写法:直接操作寄存器
#define GPIOA_BASE 0x40020000
*(volatile uint32_t *)(GPIOA_BASE + 0x14) |= (1 << 5);
// 可移植的写法:通过HAL接口
hal_gpio_set_pin(&gpio_led, 5, GPIO_LEVEL_HIGH);
嗯,这里要注意:可移植性不是免费的午餐。它需要你在设计阶段就做好抽象。我一般会先画一个硬件抽象层的分层图,把每层的职责定死。
你看这个分层,每一层只跟相邻层通信。换芯片的时候,只需要重写最下面那层硬件驱动,上面的HAL接口、RTOS内核、应用代码,一行都不用动。
2.4 可测试性
可测试性,很多人容易忽略。你想想看,一个接口如果没法单独测试,你怎么保证它没问题?
我建议,每个HAL接口都要能独立测试。怎么做?两个关键点:
- 接口参数可模拟:传入的参数能构造各种边界情况
- 返回值可验证:每个接口都有明确的返回值,方便断言检查
举个例子,一个可测试的UART发送接口:
// 可测试的接口设计
hal_status_t hal_uart_send(uart_t *uart, const uint8_t *data, uint32_t len, uint32_t timeout);
// 测试用例示例
void test_hal_uart_send_normal(void) {
uart_t uart;
uint8_t test_data[] = "Hello HAL";
hal_status_t ret;
ret = hal_uart_send(&uart, test_data, sizeof(test_data), 100);
assert(ret == HAL_OK); // 正常情况应该返回OK
}
void test_hal_uart_send_null_ptr(void) {
hal_status_t ret;
ret = hal_uart_send(NULL, NULL, 0, 100);
assert(ret == HAL_INVALID_PARAM); // 空指针应该返回错误
}
你看,每个测试用例只测一个场景。正常情况、异常情况、边界情况,全覆盖。
typedef enum {
HAL_OK = 0,
HAL_ERROR,
HAL_BUSY,
HAL_TIMEOUT,
HAL_INVALID_PARAM
} hal_status_t;
可测试性还有一个好处:方便做回归测试。每次改完底层驱动,跑一遍测试用例,马上知道有没有破坏原有功能。
2.5 四个原则的关系
这四个原则不是孤立的。它们互相支撑:
- 高内聚低耦合 是基础,让接口职责清晰
- 接口稳定性 是保障,让接口值得信赖
- 可移植性 是目标,让接口跨平台可用
- 可测试性 是手段,让接口质量可控
我做了这么多年嵌入式,发现一个规律:凡是接口设计做得好的项目,后期维护成本至少降低一半。反过来,接口设计随便糊弄的项目,后面改bug改到怀疑人生。
嗯,这一章的内容就到这里。记住这四个原则,后面咱们写接口的时候,我会反复拿它们来检验。