法则二:单一职责原则 — 一个函数只做一件事
各位工程师,咱们今天聊聊函数拆分。说实话,我见过太多人把函数写成「万能工具箱」—— 一个函数里塞了初始化、数据处理、状态判断、甚至还有打印日志。结果呢?改一个地方,崩三个功能。
单一职责原则,说白了就是:一个函数,只干一件事,并且把它干好。听起来简单,做起来可没那么容易。
为什么非要拆?
我刚开始写嵌入式代码那会儿,也喜欢把功能堆在一起。觉得函数多了调用麻烦,不如一个函数搞定。直到有一次,我在一个 300 行的函数里找 bug,找了整整两天。
你想想看,一个函数如果既做 A 又做 B 还做 C,那它的复杂度是指数级上升的。测试的时候,你得覆盖 A、B、C 的各种组合。维护的时候,改 A 的逻辑,可能把 B 搞坏了。
拆开之后,每个函数独立测试,独立修改。出了问题,定位也快。说白了,这是给自己省事。
怎么判断一个函数「做太多」?
我有个简单的判断方法:看函数名能不能准确描述它的行为。
比如你写了个函数叫 process_data(),那基本可以断定它做了不止一件事。真正好的函数名,应该像 calculate_crc()、filter_noise()、update_display() 这样,一看就知道它要干嘛。
另外,如果函数里出现了多个「段落」—— 比如先初始化,再处理,再输出,再清理 —— 那也说明该拆了。
实战:一个典型的反面教材
来看个例子。这是我在代码评审中经常遇到的写法:
// 反面教材:一个函数做了太多事
void handle_sensor_data(void)
{
// 1. 初始化传感器
sensor_init();
delay_ms(10);
// 2. 读取原始数据
uint16_t raw = sensor_read();
// 3. 数据转换
float temp = (float)raw * 0.01f - 50.0f;
// 4. 判断是否超限
if (temp > 85.0f) {
alarm_on();
} else {
alarm_off();
}
// 5. 更新显示
display_show(temp);
// 6. 记录日志
log_write(&temp, sizeof(temp));
}
这个函数干了六件事。你想想看,如果我想单独测试「数据转换」的逻辑,怎么办?只能把整个流程跑一遍。如果显示模块换了,还得改这个函数。
正确的拆分方式
按照单一职责原则,我们应该把它拆成这样:
// 每个函数只做一件事
float sensor_convert_to_temp(uint16_t raw)
{
return (float)raw * 0.01f - 50.0f;
}
bool sensor_is_over_temp(float temp, float threshold)
{
return temp > threshold;
}
void sensor_handle_alarm(float temp)
{
if (sensor_is_over_temp(temp, 85.0f)) {
alarm_on();
} else {
alarm_off();
}
}
void sensor_process(void)
{
// 这里只是「编排」各个步骤,不做具体逻辑
sensor_init();
delay_ms(10);
uint16_t raw = sensor_read();
float temp = sensor_convert_to_temp(raw);
sensor_handle_alarm(temp);
display_show(temp);
log_write(&temp, sizeof(temp));
}
你看,现在每个函数都只做一件事。sensor_convert_to_temp() 只管转换,sensor_is_over_temp() 只管判断,sensor_handle_alarm() 只管报警。而 sensor_process() 只是一个「调度者」,它不参与具体逻辑。
拆分的粒度:拆到什么程度?
有人会问:拆得太细,函数调用开销会不会太大?
嗯,这个问题我经常被问到。我的回答是:先考虑可维护性,再考虑性能。嵌入式系统确实有资源限制,但大多数情况下,函数调用的开销远小于你维护一个 500 行函数的时间成本。
我个人的经验是:
| 函数行数 | 建议 |
|---|---|
| 1-10 行 | 理想状态,一眼能看懂 |
| 10-30 行 | 可以接受,但需要检查是否还能拆分 |
| 30-50 行 | 黄色预警,大概率做了多余的事 |
| 50 行以上 | 红色警报,必须拆分 |
小技巧:如果你发现一个函数里出现了多个「if-else」分支,而且每个分支做的事情完全不同,那基本可以断定这个函数违反了单一职责。可以考虑用「策略模式」或者「状态机」来拆分。
避坑指南:我曾经踩过的坑
我曾经接手过一个项目,里面有个函数叫 system_init(),足足 400 行。它干了什么?初始化时钟、配置 GPIO、设置定时器、校准 ADC、加载配置参数、甚至还包括了自检逻辑。
后来要加一个新功能 —— 在初始化过程中增加一个「等待外部设备就绪」的步骤。我改了这个函数,结果自检逻辑出了问题。排查了半天,才发现是因为我改动了某个变量的初始化顺序。
如果当初把 system_init() 拆成 clock_init()、gpio_init()、timer_init()、adc_calibrate()、config_load()、self_test() 这几个独立函数,每个函数只负责自己的那部分,改动一个就不会影响其他的。
注意:拆分的时候,要小心「隐式依赖」。比如函数 A 和函数 B 都用了同一个全局变量,那它们其实是有耦合的。拆的时候,最好把这种依赖显式地通过参数传递,或者封装成结构体。
总结一下
单一职责原则,说白了就是让每个函数都「专一」一点。你想想看,一个团队里,如果每个人只负责自己擅长的那一块,效率是不是更高?函数也是一样的道理。
我个人的习惯是:写代码之前,先在脑子里过一遍这个函数要干几件事。如果超过三件,就停下来想想怎么拆。拆完之后,每个函数的名字要能准确描述它的职责。
最后送你一句话:好的代码,不是写出来的,是拆出来的。