2、分层架构设计原则:核心思想与实战落地
分层架构,说白了就是把代码按职责切几刀。每一层只干自己的事,不越界。我刚开始做嵌入式时,觉得分层就是多写几个文件夹,后来被一个项目狠狠教育了——一个模块改bug,牵连了三个模块一起重编。嗯,从那以后我才真正理解什么叫“层”。
2.1 分层架构的核心思想
核心就一句话:上层依赖下层,下层不依赖上层。每一层只通过接口跟相邻层通信,不能跨层调用。
典型嵌入式三层结构:
- 应用层:业务逻辑、任务调度、协议处理
- 中间层:驱动抽象、OS封装、公共组件
- 硬件抽象层(HAL):寄存器操作、外设驱动、BSP
我在项目中遇到过最典型的反例:应用代码里直接写GPIO寄存器。换了个芯片平台,整个应用层全得重写。这就是没分层的代价。
2.2 高内聚低耦合
这两个词听着抽象,我换个说法:高内聚就是每个模块只干一件事,并且干好;低耦合就是模块之间尽量少说话,说话也只通过接口。
| 原则 | 含义 | 反面案例 |
|---|---|---|
| 高内聚 | 模块内部功能紧密相关,不掺杂无关逻辑 | 一个.c文件里同时放UART驱动、PID算法、LED闪烁 |
| 低耦合 | 模块间通过接口通信,不直接访问内部数据 | task_A.c直接修改task_B.c的全局变量 |
我的判断标准:如果你改一个模块的代码,需要同时改另外两个模块,那就是耦合太高了。我曾经在一个项目里,就因为一个定时器中断处理函数里塞了三个模块的逻辑,导致每次调参都要翻三个文件。后来拆成三个独立回调,世界清净了。
2.3 接口隔离原则
接口隔离原则(ISP)说:客户端不应该依赖它不需要的接口。翻译成人话:别给调用方塞一堆用不上的函数。
举个例子,你写一个传感器驱动,不要把所有传感器的操作都塞进一个接口里:
// ❌ 坏设计:一个接口塞了所有功能
typedef struct {
void (*init)(void);
void (*read_temp)(float *temp);
void (*read_humidity)(float *hum);
void (*read_pressure)(float *pres);
void (*calibrate)(void);
void (*self_test)(void);
} sensor_if_t;
// ✅ 好设计:按职责拆分接口
typedef struct {
void (*init)(void);
void (*read)(float *value);
} sensor_read_if_t;
typedef struct {
void (*calibrate)(void);
uint8_t (*self_test)(void);
} sensor_maint_if_t;
我建议:接口越小越好,一个接口最好只干一件事。调用方只需要知道自己用到的接口,不需要知道其他接口的存在。
注意:接口隔离不是让你把接口拆得稀碎。粒度要适中——太粗了耦合高,太细了管理成本高。我个人习惯:一个接口文件不超过5个函数指针。
2.4 依赖倒置原则
依赖倒置原则(DIP)是分层架构的基石。它的核心是:高层模块不应该依赖低层模块,两者都应该依赖抽象。
你想想看,如果应用层直接调用HAL层的函数,那换硬件平台时应用层也得改。正确的做法是:应用层定义接口,HAL层去实现这个接口。
// 应用层定义抽象接口(不依赖具体硬件)
typedef struct {
void (*send)(uint8_t *data, uint16_t len);
void (*recv)(uint8_t *data, uint16_t *len);
} uart_if_t;
// HAL层实现这个接口(依赖抽象)
static void uart1_send(uint8_t *data, uint16_t len) {
// 操作UART1寄存器
}
static void uart1_recv(uint8_t *data, uint16_t *len) {
// 读取UART1数据
}
const uart_if_t uart1_driver = {
.send = uart1_send,
.recv = uart1_recv,
};
这样做的好处:应用层只认接口,不认具体硬件。换芯片?换个驱动实现就行,应用层代码一行不动。
依赖倒置的实战价值:
- 硬件平台迁移时,应用层零修改
- 单元测试时,可以注入Mock驱动
- 多人协作时,接口定义先行,开发可并行
2.5 四个原则的协同关系
这四个原则不是孤立的。我画了张图,你看它们怎么配合:
高内聚和低耦合保证模块内部不乱,接口隔离和依赖倒置保证模块之间不纠缠。四个原则一起用,你的代码才能经得起折腾。
我的经验:新项目开始时,先花半天把接口定义好。接口定了,分层就定了。后面写代码只是填空。我曾经因为急着写代码,跳过接口设计,结果项目中期重构了三次——血的教训。