简介:一份关于软件工程中面向数据流分析方法的PPT课件,适合软件工程学习者及需要掌握需求分析建模的开发者。课件以一个小型图书馆管理系统为贯穿案例,系统讲解抽象、模型、需求分析、逻辑模型与物理模型构建,以及功能需求与非功能需求的划分,内容覆盖从当前系统物理模型到目标系统逻辑模型的完整分析流程。资源为单个PPT文件,压缩包大小约1.41MB,便于下载后直接查看与教学使用。该课件目前已有223人学习,讲解思路清晰,既有基础概念阐释,也有结构化开发方法与面向对象方法的对比,可作为课堂笔记补充或考前复习参考。
1. 面向数据流的分析方法到底是什么:需求频繁变动时为什么反而稳得住
我见过不少项目在需求评审会上反复推翻功能清单,却很少有人注意到:无论功能怎么改,业务里“订单要带着客户信息走一遍审核”“考试成绩要汇总后回写到学籍档案”这类数据关系几乎不变。面向数据流的分析方法(DFD + 数据字典)正是从这条稳健路径出发,把分析重心从“系统要做什么”挪到“数据从哪里来、经过什么转换、到哪里去”。它特别适合早期需求混乱、边界不清的软件工程课程项目,也适合任何想在动手编码前先把数据链路钉死的后端团队。
这篇笔记不打算复述书上的概念,而是按一个可执行的项目推进顺序来讲:先分清三张核心模型,再照着步骤画数据流图,然后把它转成软件结构,最后给出我踩过的坑和排查清单。如果你要拿这套方法做课程设计或生产系统前期分析,照着走就能省掉至少一轮返工。
2. 核心建模工具:DFD、数据字典和它们各自的边界
2.1 数据流图的三层递进:顶层图、0层图、子图
DFD 这套记号一共只有四种元素:外部实体、加工、数据流、数据存储。也正因符号少,团队上手快,但它真正难的是“分层控制”,而不是画箭头。
| 符号 | 名称 | 含义 | 命名要求 |
|---|---|---|---|
| 圆角矩形或圆形 | 加工 Process | 把输入数据变换为输出数据 | 动词短语,如“验证选课资格” |
| 开口矩形 | 数据存储 Data Store | 静止保存的数据集合 | 名词,带 D 编号,如 D2 课程目录 |
| 箭头 | 数据流 | 数据从源到目的地的流动 | 名词,如“选课申请” |
| 矩形 | 外部实体 | 系统边界之外的人、部门或系统 | 名词,如“学生、教务处” |
我一般会给每个加工编一个唯一编号,例如 P1、P2,数据存储也做同样处理,D1、D2。一旦进入子图,编号规则就变成上位加工号加子序号,例如 P1 展开后,子加工就是 P1.1、P1.2。编号不是形式主义,后面把 DFD 映射成软件结构时,每个编号都是一个模块名,错误日志里出现 P1.3 就能直接回溯到设计图。
DFD 的分层通常分三层:
- 顶层图,也叫上下文图:整个系统是一个加工,外部实体通过数据流和系统交换,它只用来定边界。
- 0层图:把系统分解成 3 到 7 个主要加工,画出数据流和存储,是所有讨论的主战场。
- 子图:对 0 层图中某一个加工继续细分,子图编号和图内加工号要沿用父加工编号。
很多第一次画 DFD 的人会犯一个直觉错误:把数据库、打印机、缓存这些支撑组件画成外部实体。外部实体只有一个判断标准——它会不会把数据主动送进系统,或主动接收系统数据。数据库是系统内部的数据存储,不是外部实体。如果把数据库画在系统外面,边界就被戳漏了,后面做安全评审、接口划分时处处对不上。
上下文图画好后,做一次边界确认:把所有外部实体列出来,和用户核对“还有谁会和系统交换数据”。这一步只要多问一句“系统要给谁发通知”,就能筛掉“忘记把短信服务商画进来”这类低级问题。0 层图则是注意力最集中的地方,我要求 0 层图的加工数控制在 3 到 7 个,超过 7 个就让图面失去可读性,也会让后续变换分析找中心变困难。支撑性功能如登录验证、日志记录,先收进加工这个“黑匣子”,细化层再展开,这才是分层的目的。
2.2 数据字典:让图的符号落地成可校验的词条
图给的是骨架,数据字典填的是血肉。数据字典要定义数据流、数据存储、外部实体和加工的输入输出,但真正的重点是“数据流”和“数据存储”的组成。
常用三种组合记号:
- = 表示“定义为”;
- 表示“与”,即组合;
- [ | ] 表示“从多个候选中选择其一”;
- { } 表示“重复一次或多次”。
举个例子:
- 选课申请 = 学号 + 课程编号 + 选课时间
- 成绩单 = 学年学期 + 学号 + {课程名称 + 成绩} + 总评等级
- 学号 = 入学年份 + 学院代码 + 序号
实际项目里,我要求至少给三类条目建字典:系统边界上所有进出数据流(因为这些最终会成为对外接口)、0 层图中所有数据存储(因为它们会成为数据库表或领域实体)、每个子图中的输入输出流(因为它们是模块间传参)。
一条完整的数据流条目建议写成五件套:
| 字段 | 示例 |
|---|---|
| 数据流名 | 选课申请 |
| 来源 | 外部实体·学生 |
| 去向 | 加工·P1 验证选课资格 |
| 组成 | 学号 + 课程编号 + 选课时间 |
| 值域/备注 | 选课时间格式 YYYY-MM-DD HH:MM |
这样的条目有一个直接好处:不同类型的数据流如果名称不同但组成完全相同,可以判断它们是否需要合并成同一张接口。比如“课程信息查询请求”和“课程目录请求”,展开后字段一样,那它们极可能是一回事,应该统一命名,不要为后续接口设计埋下重复定义的坑。
数据字典还必须能反向校验图:图中出现一条数据流,字典里必须有对应条目;字典里新增了字段,图上某个加工必须确实消费了它。这个过程叫“数据字典与 DFD 的一致性检查”,放在每次评审会之前做,而不是评审之后补。
2.3 为什么面向数据流,而不是面向控制流或对象流
选型不是“哪个新用哪个”,而是“哪种视角能扛住需求变动”。面向控制流关注判断顺序,面向对象关注职责协作,面向数据流关注数据变换链路。在实际项目里,这三者也常混用,但如果你只能选一个作为起始建模视角,我会优先推荐数据流,有四个场景依据。
| 判断维度 | 面向数据流(DFD) | 面向对象(用例 + 类图) | 面向控制流(流程图/状态图) |
|---|---|---|---|
| 关注点 | 数据在加工之间的变换 | 对象之间的职责协作 | 状态分支和执行顺序 |
| 对功能需求变动的抵抗力 | 高,增删功能不一定改数据链路 | 中,职责边界会跟着功能变动重构 | 低,分支一变整条链受影响 |
| 下游设计产出 | 结构图/模块划分 | 类图/接口定义 | 代码伪代码 |
| 学习成本 | 低,四个符号就能上纸面 | 中,需要理解抽象、多态、依赖方向 | 低,但容易做成“会动的线框图” |
我常用一个非常朴素的问题来判断:如果用户明天把某个功能的判断顺序改了,你的核心模型会大改吗?面向控制流大概率要挪线,面向对象大概率要改协作,而面向数据流的 DFD 只是加工内部逻辑变一下,外部数据流几乎不动。这就是 DFD 在需求不稳定期的最大优势。
也有一部分团队担心“面向数据流过时了”。我的看法是:DFD 不必取代面向对象,而是给面向对象提供第一批实体和模块。你可以先把 DFD 画清楚,再用类图去细化数据存储,用用例去补加工说明,两者互补而不是对立。真正要避开的是“拿 DFD 当流程图用”,以及“拿流程图当 DFD 用”,这两种画法后面都会在 5 中具体展开。
3. 从需求到数据流图:一套可以照做的建模步骤
3.1 画DFD的完整步骤:从上下文图到细化七步法
先把完整流程列出来,再逐条说明。这套步骤我用了多个课程设计和内部系统项目,整体节奏稳定,新手照着走不会迷路。
- 收集原始需求并批量圈出名词短语;
- 画上下文图;
- 展开 0 层图;
- 补齐数据存储;
- 命名每条数据流;
- 做父子图平衡检查;
- 回填数据字典。
第一步得在需求文档里把名词全圈出来:学生、课程、选课申请、课程容量、选课记录、课表。这些名词通常是外部实体、数据流或数据存储。做完这一步,你已经有了数据字典的骨架目录。注意动词短语也要圈,它们大概率是加工:验证、登记、生成、统计。
第二步画上下文图,只保留一个加工,外部实体只通过数据流和这个加工相连。此时不允许出现数据存储,因为上下文图只表达系统边界和外部交互。外部实体能少就少,建议不超过 5 个,如果超过 5 个,先想想是不是把系统内部组件误认成外部实体了。
第三步展开 0 层图,凭业务场景把系统拆成 3 到 7 个加工。这个拆解不是从代码角度拆,而是从业务步骤角度拆。判断标准是“每个加工是否完成一次身份完整的数据变换”。比如“验证选课资格”完成的是从申请到待选状态的变化,“登记选课记录”完成的是从待选到已选状态的变化。这里的“状态”可以理解为分析层面的状态,而不是代码里的状态机。
第四步补数据存储。从加工里观察“它需要记住什么”。验证资格要读学生档案,登记要读课程目录、写选课记录。数据存储一定由加工通过数据流读写,存储之间不能直接连线,这个规则要画进团队规范里。
第五步命名每条数据流。数据流名字必须是名词,最好的命名方式是“名词 + 结果 / 申请 / 记录”。禁止出现“如果课程满则返回失败”这类带控制语义的名字,因为数据流不承担控制逻辑。
第六步做平衡检查。规则是:父图加工的输入输出数据流,必须与子图的输入输出完全一致;子图内部可以增加内部数据流和数据存储,但不能新增父图没有的外部数据流。这条是 DFD 评审最容易被挑出来的硬伤,我在 5.2 里还会专门讲。
第七步回填数据字典。这一步回到第二步圈出的名词,把每个关键数据流和数据存储的定义补全。很多团队交完 DFD 就默认完成,结果数据结构问题全部留到编码阶段才暴露,后期返工成本极高。
3.2 实操示例:一个在线课程选课系统的分层建模
下面用一个简化但完整的系统走一遍,你可以直接抄这套结构去套自己的业务。
上下文图:
- 外部实体:学生、教务处。
- 系统:选课系统。
- 数据流:
- 学生 → 选课系统:选课申请;
- 选课系统 → 学生:选课结果、个人课表;
- 教务处 → 选课系统:开课计划;
- 选课系统 → 教务处:选课统计表。
0 层图加工分解:
- P1 验证选课资格;
- P2 登记选课记录;
- P3 生成个人课表;
- P4 统计选课情况。
数据存储:
| 编号 | 名称 | 组成示例 |
|---|---|---|
| D1 | 学生档案 | 学号 + 姓名 + 年级 + 培养方案 |
| D2 | 课程目录 | 课程编号 + 课程名称 + 学分 + 课程容量 |
| D3 | 选课记录 | 学号 + 课程编号 + 选课时间 + 审核状态 |
| D4 | 开课计划 | 学期 + 课程编号 + 开课班次 + 任课教师 |
0 层图主要数据流:
| 数据流 | 来源 | 去向 | 组成 |
|---|---|---|---|
| 选课申请 | 外部·学生 | P1 | 学号 + 课程编号 + 选课时间 |
| 合格申请 | P1 | P2 | 学号 + 课程编号 + 资格验证结果 |
| 容量查询 | P2 | D2 | 课程编号 + 查询标识 |
| 选课记录写入 | P2 | D3 | 学号 + 课程编号 + 选课时间 |
| 选课结果 | P2 | 外部·学生 | 课程编号 + 结果状态 + 失败原因 |
| 课表请求 | 外部·学生 | P3 | 学号 + 学期 |
| 个人课表 | P3 | 外部·学生 | 学号 + {课程名称 + 上课时间 + 地点} |
| 计划接收 | 外部·教务处 | P4 | 学期 + {课程编号 + 开课班次} |
| 选课统计表 | P4 | 外部·教务处 | 课程编号 + 选课人数 + 容量利用率 |
再看 P1 的子图。在子图中,P1 被进一步拆成:
- P1.1 读取学生档案;
- P1.2 校验学籍状态;
- P1.3 校验培养方案匹配。
子图输入输出必须和 P1 在父图上的输入输出一致:输入是“选课申请”,输出是“合格申请”,另外会有 P1.1 → D1 的“档案读取”这类子图内部数据流,这是允许的。
而 P3 的数据字典可以这样写:
个人课表 = 学号 + 学期 + {课程名称 + 上课时间 + 上课地点 + 学分}
课表请求 = 学号 + 学期
这样写出来后,后端接口的入参和出参已经天然成型了,后面只需要补响应码。
3.3 DFD 质量自查的四个参数:命名、平衡、最小加工、数据守恒
DFD 画完不是终点,还要用四个参数自查,我把它们做成评审时的标准指标。
命名一致性。加工是动词短语,数据流是名词短语,数据存储是名词。如果同一对象出现两种名字,例如“选课结果”和“选课反馈”,要在数据字典里登记为别名,并选定一个为主名。没有统一命名,结构图阶段会开始出现模块名混乱。
层间平衡。父加工被展开成子图后,子图与父加工交界处的数据流必须完全一致。增删数据流都必须回到父图同步修改。检查方法很简单:把父加工的每条输入输出数据流抄在纸上,再去子图边界逐条对照。
最小加工粒度。一个加工最好只完成一个变换。如果加工说明里出现“同时负责校验、登记、生成通知”,就需要拆。但最小粒度也不能无限细,我的经验阈值是:单个子图内的加工不要超过 7 个,否则图面复杂度和理解成本不成比例上升。
数据守恒。加工不能凭空产生数据,也不能把一个输入字段直接吞掉。应该检查每个输出字段是否能在输入流或数据存储中找到源头。比如 P2 输出“选课结果”中包含课程编号,而输入流“合格申请”里有课程编号,守恒成立。如果输出里有“教师姓名”,但输入和数据存储里根本没有,这就是数据守恒问题,需要在图中加一条从 D4 或 D2 读入教师姓名的数据流。
这四项检查应在评审前由绘图者自检一轮,评审会上再逐项打勾,而不是把问题留给下一阶段。我见过太多项目在编码阶段才发现 DFD 里漏了一条数据流,那时改设计图的成本已经被放大很多。
4. 从数据流图到软件结构:变换分析与事务分析
4.1 变换分析:找中心变换,映射出上层模块
DFD 画完之后,下一步要把它变成软件结构图,最常见的做法是变换分析。变换分析的核心是找出“中心变换”:把输入数据转换成输出数据、并且业务价值最集中的加工。
操作步骤:
- 在 DFD 上划出三条区:输入流区、中心变换区、输出流区。输入流区是从外部实体到中心变换之前的加工链;输出流区是从中心变换到外部实体的加工链。
- 确定顶层控制模块,命名成“某某控制”,例如“选课控制”。
- 输入流区的加工映射为顶层控制模块下的输入侧模块树;输出流区映射为输出侧模块树。
- 中心变换映射为唯一的中心处理模块。
以选课系统 0 层图为例:
- 中心变换是 P2 登记选课记录;
- 输入侧:P1 验证选课资格作为输入处理模块;
- 输出侧:P3 生成个人课表、P4 统计选课情况作为输出处理模块。
结构图可以写成这样:
- 顶层:选课控制;
- 子模块:
- 选课申请接收;
- 资格验证;
- 登记选课记录(中心);
- 生成课表;
- 统计选课。
这个映射过程没有公式,靠的是对业务价值的判断。中心变换找错的典型现象是:结构图顶层模块底下挂了一大堆平行模块,看不出主次,也不容易确定模块间的调用关系。如果发生这种情况,回去重看 DFD,通常是把 P1 到 P4 当成了四个平等模块,而不是“输入-中心-输出”的链式关系。
4.2 事务分析:面对多种事务类型的处理方式
另一类系统是事务中心:一个入口进来,根据事务类型分发到不同处理链。这时候要用事务分析,而不是变换分析。
事务分析步骤:
- 在 DFD 中识别一个“事务分发”加工,它根据输入数据的类型或值域选择后续加工;
- 把事务分发加工作为顶层控制模块下的一个分支控制器;
- 每种事务类型对应一个动作模块,动作模块内部再按变换分析继续细化。
举个例子,订单处理系统:
- 外部实体用户发出“订单请求”;
- 加工 P1 事务分发接收订单请求;
- 根据“订单类型”字段,P1 把数据流分发给:
- P2 处理新订单;
- P3 处理改单;
- P4 处理退单。
这种划分下,结构图为:
- 顶层:订单处理控制;
- 子模块:
- 订单请求接收;
- 事务分发;
- 新订单处理链;
- 改单处理链;
- 退单处理链。
事务分析和变换分析可以组合:分发模块之下,每条处理链内部再走“输入-中心-输出”。实际项目里,一个中大型订单系统通常外层是事务分析框架,内层每个事务类型再应用变换分析。
| 判断信号 | 应选方法 |
|---|---|
| 一条主线数据流顺序穿过多个加工 | 变换分析 |
| 同一入口按类型分发到不同处理链 | 事务分析 |
| 外层有类型分发、内层有主处理链 | 事务分析打框架 + 变换分析细化内链 |
4.3 模块化改进三指标:扇入、扇出和控制范围
结构图出来后还要做一轮调整,三个指标最重要。
扇出指一个模块直接调用的下级模块数量。扇出过高,说明这个模块承担了太多职责。我的容忍上限是 7,超过 7 就在中间加一层控制模块。比如“选课控制”下直接挂 9 个模块,就应该把输入侧模块合并成一个“选课输入处理”。
扇入指一个模块被多少个上层模块调用。扇入高通常是件好事,说明公共逻辑被提取出来了。比如“读取学生档案”被 P1.2 和 P1.3 同时调用,可以提取为公共模块,提高扇入。但如果扇入过高像 10 个以上,要警惕该模块变成集中篡改点,需要检查它的职责是否仍保持单一。
控制范围指一个模块内部判断所影响的下级模块集合。设计目标是:一个模块的判断影响范围尽量局限在该模块和它的直接下级内,不要越过兄弟模块去影响其他分支。否则结构图就会出现“虚线跨层调用”,到编码阶段变成函数之间互相偷调。
这三个指标和 DFD 质量强相关:如果 0 层图加工分得合理,结构图自然扇出适中;如果 DFD 把多个业务步骤塞进一个加工,结构图就会扇出爆炸。所以遇到模块化指标不达标时,我的第一反应是回头改 DFD,而不是硬调结构图。
5. 避坑:数据流分析方法的常见翻车现场与排查清单
5.1 翻车一:把控制流当数据流,DFD 变成流程图
现象:图上出现菱形判断符号;数据流名写成“课程容量是否已满”“校验结果是否通过”;评审时大家能顺着箭头讲故事,但一到数据字典阶段,发现根本没有字段能对得上这些“判断”。
原因:混淆了 DFD 和流程图。DFD 只表达数据变换,不表达两路分支。判断逻辑是加工内部的事情,不属于数据流。
解决:把所有判断结果都包装成数据流字段。比如“容量查询结果”这条数据流,字典定义为“课程编号 + 可选状态 + 剩余容量”,加工内部根据实数值做分支。如果确实要展示判断分支,就另外画一张清晰的控制流程图,放在加工说明里,不要和 DFD 混在同一张图上。
5.2 翻车二:子图与外层不平衡,评审现场对不上号
现象:子图边界多了一条“选课申请表”数据流,父加工上只有“选课申请”;或者子图里写“学籍验证结果”,父图写“资格检查结果”。评审时用户和管理层都按自己习惯叫名字,分析员挨个解释,一场评审变成命名大战。
原因:画子图时新建了数据流,但没有回到父图同步增删;命名没有沿用顶层字典的主名,随手起了新别名。
解决:把平衡检查放到画下一层之前。子图画完后,立刻把父加工的全部输入输出清单抄一次,一行行对照。不一致就直接改,不要拖。命名上强制使用数据字典里的主名,别名只能在字典备注中出现,不能出现在图上。
5.3 翻车三:复合数据流不展开,数据字典形同虚设
现象:图上“选课申请”就一条箭头,字典里写“选课申请 = 选课相关信息”。到编码阶段,后端问接口要哪些字段,前端说“图上不是有吗”,打开图却只有一个名字。
原因:数据字典只定义了数据流名,没有展开组成;复合数据流没有被拆成基础字段;团队对字典的力度没有共识。
解决:给所有系统边界数据流做两层展开。第一层展开到基础字段,例如“选课申请 = 学号 + 课程编号 + 选课时间”;第二层展开基础字段的来源引用,例如“学号引用 D1 学生档案”。这样做之后,接口设计、数据库字段核对、异常信息字段覆盖都变得可以直接从字典抄作业。
5.4 排查清单:一份可以直接用于复审的核对表
每次评审 DFD 前,我用这份清单逐项过。
| 序号 | 检查项 | 判断标准 |
|---|---|---|
| 1 | 加工是否存在孤立加工 | 每个加工至少有一个输入和输出,没有输入或没有输出的都要解释 |
| 2 | 外部实体连接 | 外部实体不能直接连外部实体,不能直接连数据存储 |
| 3 | 存储间连线 | 数据存储之间不能直接有数据流,必经加工 |
| 4 | 父图子图平衡 | 父加工的输入输出与子图边界完全一致 |
| 5 | 编号唯一性 | 全图加工编号、存储编号全局唯一 |
| 6 | 数据流命名 | 所有数据流为名词短语,无“是否”“如果” |
| 7 | 字典覆盖 | 每条数据流和数据存储均有字典条目 |
| 8 | 数据守恒 | 输出字段均在输入流或存储中找到来源 |
| 9 | 最小加工 | 加工说明中不出现“同时负责”多个变换 |
| 10 | 扇出适配 | 0层图加工数量控制在 3 到 7 个 |
这份清单可以直接复制到评审纪要去用。带上它能省掉不少重复解释,也逼着分析员在评审前自己先做一轮硬检查。
6. 进阶:让DFD的数据字典走向接口契约
6.1 给数据流命名并编号,形成模块边界
当 DFD 稳定后,我最推荐做的一步是把数据字典升级成接口契约的初稿。数据流“选课申请”展开后的字段可以直接作为 POST 接口的入参设计,数据流“选课结果”展开后的字段直接作为响应体设计。加工编号 P1.3 可以映射成接口路径或接口名的一部分,比如接口名规范为enrollment.validate,P1.3 对应enrollment.validate.qualification。错误日志里的处理号就能回溯到架构图,排查问题时不用再从代码往上逆推。
6.2 与接口契约、领域模型的衔接方式
DFD 并不老派,把它和面向对象衔接起来会有很好的效果。数据存储 D1 到 D4 可以转成领域实体,但不需要每个存储都建一张表。先看数据字典里的组成,再决定哪些字段属于同一实体、哪些是跨实体的关联字段。数据流中的字段集合可以映射成 DTO,加工说明可以转成服务方法的方法注释。这样从分析到编码,信息只做平移,没有断层。
6.3 我经常犯的错:并行数据流写成串行数据流
进阶部分最后说一个我真实翻车多次的细节。早期画 DFD 时,为了图面整洁,我喜欢把“资格验证”和“课程目录读取”画成一条串行链,导致后来设计并发逻辑时,代码跟着串行走了,吞吐量白白损失一截。后来我纠正了这个习惯:只要两个加工之间没有强依赖,就应该画成两条并行数据流,数据字典里用依赖关系标注先后顺序,而不是在图上强行串起一条线。
这个纠正非常有价值。它看似是画法问题,实际决定了整个系统的性能上限。当你发现一次单纯的数据流重画,能解决团队一周的结构讨论争执时,就会真正感受到这套“面向数据流”分析方法的甜头。希望帮到你。
本文还有配套的精品资源,点击获取