作为一名常年跟需求文档和系统设计图打交道的软件工程师,我可以说:DFD数据流图是结构化分析中最“耐看”也最容易“画废”的一种模型。只要同时拿三份不同人画的订单系统DFD放在一起,大概率会看到完全不同的加工编号、五花八门的命名,以及层级之间根本对不上的数据流。前段时间评审一份库存管理需求时,我盯着投影上那张“顶层图”看了整整二十分钟,大家最后只能得出一个结论——不如用文字写清楚。问题并不出在DFD这种表达工具本身,而是很多人在建模时只关注“图好看”,忽略了结构化分析对DFD的硬性约束。今天这篇就想把这件事说透:DFD建模到底有哪些核心原则,它们又是怎么从逻辑一致性、可追溯性、工程实用性三个方向,把一张张“示意图”变成真正的工程模型的。
1. 先回答一个问题:DFD建模到底在保什么
1.1 逻辑一致性:层与层、流与流不许打架
很多人把DFD单纯理解成“业务流程的数据版”,画完顶层图画底层图,感觉对得上就行。实际上,结构化分析中的DFD是一套分层逻辑模型,它最底层的约束就是逻辑一致性。什么叫“打架”?我举几个真实场景:父图中某个加工明明只接收了“订单信息”这一条输入,子图里却凭空出现了一个从“支付网关”指向同一加工的数据流;或者父图里加工2对外输出的是“有效订单”,到了子图里同样的位置却变成了“有效订单+客户等级信息”。这两种情况在评审时大家往往盯着图看半天才发现,可一旦用分析工具自动做父子图数据流比对,全部一秒暴露。
逻辑一致性不只是“对得上”,它还包括:同一个数据流在整个分层结构里名称和内容保持统一;同一个数据存储在不同层次引用时不换名字;外部实体出现在某一层图中后,不能在该层之外又被当作加工来画。这些规则保证了整棵DFD树在任何一层去看,用的都是同一套“语言”。如果你画DFD却允许不同人给同一个数据流起不同的名字,那这套模型就无法被机器检查,也无法被严谨的人阅读,最后就沦为了PPT素材。
1.2 可追溯性与工程实用性:从分析模型到物理实现的桥梁
可追溯性解决的是“为什么这里有个加工”“这条数据流是从哪条业务需求派生出来”的问题。在建大型系统时,需求分析师画完DFD,设计人员要依据它做模块划分,测试人员要根据它做用例分析,维护人员要顺着它定位变更影响范围。如果模型本身不可追溯,每个人就只能靠猜。
工程实用性更实在:DFD最终要被翻译成系统设计,比如模块结构图、数据库关系、接口定义。结构化分析与结构化设计之所以在上世纪能形成完整方法体系,核心就是DFD和模块结构图之间存在一种近乎一一对应的映射关系——高内聚的加工可以直接对应一个模块,而数据存储往往对应数据库中的实体集合。如果加工划分太粗,后续设计无从下手;加工划分太碎,设计文档数量爆炸。说白了,DFD建模的四条基本原则,每一次权衡都是在为下游铺路。接下来我按这四条原则逐条拆,每条都会结合我自己的实操经验来讲。
2. 原则一:自顶向下逐层分解,让每个层次都有明确的“边界感”
2.1 上下文图:先锁定系统的全部外部交互
自顶向下分解的第一个产物就是上下文图,也叫顶层图或0层图。这张图只有一个加工,代表整个待开发系统;围绕它的全部外部实体,则是系统边界外的角色、组织或系统。上下文图最大的价值,是逼你在动笔画任何内部逻辑之前,先把“系统算什么、外部有什么”说清楚。这个过程听起来简单,做起来非常容易出错。
我举一个在线订单处理的例子。大多数新手画上下文图时会这样:左边画一个“客户”,右边画一个“管理员”,然后从“客户”拉一条线到“订单系统”,再拉一条线出来,就完事。但你只要稍微多问两句就会发现外部交互远不止这些:订单系统要不要跟支付网关交互?要不要跟库存系统同步?短信或邮件通知服务算不算外部实体?这些东西画漏了,后面所有层级的图都会跟着错。
实操上我习惯用一个笨办法:把所有可能需要的数据来源和数据去向全部写出来,再逐个判断“这个数据是系统内部的加工产物,还是外部角色的主动输入/被动输出”。拿不准的,宁可多画一个外部实体,也不要漏画。因为在上下文图上多一个外部实体,最多就是后来评审时被删掉;漏画一个,等于让整个系统白做了一个外部接口。
2.2 分解粒度:什么时候该停手,什么时候必须继续
确定了上下文图之后,接下来的核心工作就是“把加工逐个展开成子图”。这里最考验经验的是分解粒度。教科书通常说“每个加工应能用一个模块或一个程序实现”,但这句话太抽象,实际画图时完全无法直接操作。
我自己的判断标准有三个。第一,加工的名字是不是一个“动词+宾语”的完整业务动作,比如“校验订单信息”“计算订单金额”,如果名字本身就含糊不清,说明内部可能混了多个逻辑,需要继续分解。第二,这个加工有没有清晰的输入、输出、存储读写,如果画完之后你发现自己要额外补充“这个加工其实还需要从某个存储读数据”,那说明当前粒度不对。第三,子图展开后加工数量大致控制在4到9个,超过9个,说明上一层分解得太粗,应该先把若干加工合并到一个父加工里;少于4个,通常说明这个中间层没有存在的必要,可以考虑砍掉这一层直接深化。
这里要特别提醒:很多人会把“编号”当作分解的附属品,随手标一个1、2、3,甚至用系统自动生成的ID。编号其实是你日后做可追溯性追踪的关键锚点。我建议在开始分解之前就先规划好三层以内编号结构,比如0层图加工编号1、2、3,1号加工的子图加工编号1.1、1.2、1.3,再往下就是1.1.1、1.1.2。这样做的好处是,任何后续人员拿到任意一张子图,都可以通过编号立刻知道它在整棵DFD树的什么位置,以及它的父加工是谁。
2.3 父子平衡:分层正确性的硬性检验
自顶向下分解有一个无法回避的规则,叫父子图平衡,也叫数据流平衡。它说的是:父图中某个加工的输入、输出数据流,必须与它的子图边界上的输入、输出数据流完全一致。注意是“完全一致”——不只是数量一致,每个数据流的名称、方向、语义都必须对得上。
为什么这条规则如此重要?因为分解的过程本质上是在“放大”一个加工,而不是“修改”一个加工。如果你在放大过程中给这个加工新增了输入或输出,那说明你对这个加工的业务含义理解发生了变化,等于是在父图之外悄悄发明了一套新逻辑。这种增量可能当时觉得理所当然,等画到第三层、第四层,整张DFD已经变成一张“双层的矛盾图”,后续需求变更时拿它做影响分析,结论一定是错的。
做平衡检查时,通常要注意一种容易被忽略的情况:子图边界上新增了“存储读取”数据流。父图的加工旁边如果画了一个数据存储,并没有用一条数据流显式连接存储和加工,那子图里却出现了一个加工直接读写该存储。这种“存储访问”要不要算作平衡关系?我在项目里遇到的绝大多数经验丰富的分析师,都会把存储访问视作加工对存储的隐式数据流,并要求子图中的存储读写与父图中该加工对应的存储引用保持一致。如果父图根本没画这个存储,子图里却出现了,就属于越权修改,需要回到父图补充或者调整子图。
分层分解还有个附带好处:评审会效率大幅提升。讨论顶层图时,各角色只需关注系统边界和外部接口;讨论某一层子图时,只需让相关模块负责人到场。这比拿着一整张大图从头讲到尾要高效得多,也是“工程实用性”的体现。
3. 原则二:数据守恒,数据流不能无中生有也不能凭空消失
3.1 加工必须有合法的输入与输出
第二条核心原则是数据守恒。听起来像物理定律,放在DFD里一点都不过分:数据流不能无中生有,也不能凭空消失,每个加工都必须有与业务语义相匹配的输入数据流和输出数据流。
具体我会从三种典型的违规形态去查图:
- 黑洞加工:只有输入,没有输出。数据流进去之后不知所终。常见于画图时忘了画输出,但真正需要警惕的是业务逻辑本身有缺陷,比如某个加工确实只把数据写进存储,却没有定义后续任何人怎么读取它。
- 奇迹加工:只有输出,没有输入。加工凭空“变”出数据流。最常见的原因是漏画了外部实体到加工之间的数据流,或者是画图时把一个存储读取遗漏了。
- 灰洞/灰洞加工:输入不足以支撑输出。例如输入是“订单号”,输出却是“完整订单详情+客户信用额度”,中间没有从订单存储或客户存储读取任何数据的流,这就是典型的不守恒。
我用“订单处理系统”第三层子图举个具体例子。加工“检查库存”接收“库存查询请求”,按理说它必须去读取“库存存储”,然后输出“库存状态”。如果画出来的图里,“库存存储”压根没出现在这张子图上,而“库存状态”却直挺挺地画在加工的输出上,那这个加工就是“奇迹加工”。懂业务的人当然知道检查库存要查库存表,可图一旦这么画,等系统设计阶段做模块接口定义时,负责设计服务的人可能就不会去设计专门的读库存接口。
3.2 存储读写的守恒检查
数据守恒除了约束“数据流要不要存在”,还约束“数据存储的读写是否自洽”。我常说一句话:存储是数据流的“水库”,你可以绕过它,但不能长期不补给它或完全不泄洪。具体检查时,对每一个数据存储,至少要回答三个问题:
- 有哪些加工向这个存储写入数据?
- 有哪些加工从这个存储读取数据?
- 写入与读取是否构成完整的数据生命周期?
还是拿订单系统说。订单存储如果只有“生成订单”这个加工在写,没有任何加工读它,那这张DFD一定会被下游开发挑战:订单存进去干什么用?反之,如果“订单分析”加工经常读订单存储,但图上没有任何一个加工向订单存储写过数据,那就是典型的“奇迹存储”。另外,不要出现同一个数据名在两张子图里指向不同的存储实现,这种歧义在后期做数据库设计时会爆发成字段级冲突。
关于读的方向,这里有个容易被忽略的细节。DFD画存储与加工之间的箭头时,很多人习惯只画一条无方向或双向的线。严格做法是:读取数据时箭头从存储指向加工,写入数据时箭头从加工指向存储,如果同一对“存储-加工”既读又写,应画两条数据流,而不是一条双向箭头。这样做的意义在于:你的数据字典和后续接口设计能够在存储平面上看出读写方向,对判断数据一致性和并发冲突很有帮助。
3.3 数据字典:把“守恒”落实到字段级别
只检查流的方向和个数还不够,数据守恒最终要落到字段级别,这就必须引入数据字典。数据字典是一个独立于DFD的体系,它把图上的每一个数据流、存储、加工说明都展开到数据项。比如数据流“有效订单”,不能只写这三个字,至少要说明它的构成:订单号+客户编号+商品编号+数量+金额+下单时间。这样当你校验“检查库存”这个加工时,输入数据流“库存查询请求”里的字段是否足够支撑输出“库存状态”里的字段,就可以逐项对齐。
我通常会在建DFD的同时维护数据字典,而不是等图画完再补。一个简单实用的模板是:数据流条目用“= ”表示组成,用“+”表示并列,用“{}”表示重复,用“[]”表示选一。例如:
有效订单 = 订单号 + 客户编号 + {商品编号 + 数量} + 订单金额 + 下单时间这样写的好处是,在评审会上不用靠眼睛盯线条,直接看数据流条目就能检查“守恒”。很多资深评审专家看DFD时,第一眼看图结构,第二眼就开始翻数据字典。画了图不写数据字典,DFD只能算半成品。
4. 原则三:命名与编号规则,可追溯性的地基
4.1 加工的编号规则:分层编号如何实现需求追踪
命名与编号看上去像洁癖,实际上是最便宜的可追溯性基础设施。一个加工只有三个字“做处理”,一个数据流只有“数据”两个字,你上哪儿去追踪它对应的需求条款?在大型软件里做需求追踪矩阵时,每一层DFD加工都应当能追溯到原始需求,而编号就是那个“钩子”。
具体做法分三步走。第一步,为每个顶层加工分配一个层级编号,0层加工用1、2、3,子图加工用1.1、1.2,再往下用1.1.1、1.1.2。第二步,建立一个“加工编号-需求编号”对照表,比如加工1.2“校验订单信息”对应需求FR-003“系统必须对订单关键字段进行合法性校验”。第三步,在子图展开时,子加工编号必须继承父加工编号前缀,这样任何人看到“1.2.2”就知道它属于“1.2”的子加工,不需要翻文件夹。
这里提醒一个容易犯的错:千万不要在中途重新编号。我见过有团队在修改DFD时,为了排序整齐把加工1和加工2对调,结果所有下游文档都跟着改,半个月内大家都在讨论“为啥这个需求挂在2下面”。要么用增量编号,要么在删加加工时暂时留空号,直到发布新版本再统一调整。
4.2 命名规范:动词+宾语与名词性标签
DFD中的四类元素各有命名规范,这条虽然琐碎,却是评审重灾区。
- 加工:必须用“动词+宾语”的结构,例如“校验订单信息”“计算应付金额”“生成出库单”,不能只用“数据处理”“后台操作”这种抽象短语。加工命名最能体现一个建模者的业务理解深度。
- 数据流:必须用“名词或名词性短语”,例如“审核通过的订单”“库存扣减结果”。数据流的标签应当是“数据”而不是“动作”,如果你发现标签写成“保存订单”或“发送通知”,说明你画的其实是控制流或动作,需要回到逻辑模型的基本定义。
- 外部实体:用“业务角色+边界说明”的组合,例如“已注册客户”“后台运营人员”“支付宝支付网关”,不要含糊地说“用户”“系统”。用户这个词在交互设计里够用,在结构化分析里太含糊。
- 数据存储:用“数据集合+用途”的组合,例如“订单存储”“库存台账”“用户资料库”。不能直接写“数据库”“文件夹”“缓存”,那样会跟物理实现混淆。
很多团队画DFD时会觉得“命名哪有那么讲究,能区分就行”。但在可追溯性体系里,不规范命名会让自动化的需求追踪工具完全失效。你写“有效订单”,数据字典里叫“合法订单”,测试用例生成器就会把它们当成两种数据,后续的追踪矩阵里永远缺一条链路。
4.3 命名与编号的常见坏味道
下面这张表是我平时做设计评审时拿来对照的,建议你直接收藏,画完图逐项排一遍。
| 元素 | 坏命名/编号 | 好命名/编号 | 问题所在 |
|---|---|---|---|
| 加工 | 处理(编号:无) | 校验订单信息(编号:2.3) | 无法定位业务逻辑,无法追踪需求 |
| 数据流 | 数据 | 审核通过的订单 | 数据流语义缺失,数据字典无法维护 |
| 外部实体 | 用户 | 已注册客户、后台运营人员 | 边界不清,容易混淆不同角色 |
| 数据存储 | 数据库 | 订单存储、库存台账 | 引入物理概念,过早限定实现方案 |
| 加工编号 | 从1重新排列所有子图加工 | 继承父编号,如1.2.1 | 重编号导致需求追踪失效 |
| 数据流命名 | 订单信息(在不同层级指向不同内容) | 整棵DFD树内统一术语 | 内容不一致,递延错误在下游爆发 |
除了上面的表,命名规范还有一条特别容易被忽略:数据流名称必须和数据字典条目的名称完全一致。很多团队图上是“订单信息”,数据字典里写的却是“订单请求”,这俩细微差异就会导致评审时不断有人举手问“这两个是同一个东西吗”。我自己的做法是,在最终评审前做一次“命名一致性核对”,把图上的每一个数据流名称拉出来跟数据字典条目比对,保证一字不差。
5. 原则四:加工划分的高内聚低耦合,工程实用性的命脉
5.1 逻辑内聚:一个加工只回答一个业务问题
如果说前三条原则保证了模型“正确”,第四条原则则决定了模型“好用”——能不能顺利变成代码、服务、模块和接口。工程实用性的命脉,就在加工划分之上。
加工划分的第一准则是逻辑内聚:一个加工应当完整地实现一个单一、明确的业务逻辑。判断方法很简单:如果这个加工需要靠“然后再”“接着”“此外”这种词才能说清它做什么,那必然是多个逻辑硬融在一起。比如“校验订单并计算金额并生成发票”就是三个逻辑三个变化的点,画到一张表里时看似省钱,后续需求只要有一条变化,就得动这个加工的定义、输入输出和数据字典,下游模块接口全跟着牵一发动全身。
与此对应的是耦合最小化:加工之间只能通过数据流交换信息,不能共享隐性状态,不能通过全局存储偷偷传参数。DFD的存储机制是一个天然的“去耦合层”,但如果一个加工读取某个存储只是为了给另一个加工传值,那你其实就是制造了一条隐式耦合的数据通路。这种问题在图上不明显,等到物理设计时两个模块都去操作同一个数据库表,就变成了并发和一致性的大坑。
5.2 不画控制流:DFD不是流程图
DFD建模最容易犯的颠覆性错误,是把控制流混进数据流图。DFD是数据逻辑模型,它描述的是数据在加工之间的流动与变换,不是指令执行顺序。也就是说,不要画“判断是否通过后跳转到某个加工”这类控制线,也不要用箭头表示时间先后。
我用一个生活化类比来解释:把DFD想象成一家医院的科室导诊图,它只告诉你每个科室接收什么单据、输出什么单据,哪个科室需要调用检验科的报告,而不会画一个箭头告诉患者“先挂号再就诊”。哪些处置有先后,属于流程逻辑,不该塞进DFD。这和流程图是两种完全不同的建模工具。
那么当业务里确实存在条件分支,比如“订单金额大于1000走人工审核,否则自动通过”,怎么在DFD里表达?正确做法是:把条件判断视为一个独立的加工,例如“判断订单审核方式”,它的输入是“订单金额信息+订单状态”,输出是“需人工审核的订单”或“可自动通过的订单”。两条输出数据流分别进入下游的不同加工。判断逻辑本身用加工说明(结构化英语、判定表、判定树)来描述,而不是画在DFD图上。
5.3 从DFD到模块结构图的映射
为什么我如此执着于加工划分的独立性?因为结构化设计的下一阶段,就是要把DFD里的加工“翻译”成模块结构图。高内聚的加工映射成一个模块后,模块内部只解决一个业务问题;低耦合的数据流映射成模块间的接口参数;数据存储映射成公共数据区或数据库接口。
这个映射不是线性的,但有了高质量的DFD,至少能得到一个准确的起点。我通常会在评审DFD时,顺手画一份模块初始划分草图,看每个加工是否都落在某个候选模块的职责范围内。如果某个加工的内容同时涉及“订单校验+订单价格计算+促销活动计算”,那它映射成模块后必定是一个职责混乱的大泥球,测试工作量和维护成本都会呈指数上升。所以,与其在设计阶段拆模块,不如在DFD建模阶段就把加工划分得干净利落。
6. 用一份检查清单把四大原则串起来评审
6.1 评审顺序与关键步骤
很多刚接触DFD的人喜欢按“从顶到底、逐层评审”的方式看图,这在评审会上效率很低。我自己的评审习惯是:先看一致性,再看可追踪性,最后抠实用性,走完一轮就能把绝大多数问题揪出来。
第一步,检查上下文图。确认外部实体是否完整,系统边界是否清晰,实体与加工之间的每条数据流是否都有明确业务含义。
第二步,逐层检查父子平衡。自顶向下遍历每一张子图,核对父加工的所有输入、输出是否在子图边界完整复现。这里不只看名称,还要看方向。一旦发现子图边界上出现父图没有的数据流,说明这一层分解引入了未经批准的逻辑变更。
第三步,做数据守恒检查。找出图中的黑洞、奇迹和灰洞,再逐条检查加工存储在单个“存储-加工”对上的读写方向是否一致。这一步建议配合数据字典做字段级核对。
第四步,抽查命名与编号。扫一眼加工和数据流的命名是否符合“动词+宾语”“名词性标签”规范,编号是否能从子图一路回追到上下文图,有没有重编号和悬挂编号。
第五步,评估加工划分的内聚性。随机挑几个加工,问一句“这个加工如果只回答一个业务问题,是什么问题?”答不上来的,说明该加工划分过粗或职责混合。
6.2 一个案例:图书预约系统的DFD体检
最后用一个简化案例演示这套流程。假设我们要评审一个“图书预约系统”的DFD,上下文图外部实体有“读者”“图书管理员”和“图书库存系统”。
打开顶层图时,我发现“图书库存系统”只画了一条“库存数量”数据流从外部实体指向加工“预约登记”,可是上下文图里读者发起的“预约请求”需要经过“查询可借副本”才能决定是否允许预约,而“查询可借副本”的来源应该是“图书库存系统”的“副本信息”,而不是外部实体直接给出。这个在父子平衡检查时立刻暴露为子图边界多出一条“副本信息”数据流,而父图没有。
继续往下检查数据守恒时,我又发现一个加工“打印取书通知”只有输入流“取书信息”,但它要输出“取书通知单”,并没有从“预约存储”读取读者的联系方式。负责这个加工的人可能是想当然觉得“取书信息”里包含了所有字段,但数据字典里“取书信息”只有“预约单号+取书时间”,根本不包含手机号。这是典型灰洞,必须在子图上补充“读者联系方式”这一数据流来源。
命名检查时,一张子图里出现了一个加工叫“处理预约”,另一个叫“处理通知”,这俩命名几乎等于没命名。我建议改成“登记预约申请”和“生成取书通知”,再补充对应编号和需求追踪ID。整份评审下来,我们一共发现了三处父子不平衡、两个灰洞加工、五个命名不规范项,以及一个加工职责过重(把“校验读者权限+查询库存+登记预约”三件事合并在一个加工里)。经过两轮修改后,这份DFD才真正达到可以交付设计阶段的质量。
在我实际的评审经验里,能一次性通过DFD建模评审的团队寥寥无几,但绝大多数问题都集中在上述四类。这不是因为大家能力不足,而是因为DFD建模是一门需要纪律的技术,它的价值只有在严格的规则约束下才会成倍体现。把这三条目标、四个原则变成团队画图时的检查习惯,那些“看起来没毛病”的DFD,才算真正能扛起后续设计和实施的重任。