4、高内聚模块设计实战:如何用C语言实现功能内聚模块

好,咱们直接进入正题。上一章讲了内聚性的理论,这一章咱们就撸起袖子,用C语言把功能内聚模块给写出来。

我个人习惯,讲理论的时候喜欢画图,写代码的时候喜欢先定规矩。功能内聚,说白了就是一个模块只干一件事,并且把这件事干彻底。你想想看,如果一个函数又负责读取传感器,又负责滤波,还负责把数据发出去,那这个模块基本就和“可测试”说再见了。

4.1 功能内聚的核心:单一职责的接口设计

先看一个反面教材。我在项目中遇到过这样的代码:

// 反面教材:一个函数干了三件事
int process_sensor_data(void) {
    uint32_t raw = read_adc();          // 1. 读取ADC
    uint32_t filtered = moving_avg(raw); // 2. 滤波
    send_to_can(filtered);               // 3. 发送CAN
    return 0;
}

这个函数看起来挺简洁,对吧?但问题大了。你想单独测试滤波算法?不行,因为它依赖ADC读取和CAN发送。你想模拟ADC输入?得先搭一套硬件环境。这就是典型的“功能混杂”,内聚性极低。

正确的做法是什么?拆!

// 功能内聚模块:每个函数只做一件事
uint32_t sensor_read(void) {
    return read_adc();
}

uint32_t sensor_filter(uint32_t raw) {
    return moving_avg(raw);
}

void sensor_send(uint32_t value) {
    send_to_can(value);
}

嗯,这里要注意:拆完之后,每个函数都可以独立测试。你可以直接给 sensor_filter 传入一组已知数据,验证输出是否正确。这就是功能内聚带来的直接好处——可测试性。

功能内聚的判断标准: 如果你需要“模拟”或“屏蔽”某个外部依赖才能测试这个模块,说明它还不够内聚。

4.2 实战:构建一个温度采集功能内聚模块

咱们用一个完整的例子来走一遍。假设你要做一个温度采集模块,它需要:读取温度传感器、做线性校准、检查是否超限。按照功能内聚原则,我们把它拆成三个独立的模块文件。

4.2.1 模块接口定义(temp_sensor.h)

// temp_sensor.h
#ifndef TEMP_SENSOR_H
#define TEMP_SENSOR_H

#include <stdint.h>
#include <stdbool.h>

// 模块初始化
void temp_sensor_init(void);

// 功能1:读取原始温度值
int16_t temp_sensor_read_raw(void);

// 功能2:校准温度值(线性校准)
int16_t temp_sensor_calibrate(int16_t raw);

// 功能3:超限检测
bool temp_sensor_is_over_limit(int16_t calibrated);

// 获取最终温度(组合函数,但内部仍调用独立功能)
int16_t temp_sensor_get_temperature(void);

#endif

你看,每个函数的名字都清楚地表达了它做什么。这就是功能内聚在接口层面的体现。

4.2.2 模块实现(temp_sensor.c)

// temp_sensor.c
#include "temp_sensor.h"

static int16_t raw_value;
static int16_t calibrated_value;

void temp_sensor_init(void) {
    // 初始化ADC和传感器硬件
    adc_init();
    gpio_set_direction(TEMP_SENSOR_PIN, GPIO_MODE_INPUT);
}

int16_t temp_sensor_read_raw(void) {
    // 只做一件事:读取ADC原始值
    uint32_t adc_val = adc_read(TEMP_SENSOR_CHANNEL);
    return (int16_t)adc_val;
}

int16_t temp_sensor_calibrate(int16_t raw) {
    // 只做一件事:线性校准
    // 校准公式:temp = raw * 0.0625 - 273.15
    // 这里用整数运算避免浮点
    int32_t temp = (int32_t)raw * 625 / 10000 - 27315;
    return (int16_t)(temp / 100);
}

bool temp_sensor_is_over_limit(int16_t calibrated) {
    // 只做一件事:判断是否超限
    return (calibrated > TEMP_HIGH_LIMIT) || (calibrated < TEMP_LOW_LIMIT);
}

int16_t temp_sensor_get_temperature(void) {
    // 组合函数:调用独立功能模块
    raw_value = temp_sensor_read_raw();
    calibrated_value = temp_sensor_calibrate(raw_value);
    return calibrated_value;
}
我的经验: 组合函数(如 temp_sensor_get_temperature)本身也是一个功能内聚的体现——它的职责就是“获取最终温度”,但它内部调用了其他功能内聚的函数。这种层次化的内聚设计,让代码既清晰又灵活。

4.3 功能内聚模块的测试策略

模块写好了,怎么测?这才是咱们课程的重点——可测试性设计。

测试目标 测试方法 测试代码示例
测试校准函数 直接传入已知原始值,验证校准结果 assert(temp_sensor_calibrate(1600) == 27);
测试超限检测 传入边界值和越界值,验证返回布尔值 assert(temp_sensor_is_over_limit(150) == true);
测试读取函数 Mock ADC读取,验证返回值 需要模拟硬件层

我曾经在一个项目中,就是因为没有做功能内聚,导致测试覆盖率死活上不去。后来重构之后,每个函数都能单独写单元测试,覆盖率从30%直接飙到85%。

避坑指南: 我曾经犯过一个错误——在测试组合函数时,试图同时验证内部所有功能。结果测试用例又长又脆弱,稍微改点逻辑就全崩。正确的做法是:只测试组合函数的编排逻辑,内部功能由各自的单元测试覆盖。

4.4 功能内聚模块的知识体系

下面这张图,是我做嵌入式架构设计时经常参考的结构。它把功能内聚模块的整个设计思路串起来了。

功能内聚模块设计知识体系 功能内聚模块设计知识体系 功能内聚模块 单一职责 接口清晰 可独立测试 函数级拆分 模块级封装 组合函数 单元测试 Mock硬件依赖 边界值测试 收益:高可测试性 + 低耦合 + 易维护 核心思想:每个模块只做一件事,并且把这件事做到可以被独立验证

4.5 功能内聚的边界:什么时候该拆,什么时候该合?

你可能会问:是不是所有函数都要拆到最细?当然不是。我见过有人把 a = b + c 都单独写成一个函数,这就过度设计了。

我的判断标准很简单:

  • 需要单独测试的,拆。 比如校准算法、滤波算法、协议解析。
  • 需要复用的,拆。 比如CRC计算、数据格式转换。
  • 逻辑上不可分割的,合。 比如读取寄存器的两个连续字节,拆开反而增加复杂度。
实战口诀: 一个函数如果超过20行,或者有超过2个缩进层级,就考虑拆。拆到每个函数都能用一句话说清楚它做什么。