30、函数设计最佳实践:函数单一职责原则,函数命名规范,参数个数控制,代码复用与重构。

好,咱们今天聊点实在的。函数怎么写才算「好」?

我见过太多代码,一个函数动辄几百行,变量命名全靠拼音首字母,参数列表长得像购物清单。这种代码,写的时候很爽,改的时候想哭。你想想看,是不是这个理?

这一讲,我就把我在嵌入式项目里摸爬滚打总结出来的几条铁律,掰开了揉碎了讲给你听。说白了,就是四个字:整洁、克制

核心观点:函数是代码的最小组织单元。它的质量,直接决定了整个系统的可维护性和可测试性。别小看一个函数,它背后藏着工程师的全部修养。

30.1 单一职责原则:一个函数,只做一件事

这是最基础,也最容易被打破的原则。什么叫「一件事」?我举个例子。

你写了一个函数,叫 process_sensor_data()。它干了什么?读取传感器、滤波、计算平均值、更新全局状态、还要往串口打印日志。这算几件事?至少五件。

我个人习惯是:如果一个函数,你没办法用一句话说清楚它干了什么,那它一定干了太多事。

来看个反面教材:

// 反面示例:一个函数干了太多事
void handle_temperature(void) {
    uint32_t raw = read_adc(ADC_CH_TEMP);   // 读ADC
    float temp = (float)raw * 0.125f - 50.0f; // 转换公式
    if (temp > 85.0f) {
        set_alarm(ALARM_OVERHEAT);           // 触发报警
        fan_on();                            // 开风扇
    }
    printf("Temp: %.2f\n", temp);            // 打印日志
    update_display(temp);                    // 更新屏幕
}

这个函数,读数据、算温度、判断阈值、控制风扇、打印日志、更新显示——全搅在一起。如果哪天你想把打印日志去掉,或者换一种报警方式,你都得动这个函数。牵一发动全身。

我建议改成这样:

// 推荐:每个函数只做一件事
float read_temperature(void) {
    uint32_t raw = read_adc(ADC_CH_TEMP);
    return (float)raw * 0.125f - 50.0f;
}

bool is_overheat(float temp) {
    return temp > 85.0f;
}

void handle_overheat(void) {
    set_alarm(ALARM_OVERHEAT);
    fan_on();
}

void log_temperature(float temp) {
    printf("Temp: %.2f\n", temp);
}

// 主控逻辑:只负责编排,不负责实现细节
void temperature_task(void) {
    float temp = read_temperature();
    log_temperature(temp);
    if (is_overheat(temp)) {
        handle_overheat();
    }
    update_display(temp);
}

你看,每个函数都只做一件事。名字就是它的说明书。测试也好,修改也好,都变得非常清晰。

我的经验:在嵌入式系统里,单一职责还有一个隐藏好处——方便做单元测试。每个小函数都可以单独拿出来验证,不用搭整个硬件环境。我曾经在一个电机控制项目里,把一个大函数拆成7个小函数,测试覆盖率从30%直接干到95%。

30.2 函数命名规范:名字就是最好的注释

我见过最离谱的命名是什么?func1()deal()handle()。你问我这函数干嘛的?我只能去看代码。

命名这件事,其实有套路。我总结了三句话:

  • 动词开头,表明动作。比如 read_sensor()set_speed()enable_interrupt()
  • 返回值类型暗示在名字里。如果是判断,用 is_has_should_ 开头。比如 is_buffer_full()has_data()
  • 不要缩写,除非是行业共识。 tmp 可以,tmprtr 不行。 cnt 可以,cou 不行。

来看个对比:

不好的命名 好的命名 说明
deal() process_uart_rx() 明确处理的是UART接收
check() is_checksum_valid() 明确检查的是校验和
set() set_pwm_duty_cycle() 明确设置的是PWM占空比
get() get_system_tick_ms() 明确获取的是系统滴答值

注意:我曾经在一个项目里看到有人用 aaa()bbb() 来命名函数,说是「临时用一下,后面再改」。结果这个项目维护了三年,这些名字一直没改。嗯,后来接手的人骂了三年。

30.3 参数个数控制:超过3个,就该想想了

函数参数越多,调用者越容易犯错。你想想看,一个函数有6个参数,调用的时候你得一个一个对位置,少传一个、传错一个,编译器都不一定报错。

我给自己定了个规矩:参数超过3个,就用结构体。

来看个例子:

// 坏味道:参数太多
void config_timer(uint32_t period, uint32_t prescaler, uint8_t mode, 
                  bool enable_interrupt, void (*callback)(void), uint8_t priority);

// 好做法:用结构体封装
typedef struct {
    uint32_t period;
    uint32_t prescaler;
    uint8_t  mode;
    bool     enable_interrupt;
    void     (*callback)(void);
    uint8_t  priority;
} timer_config_t;

void config_timer(timer_config_t *cfg);

这样做的好处是什么?

  • 调用的时候,可以指定成员名赋值,不容易搞混。
  • 以后要加参数,只需要在结构体里加字段,函数接口不用变。
  • 结构体可以复用,比如多个定时器共用一套配置模板。

我的习惯:如果参数是0个或1个,直接用。2到3个,看情况。超过3个,强制用结构体。这个习惯帮我避免了很多低级bug。有一次我传参顺序搞反了,结构体方式直接让编译器帮我查出来了。

30.4 代码复用与重构:别重复你自己

DRY 原则——Don't Repeat Yourself。听起来简单,做起来难。

我见过最夸张的代码,一个项目里同样的 crc16 计算函数写了5遍,每遍还略有不同。为什么?因为不同的人在不同的模块里各自实现了一遍。

重构的核心思路是什么?我总结了三步:

  1. 识别重复。看到两段代码长得像,先别急着复制粘贴。停下来想一想,能不能抽象成一个公共函数?
  2. 提取共性。把重复的部分抽出来,不同的部分用参数控制。
  3. 测试验证。重构完一定要跑一遍测试,确保行为没变。

来看一个实际例子。假设你有两个函数,一个读温度,一个读湿度:

// 重复代码
float read_temperature(void) {
    uint32_t raw = read_adc(ADC_CH_TEMP);
    return (float)raw * 0.125f - 50.0f;
}

float read_humidity(void) {
    uint32_t raw = read_adc(ADC_CH_HUM);
    return (float)raw * 0.1f + 20.0f;
}

重构之后:

// 提取公共逻辑
typedef struct {
    uint8_t  adc_channel;
    float    scale;
    float    offset;
} sensor_calib_t;

float read_sensor(sensor_calib_t *calib) {
    uint32_t raw = read_adc(calib->adc_channel);
    return (float)raw * calib->scale + calib->offset;
}

// 使用时
static sensor_calib_t temp_calib = {ADC_CH_TEMP, 0.125f, -50.0f};
static sensor_calib_t hum_calib  = {ADC_CH_HUM,  0.1f,   20.0f};

float temp = read_sensor(&temp_calib);
float hum  = read_sensor(&hum_calib);

你看,代码量没少多少,但逻辑清晰了。以后要加新的传感器,只需要定义一个新的校准结构体,不用再写一遍读取逻辑。

避坑指南:重构的时候,千万别想着「顺便改点功能」。重构只改变结构,不改变行为。我曾经有一次重构时顺手改了一个阈值,结果整个系统的行为都变了,排查了两天才找到原因。记住:重构和加功能,分开做。

30.5 知识体系总览

下面这张图,我把这一讲的核心逻辑串起来了。你可以把它当作函数设计的「检查清单」。

函数设计最佳实践 · 知识体系 函数设计核心 单一职责原则 函数命名规范 参数个数控制 代码复用与重构 一个函数只做一件事 一句话说清函数功能 便于单元测试 动词开头表明动作 is_/has_/should_ 前缀 避免模糊缩写 不超过3个参数 多用结构体封装 接口稳定易扩展 识别重复代码 提取公共逻辑 重构不改行为 最终目标:可读、可测、可维护 写代码是给人看的,顺便给机器执行

这张图把四个核心原则串在了一起。你写每个函数之前,都可以拿这张图过一遍:这个函数是不是只做一件事?名字能不能让人一眼看懂?参数是不是太多了?有没有重复的代码可以抽出来?

养成这个习惯,你的代码质量会有质的提升。


总结一句话:好的函数设计,不是为了炫技,而是为了让你自己——以及以后接手你代码的人——少掉几根头发。

公众号:蓝海资料掘金营,微信deep3321