法则二:单一职责原则 — 一个函数只做一件事

各位工程师,咱们今天聊聊函数拆分。说实话,我见过太多人把函数写成「万能工具箱」—— 一个函数里塞了初始化、数据处理、状态判断、甚至还有打印日志。结果呢?改一个地方,崩三个功能。

单一职责原则,说白了就是:一个函数,只干一件事,并且把它干好。听起来简单,做起来可没那么容易。

为什么非要拆?

我刚开始写嵌入式代码那会儿,也喜欢把功能堆在一起。觉得函数多了调用麻烦,不如一个函数搞定。直到有一次,我在一个 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 都用了同一个全局变量,那它们其实是有耦合的。拆的时候,最好把这种依赖显式地通过参数传递,或者封装成结构体。

总结一下

单一职责原则,说白了就是让每个函数都「专一」一点。你想想看,一个团队里,如果每个人只负责自己擅长的那一块,效率是不是更高?函数也是一样的道理。

我个人的习惯是:写代码之前,先在脑子里过一遍这个函数要干几件事。如果超过三件,就停下来想想怎么拆。拆完之后,每个函数的名字要能准确描述它的职责。

最后送你一句话:好的代码,不是写出来的,是拆出来的