4. 重构前评估方法

重构最怕什么?最怕你改了一堆代码,最后说不清楚到底改好了没有。

我见过太多团队,重构前热血沸腾,重构后一地鸡毛。为什么?因为目标没定清楚。你连「什么是好」都没定义,怎么知道做完了?

这一章,我们就来聊聊怎么把重构目标从「感觉」变成「数字」。

4.1 接口边界:先画线,再动手

接口边界,说白了就是「谁该管什么,谁不该管什么」。我个人的习惯是:先画图,再写代码。画图不是画 UML 那种大而全的,而是画模块之间的通信线。

核心原则:接口是契约,不是实现。接口只告诉别人「我能做什么」,不告诉别人「我怎么做的」。

举个例子,一个温控模块的接口应该长这样:

/* 好的接口:只暴露行为,不暴露内部 */
typedef struct {
    float   (*get_temperature)(void);
    int32_t (*set_target)(float temp);
    bool    (*is_stable)(void);
} temp_controller_if_t;

而不是这样:

/* 坏的接口:把内部实现细节全暴露了 */
typedef struct {
    uint32_t adc_value;          /* 内部ADC值 */
    float    pid_kp;             /* PID参数 */
    uint8_t  heater_pwm;         /* 加热器PWM */
    void     (*calibrate)(void); /* 校准函数,调用方根本不需要知道 */
} temp_controller_t;

我在项目中遇到过一件事:一个同事把 SPI 的寄存器地址直接写在接口头文件里,结果换了芯片型号,所有调用方都得改。这就是接口边界没划清楚。

避坑指南:我曾经在一个项目里,接口头文件包含了硬件寄存器定义。后来换了 MCU,改了一个月。从那以后,我定了一条铁律:接口头文件里,不准出现任何硬件相关的宏或类型定义。

4.2 模块划分:高内聚、低耦合,但别过度

模块划分这件事,很多人一上来就想着「我要拆成 20 个模块」。其实没必要。模块划分的核心就两句话:

  • 一个模块只做一件事(高内聚)
  • 模块之间只通过接口通信(低耦合)

我一般按这个步骤来划分:

  1. 先列功能清单:把系统所有功能写出来,别管模块,先列全。
  2. 找自然边界:哪些功能经常一起变?哪些功能依赖同一个硬件资源?
  3. 画依赖图:模块 A 调用模块 B,箭头别画反了。
  4. 检查循环依赖:如果有 A→B→C→A,说明边界没划对。

注意:模块不是越细越好。拆得太碎,接口数量爆炸,维护成本反而更高。我见过一个项目,一个传感器驱动拆了 5 个文件,最后没人看得懂调用关系。

下面这张图,是我常用的模块划分参考结构:

嵌入式系统模块划分参考结构 应用层(Application Layer) 业务逻辑、状态机、用户交互 服务层(Service Layer) 协议栈、数据管理、任务调度 硬件抽象层(HAL) GPIO、UART、SPI、I2C、Timer 抽象接口 驱动层(Driver Layer) 关键规则:上层只能调用相邻下层,不允许跨层调用

4.3 性能指标:用数字说话

重构之后,系统是变快了还是变慢了?你得有数据。我一般会定这几个指标:

指标类别 具体指标 测量方法 典型目标
响应时间 中断响应延迟、任务切换时间 GPIO 示波器打点 < 10 μs
吞吐量 每秒处理消息数、数据采样率 计数 + 定时器 ≥ 1000 msg/s
内存占用 RAM 峰值、Flash 占用 编译器 map 文件 ≤ 总资源 70%
CPU 负载 空闲任务占比 RTOS 自带统计 ≥ 30% 空闲

个人经验:我习惯在重构前先跑一遍性能基线,把数据记下来。重构过程中每改完一个模块,就跑一遍对比。这样能及时发现「改坏了」的情况。

举个例子,我曾经重构一个 CAN 通信模块。重构前,发送一条消息平均耗时 2.3 ms。重构后,我测出来是 1.1 ms。数据摆在那,谁都没话说。

4.4 可测试性目标:让测试成为习惯

很多嵌入式工程师觉得测试是 QA 的事。其实不对。可测试性,是设计出来的。

我定可测试性目标时,会关注这几点:

  • 接口可模拟:每个模块的接口,都能用桩函数(stub)替换。
  • 状态可观测:模块内部的关键状态,能通过接口读出来。
  • 依赖可注入:模块依赖的外部资源(如定时器、存储),能通过参数传入。

一个简单的可测试性检查清单:

  • ✅ 每个模块的接口是否独立?
  • ✅ 能否在 PC 上编译运行单元测试?
  • ✅ 模块内部是否有全局变量?如果有,能否通过接口访问?
  • ✅ 硬件依赖是否被抽象到了 HAL 层?

我见过一个项目,所有模块都直接调用 HAL 的 GPIO 写函数。想测一个业务逻辑,得先接上硬件。后来我把 HAL 接口改成了函数指针表,测试时传入模拟函数,问题就解决了。

注意:可测试性不是一蹴而就的。我建议在重构初期就定好目标,比如「每个模块至少有一个单元测试」。别想着一次到位,先跑起来再说。

4.5 目标设定清单:照着做就行

最后,我整理了一份目标设定清单。每次重构前,我都会过一遍:

维度 目标 验收标准
接口边界 接口不暴露实现细节 头文件中无硬件宏、无全局变量
模块划分 无循环依赖 依赖图检查通过
性能指标 响应时间 < 10 μs 示波器测量 100 次取平均
可测试性 每个模块可独立测试 单元测试覆盖率 ≥ 80%