4. 需求分析与模块划分:如何将需求拆解为模块、接口定义规范、数据流图绘制
很多刚入行的朋友,拿到需求文档就急着写代码。我年轻时也这样,结果呢?代码写到一半发现结构不对,推倒重来。浪费的时间,够我喝好几杯咖啡了。
说白了,需求分析就是搞清楚「到底要做什么」。模块划分就是决定「谁来做哪一块」。这两步走稳了,后面开发就是水到渠成的事。
4.1 需求拆解:从模糊到清晰
客户说「我要一个智能温控系统」。这句话太模糊了。你得追问:控制几个温度点?精度多少?要不要远程控制?有没有报警功能?
我个人习惯,拿到需求先做三件事:
- 划边界:系统跟外界怎么交互?输入是什么?输出是什么?
- 找功能点:把一句话需求拆成一个个可验证的功能。
- 定优先级:哪些是核心功能,哪些是锦上添花。
举个例子:一个智能温控器需求
- 核心功能:温度采集、PID控制、过温保护
- 扩展功能:WiFi联网、手机App查看、历史曲线
- 边界条件:供电电压范围、工作温度范围、EMC等级
我在项目中遇到过最坑的事,就是客户说「先做基本功能」,结果验收时把扩展功能当必须项。所以,需求一定要白纸黑字写下来,双方签字。
4.2 模块划分:高内聚、低耦合
模块划分的原则,圈内人都知道:高内聚、低耦合。但怎么落地?我分享几个实战经验。
4.2.1 划分方法
| 方法 | 适用场景 | 我的经验 |
|---|---|---|
| 按功能划分 | 功能边界清晰 | 最常用,比如传感器模块、通信模块 |
| 按层次划分 | 分层架构(如OSI模型) | 适合复杂系统,每层职责明确 |
| 按数据流划分 | 数据处理流程明显 | 信号处理类项目首选 |
我个人偏爱功能+层次混合划分。比如一个数据采集系统:
- 硬件抽象层:封装ADC、SPI、I2C等外设操作
- 数据处理层:滤波、校准、单位转换
- 应用层:逻辑判断、报警、显示
- 通信层:协议封装、数据收发
小技巧:每个模块的代码量控制在300-500行。超过这个数,就该考虑拆分了。这是我在一个3000行单文件项目里学到的教训。
4.3 接口定义规范:模块间的契约
模块划分好了,接口就是它们之间的「合同」。合同写不清楚,后面联调就是灾难。
4.3.1 接口三要素
- 函数原型:函数名、参数、返回值
- 数据结构:结构体定义、枚举值
- 时序约束:调用顺序、超时时间、重入性
我曾经在一个项目里,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 绘制步骤
- 画顶层图:系统跟外部实体的数据交互
- 逐层细化:把每个处理过程拆成子图
- 标注数据:每条数据流都要有名字和类型
下面是我为一个温控系统画的顶层数据流图。你看一眼就明白数据怎么走了。
画完顶层图,再对「温控系统」这个方框做细化。比如拆成采集模块、控制模块、通信模块,每个模块再画子图。这样一层层下去,直到每个处理过程都能对应到一段代码。
工具推荐:我习惯用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。这是我从无数个加班夜里总结出来的真理。