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 功能内聚模块的知识体系
下面这张图,是我做嵌入式架构设计时经常参考的结构。它把功能内聚模块的整个设计思路串起来了。
4.5 功能内聚的边界:什么时候该拆,什么时候该合?
你可能会问:是不是所有函数都要拆到最细?当然不是。我见过有人把 a = b + c 都单独写成一个函数,这就过度设计了。
我的判断标准很简单:
- 需要单独测试的,拆。 比如校准算法、滤波算法、协议解析。
- 需要复用的,拆。 比如CRC计算、数据格式转换。
- 逻辑上不可分割的,合。 比如读取寄存器的两个连续字节,拆开反而增加复杂度。
实战口诀: 一个函数如果超过20行,或者有超过2个缩进层级,就考虑拆。拆到每个函数都能用一句话说清楚它做什么。