22、函数设计原则:高内聚低耦合,单一职责原则
写代码这事儿,写得久了你会发现一个规律:好函数和烂函数之间的差距,往往不是语法问题,而是设计问题。语法错了编译器会告诉你,但设计烂了,只有维护的人会骂你——而那个人,很可能就是三个月后的你自己。
今天咱们聊两个核心原则:高内聚低耦合和单一职责原则。这两个东西,说白了就是告诉你「函数该怎么切分」以及「函数该管多少事」。
22.1 单一职责原则:一个函数只做一件事
单一职责原则(Single Responsibility Principle,SRP)最早是面向对象设计里的概念,但放在C语言函数设计里同样适用。它的核心思想很简单:一个函数应该只有一个让它修改的理由。
嗯,这句话有点绕。我换个说法:一个函数只干一件事,而且要干好。
我在项目中遇到过这样的代码:一个函数叫 process_data(),打开文件、解析数据、计算校验和、写日志、更新UI……全塞在里面。你想想看,这个函数有多少个修改理由?文件格式变了要改它,校验算法变了要改它,日志格式变了还要改它。每次改都提心吊胆,生怕碰了不该碰的逻辑。
单一职责的衡量标准:如果你不能用一句话说清楚这个函数是干什么的,或者那句话里出现了「并且」「同时」「然后」这些词,那这个函数大概率违反了单一职责原则。
来看个对比。假设我们要实现一个功能:从传感器读取温度值,转换成摄氏度,然后打印出来。
❌ 违反单一职责的写法:
// 这个函数干了三件事:读取、转换、打印
void read_and_display_temperature(void) {
uint16_t raw = 0;
// 读取原始值
raw = *(volatile uint16_t *)0x4000;
// 转换成摄氏度
float celsius = (raw * 3.3f / 4096.0f - 0.5f) * 100.0f;
// 打印
printf("Temperature: %.2f°C\n", celsius);
}
✅ 符合单一职责的写法:
// 职责1:读取原始传感器值
uint16_t sensor_read_raw(void) {
return *(volatile uint16_t *)0x4000;
}
// 职责2:将原始值转换为摄氏度
float sensor_raw_to_celsius(uint16_t raw) {
return (raw * 3.3f / 4096.0f - 0.5f) * 100.0f;
}
// 职责3:显示温度
void display_temperature(float celsius) {
printf("Temperature: %.2f°C\n", celsius);
}
// 组合使用
void read_and_display_temperature(void) {
uint16_t raw = sensor_read_raw();
float celsius = sensor_raw_to_celsius(raw);
display_temperature(celsius);
}
你看,拆开之后每个函数都只有一行核心逻辑。测试的时候可以单独测转换函数,换传感器只需要改 sensor_read_raw(),换显示方式只需要改 display_temperature()。互不影响。
我的个人习惯:写函数时先问自己三个问题——这个函数的名字能准确描述它的行为吗?它有没有副作用?如果别人只看函数名不看实现,能猜出它做了什么吗?如果答案都是肯定的,那这个函数大概率设计得不错。
22.2 高内聚:函数内部的逻辑要紧密相关
高内聚,指的是函数内部的各个操作之间应该有强关联性。它们应该为了完成同一个目标而协同工作,而不是各干各的。
我举个例子你就明白了。假设有个函数叫 init_system(),它里面做了这些事:
void init_system(void) {
// 初始化硬件
gpio_init();
uart_init();
timer_init();
// 初始化软件模块
task_manager_init();
log_buffer_init();
// 校准传感器
sensor_calibrate();
// 播放启动音效
buzzer_play_startup_tone();
}
这个函数内聚性高吗?不高。因为「播放启动音效」和「初始化硬件」之间没有必然联系。如果系统不需要音效了,这个函数还得改。更好的做法是把音效初始化单独拿出来,或者放到一个专门的启动流程里。
高内聚的函数有什么好处?
- 可读性强:看函数名就知道它要干什么,不用深入细节
- 可维护性好:修改一个功能只需要改一个地方
- 可复用性高:内聚的函数更容易被其他地方调用
我曾经踩过的坑:在一个嵌入式项目中,我把CRC校验和日志输出写在了同一个函数里。后来日志格式要改,我改了日志部分,结果不小心把CRC计算的临时变量覆盖了。排查了整整一个下午才发现问题。从那以后,我坚决不让一个函数同时做「计算」和「输出」这两件事。
22.3 低耦合:函数之间的依赖要尽量少
低耦合,指的是函数与函数之间、模块与模块之间的依赖关系要尽可能弱。一个函数不应该知道另一个函数的内部实现细节,也不应该依赖全局状态。
耦合度高的典型表现:
- 函数A调用了函数B,但函数B需要先调用函数C才能正常工作
- 函数内部直接操作了全局变量
- 函数之间通过共享内存或全局缓冲区传递数据
高耦合的代码长什么样?
// 全局变量,到处都在用
int g_temperature = 0;
void sensor_update(void) {
g_temperature = read_sensor();
}
void display_update(void) {
// 直接依赖全局变量
printf("Temp: %d\n", g_temperature);
}
void alarm_check(void) {
// 也依赖同一个全局变量
if (g_temperature > 80) {
alarm_on();
}
}
这种代码的问题在于:display_update() 和 alarm_check() 都隐式依赖 sensor_update() 先被调用。如果调用顺序错了,或者中间有人改了 g_temperature,结果就全乱了。
低耦合的改进方案:
// 通过参数传递数据,不依赖全局变量
void sensor_update(int *temperature) {
*temperature = read_sensor();
}
void display_update(int temperature) {
printf("Temp: %d\n", temperature);
}
void alarm_check(int temperature) {
if (temperature > 80) {
alarm_on();
}
}
// 调用方负责协调
int temp = 0;
sensor_update(&temp);
display_update(temp);
alarm_check(temp);
你看,改成参数传递之后,每个函数都不依赖全局状态。测试的时候可以随便传值进去测,不用管调用顺序。
我建议:能用参数传递就别用全局变量,能用返回值就别用输出参数。函数之间的接口越简单,耦合度就越低,以后改起来就越轻松。
22.4 知识体系结构图
下面这张图总结了函数设计的核心原则和它们之间的关系:
22.5 实践中的权衡
理论说完了,咱们聊聊实际。你可能会问:是不是函数拆得越细越好?
不是。拆得太细也有问题:
- 函数调用有开销(尤其是嵌入式环境,栈空间有限)
- 函数太多会让代码变得碎片化,反而不好读
- 过度抽象会让逻辑变得隐晦
我个人习惯的衡量标准是:如果一个函数超过30行,或者有超过3层缩进,我就考虑拆它。但也不是死规矩,有时候一个20行的函数逻辑非常紧凑,拆开反而破坏可读性,那就不拆。
核心原则总结:
- 单一职责:一个函数只做一件事,用一句话能说清楚
- 高内聚:函数内部的操作都是为了同一个目标
- 低耦合:函数之间通过参数传递数据,不依赖全局状态
- 适度拆分:不要为了拆分而拆分,保持代码的自然表达
最后说一句:这些原则不是教条,是经验总结。你写代码的时候多想想「如果三个月后我来改这个函数,我还能看懂吗?」——想明白了,设计自然就对了。
公众号:蓝海资料掘金营,微信 deep3321