4. 需求分析与模块划分:如何将需求拆解为模块、接口定义规范、数据流图绘制

很多刚入行的朋友,拿到需求文档就急着写代码。我年轻时也这样,结果呢?代码写到一半发现结构不对,推倒重来。浪费的时间,够我喝好几杯咖啡了。

说白了,需求分析就是搞清楚「到底要做什么」。模块划分就是决定「谁来做哪一块」。这两步走稳了,后面开发就是水到渠成的事。

4.1 需求拆解:从模糊到清晰

客户说「我要一个智能温控系统」。这句话太模糊了。你得追问:控制几个温度点?精度多少?要不要远程控制?有没有报警功能?

我个人习惯,拿到需求先做三件事:

  1. 划边界:系统跟外界怎么交互?输入是什么?输出是什么?
  2. 找功能点:把一句话需求拆成一个个可验证的功能。
  3. 定优先级:哪些是核心功能,哪些是锦上添花。

举个例子:一个智能温控器需求

  • 核心功能:温度采集、PID控制、过温保护
  • 扩展功能:WiFi联网、手机App查看、历史曲线
  • 边界条件:供电电压范围、工作温度范围、EMC等级

我在项目中遇到过最坑的事,就是客户说「先做基本功能」,结果验收时把扩展功能当必须项。所以,需求一定要白纸黑字写下来,双方签字。

4.2 模块划分:高内聚、低耦合

模块划分的原则,圈内人都知道:高内聚、低耦合。但怎么落地?我分享几个实战经验。

4.2.1 划分方法

方法 适用场景 我的经验
按功能划分 功能边界清晰 最常用,比如传感器模块、通信模块
按层次划分 分层架构(如OSI模型) 适合复杂系统,每层职责明确
按数据流划分 数据处理流程明显 信号处理类项目首选

我个人偏爱功能+层次混合划分。比如一个数据采集系统:

  • 硬件抽象层:封装ADC、SPI、I2C等外设操作
  • 数据处理层:滤波、校准、单位转换
  • 应用层:逻辑判断、报警、显示
  • 通信层:协议封装、数据收发

小技巧:每个模块的代码量控制在300-500行。超过这个数,就该考虑拆分了。这是我在一个3000行单文件项目里学到的教训。

4.3 接口定义规范:模块间的契约

模块划分好了,接口就是它们之间的「合同」。合同写不清楚,后面联调就是灾难。

4.3.1 接口三要素

  1. 函数原型:函数名、参数、返回值
  2. 数据结构:结构体定义、枚举值
  3. 时序约束:调用顺序、超时时间、重入性

我曾经在一个项目里,A模块调用B模块的函数,结果B模块内部有延时,导致A模块的实时性崩溃。这就是接口文档里没写时序约束的后果。

4.3.2 命名规范

我建议统一用模块名_动作_对象的格式:

// 好的命名
temp_sensor_init()
temp_sensor_read()
temp_sensor_calibrate()

// 不好的命名
init()
read_temp()
do_cal()

注意:接口参数尽量用结构体指针,别用全局变量。全局变量是魔鬼,调试时你根本不知道谁改了它。

4.4 数据流图绘制:让数据流动起来

数据流图(DFD)是模块划分的视觉化工具。它不画控制逻辑,只画数据怎么流动、怎么变换。

4.4.1 基本元素

符号 含义 例子
圆/椭圆 外部实体 用户、传感器、云端
方框 处理过程 滤波、校准、存储
箭头 数据流 温度值、控制指令
双横线 数据存储 数据库、文件、缓存

4.4.2 绘制步骤

  1. 画顶层图:系统跟外部实体的数据交互
  2. 逐层细化:把每个处理过程拆成子图
  3. 标注数据:每条数据流都要有名字和类型

下面是我为一个温控系统画的顶层数据流图。你看一眼就明白数据怎么走了。

温控系统顶层数据流图 温度传感器 用户 温控系统 采集/控制/通信 加热器 云端服务器 温度值(float) 目标温度(float) PWM占空比(uint8) JSON数据包 配置参数存储 读写参数 说明:箭头方向表示数据流动方向,每条数据流都标注了数据类型 顶层图只画系统与外部实体的交互,内部细节在子图中展开

画完顶层图,再对「温控系统」这个方框做细化。比如拆成采集模块、控制模块、通信模块,每个模块再画子图。这样一层层下去,直到每个处理过程都能对应到一段代码。

工具推荐:我习惯用draw.io画数据流图,免费且支持团队协作。画图时记住一条原则——一个处理过程只做一件事。如果发现一个方框里塞了太多逻辑,说明模块划分有问题。

4.5 实战案例:智能温控器模块划分

说了这么多理论,咱们看个完整的例子。一个智能温控器,需求如下:

  • 采集NTC温度,精度±0.5°C
  • PID控制加热器,目标温度可设
  • 过温保护,超过80°C自动断电
  • 通过UART上报数据到上位机
  • 掉电保存目标温度

我把它拆成这几个模块:

模块名 职责 接口函数
temp_sensor NTC采集、查表转换 temp_sensor_init(), temp_sensor_read()
pid_ctrl PID计算、输出限幅 pid_init(), pid_calc()
over_temp_protect 过温检测、紧急断电 otp_check(), otp_shutdown()
uart_comm 协议封装、数据收发 uart_send(), uart_recv()
nv_storage EEPROM读写、校验 nv_write(), nv_read()

关键点:每个模块的.h文件里只暴露必要的接口函数。内部实现细节用static隐藏。这样别人用你的模块时,只需要看头文件,不用关心里面怎么实现的。

嗯,这里要注意:模块之间的依赖关系要画清楚。比如pid_ctrl依赖temp_sensor提供温度值,uart_comm依赖nv_storage读取配置。这些依赖关系在接口文档里要写明白,不然联调时你会被各种「undefined reference」折磨。

我曾经在一个项目里,因为没画清楚依赖关系,结果两个模块互相调用,形成了循环依赖。编译没问题,但一运行就死机。查了两天才发现是初始化顺序的问题。从那以后,我每个项目都先画模块依赖图。

好了,需求分析和模块划分就聊到这儿。记住一句话:前期多花一小时画图,后期省一天时间改bug。这是我从无数个加班夜里总结出来的真理。