见过太多画得很精致、逻辑却一塌糊涂的数据流图了。之前有一次内部评审,同事放出一张订单系统DFD,方框、圆圈、箭头一个不少,配色也舒服。但我盯了几分钟后问了三个问题:客户档案这个数据存储为什么直接连着“客户”这个外部实体?中间这个只出不进的加工,数据是从哪儿“变”出来的?这条叫“数据”的流到底是订单还是发货单?全场安静了一会儿,然后大家开始逐条补需求。后来整理下来,缺的那些功能,恰恰都藏在被“画飞了”的数据流里。
这就是DFD建模中那四条规则的价值。它来自结构化分析里最经典的一脉,SA方法、Yourdon/DeMarco方法都把它当作建模底线。不管你是软件工程专业的学生、刚入行的需求分析师,还是被分配去画系统现状图的开发,只要你画数据流图,这套规则就绕不过去。它能帮你从“画得热闹”变成“画得对”,也能在项目评审时帮你提前发现需求漏洞。这篇文章不打算复述教科书,我想把它拆开揉碎,从为什么会有四条规则,到每条规则怎么用,再到怎么靠它们去做上下文图逐层分解,一次说清楚。
1. 画了那么多DFD,为什么还是分不清数据流图与业务流程图
1.1 数据流图是“数据加工的蓝图”,不是流程顺序图
很多新手把数据流图画成了带箭头的业务流程图,这是一个很常见的误区。业务流程图的箭头往往表示时间先后、责任岗位流转、判断分支;而数据流图的箭头只有一个含义:数据正在流动,流向哪里、从哪里流出。它不表达“先做什么后做什么”,也不表达“谁通知谁”,更不表达某个数据库表里具体怎么更新。
我经常用一段话解释DFD:它是一张“数据在系统边界内如何被加工”的路线图。数据从外部实体进入系统,经过一个个加工被变换、校验、计算、存储,最终形成结果返回给外部实体。整个过程里,只有四种角色:外部实体、加工、数据存储、数据流。其中“加工”是唯一能修改数据、产生新数据的地方。记住这句话,后面四条规则就顺理成章了。
外部实体是系统外的人、组织或系统,比如“顾客”“财务软件”,是数据的来源或终点。加工是把输入数据变成输出数据的动作,比如“校验订单”“计算总价”。数据存储是静止状态的数据集合,比如“D1订单表”“D2客户档案”。数据流则是正在移动的数据,比如“订单信息”“支付结果”。DFD里不画循环、不画判断菱形、不画时间线,只画数据在这些角色之间的流动。
1.2 四种元素之间,只有哪些连接是“合法”的
一旦把四种角色定位说清楚,一个核心问题就出来了:谁和谁之间可以直接画数据流?
可以直接相连的组合其实很有限。外部实体可以连接到加工,加工可以连接到外部实体;加工可以连接到加工;加工可以读写数据存储。反过来说,下面这些连接都是不合规的:外部实体之间不能直接相连,数据存储不能直接连外部实体,数据存储之间也不能直接相连。
你把这些非法连接列出来,就是DFD建模中最经典的四条规则最常见版本:
- 规则一:外部实体之间不能直接有数据流。
- 规则二:数据存储不能与外部实体直接相连。
- 规则三:数据存储之间不能直接相连。
- 规则四:每个加工至少要有流入的数据和流出的数据,不能产生“黑洞”或“奇迹”。
有的教材把规则四写成“加工必须既有输入又有输出”,有的表述是“加工要满足数据守恒”,本质是一样的。也有的老师会把命名规范和父子图平衡单列成第五条、第六条,这就属于分层建模的扩展规则,我在第三部分会展开。不管表述怎么换,核心思想从来没变:加工是数据唯一被处理的地方,所有跨边界的数据行为都必须经过加工,数据不能凭白无故地出现和消失。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心四条规则逐条拆解:每条背后都是真实踩过的坑
2.1 规则一:外部实体之间不能直接连接
第一次接触这条规则时,有人会觉得莫名其妙。画图时有一条箭头从“顾客”指向“管理员”,表示顾客申请借书,管理员审核,看起来挺顺的,为什么不行?
问题在于:如果“顾客”和“管理员”之间可以直接传数据,系统在这里就变成了旁观者。顾客向管理员提交申请、管理员审核通过,这些行为全部绕开了你正在建模的系统。那项目里要求记录申请、控制审核权限、保留审计日志,这些东西谁来管?没有加工参与,数据到底是什么时候进入系统的?边界之外的数据交互,系统不可能处理,也不可能追踪。
举一个我实际见过的例子。有人在新员工入职系统DFD中画了一条“候选人 -> 用人部门”的箭头,想表达简历送达。可图中系统的“招聘管理”加工完全没出现。候选人联系用人部门,确实是线下可能发生的事,但作为系统建模,要么它不属于当前系统边界所以不该画进来,要么它必须先进“简历投递”加工,再经过“面试安排”加工到达用人部门。不画进系统,评审时需求就会漏;画进系统却不经过加工,图就是在自欺欺人。
所以拿到一张图,第一步就是扫所有箭头,看看有没有两个外部实体之间直接连着的线。一旦发现,优先考虑两种修法:如果这段交互不需要系统参与,说明它不在系统边界内,删掉;如果系统需要记录或处理它,就在中间增加加工节点。
注意:DFD中的“外部实体”不一定是人,其他系统也算。比如你的订单系统要和仓库系统对接,如果“订单系统”和“仓库系统”都作为外部实体出现在图里,它们之间也不能直接画一条“出库指令”,必须经过你系统内部的加工,否则你的系统在这条链路上就没有存在意义。
2.2 规则二:数据存储不能与外部实体直接相连
这条规则是新手最容易犯的错。很多人画到“用户注册”时,随手就是一条箭头从“用户”指向“D1用户表”,觉得意思到了——用户数据存进数据库了。可这条线在DFD语义里是完全错误的,它等于允许外部实体直接读写数据存储,中间的校验去哪了?密码加密去哪了?用户名是否重复谁来检查?
正确的画法是:用户把“注册信息”发送给加工“用户注册”,加工对数据做校验、加密,再把“新增用户数据”写入数据存储D1。DFD表达的是逻辑模型,不是物理登录数据库的过程。只有在加工内部,数据才允许经过业务规则被处理;数据存储是一个被动的“仓库”,它自己不能接收外部数据,也不能主动把数据吐给外部,必须由一个加工代为读写。
我在评审时还见过一种变体:外部实体“管理员”直接连在“D2操作日志表”上,理由是“管理员需要查日志”。管理员确实要查日志,但你的图也应该画出一个加工,比如“查询日志”,让“管理员 -> 查询日志 -> D2操作日志表”这样一条链路。否则后续做权限设计时,你根本不知道管理员是以什么条件、什么角色、什么操作去访问日志的,这些限制只能由加工来表达。
反向也一样。数据存储直接输出给外部实体,例如在图上画“D1库存表 -> 采购员”,意思是库存数据被采购员看到。可它怎么被看到?是定时报表、页面查询还是消息推送?这些业务差异决定了多个不同加工,而不是一条直连线。因此,开发一个简单的判断习惯:凡是在图中看到外部实体旁边直接连着存储符号,基本可以断定作者把“逻辑处理”省略了,缺了一个加工。
2.3 规则三:数据存储之间不能直接相连
存储与存储之间的直连箭头,在数据流图上露馅得很快。如果你画“D1订单表 -> D2报表统计表”,翻译成业务语言就是:有一张表的数据自动流到另一张表里了。数据库表之间不经过代码逻辑自己搬家,这在现实系统里并不存在。表结构之间的同步、汇总、清洗,都是由一段程序完成的,表现在DFD里就必须有一个加工。
好比让你画一个“每日销售统计”功能,你可能会画一条线把“D1销售明细”直接指向“D2日报表”。这条线不能表达统计口径:按商品还是按门店汇总?统计时间怎么截取?历史数据重跑怎么办?这些规则恰恰是业务分析里最需要确认的东西。正确做法是设置加工“生成每日销售统计”,从D1读取销售明细,进行聚合计算,再把结果写入D2。规则三的本质,其实和规则一、规则二完全一致:数据存储之间的任何数据移动,都必须有一个加工在中间承担业务逻辑。
在检查数据流图时,可以按这条经验快速搜索:所有存储符号之间是否只通过“加工”间接相连。如果看到存储A旁边有箭头直接指着存储B,就在中间补加工,并且给数据流命名,比如“汇总后的销售数据”。这个改动的过程,往往能逼着分析人员把“到底怎么从A得到B”的逻辑讲明白。
2.4 规则四:加工不能是黑洞或奇迹,输入输出要守恒
前面三条规则都聚焦在“连接不能绕过加工”,第四条规则则要求每个加工本身必须是完整的。它可以理解为三个具体病症:黑洞、奇迹、灰洞。
黑洞加工只有输入没有输出。比如图里有个“1.校验订单”,流入“订单原始数据”,但这个加工没有任何流出方向,也没写存储。校验完数据去哪了?是被丢弃了,还是应该有后续输出?如果这个加工的职责是校验后通过合法订单,那么它应该有输出流,比如“有效订单信息”进入下一个加工或数据存储。只有进没有出,数据被黑洞吞掉,需求一定是断的。
奇迹加工正好相反,只有输出没有输入。一个加工突然输出“对账单”,但没有任何数据流进入,也没有读取任何存储,这张对账单是从天上掉下来的吗?实际业务中,它大概率需要读取“D1订单明细”和“D2客户信息”才能生成。缺了这些入边,就说明需求描述时漏掉了“对账单数据来源”的讨论。
灰洞加工稍微隐蔽一点,它既有输入也有输出,但输出远远超出了输入能推导的范围。比如输入一个“客户ID”,却输出了“客户全套地址、订单历史、积分变动明细”。如果系统里没有按客户ID索引到这些信息的数据存储,输出就成了无源之水。我在做需求分析时,只要发现某个加工的输出信息在输入流和它能读取的存储里找不到来源,就会回到业务方问一句:“这些数据从哪来?”这一问经常能问出被遗漏的关联关系。
实操口诀是:每个加工至少有一条流入或读取存储的来源,也至少有一条流出或写入存储的去向。画完图后把每个加工当成一个“黑盒”,用输入、输出清单去核对,看哪些字段对不上,往往就是需求逻辑最薄弱的地方。
3. 上下文图怎么一层层分解到子图:图书借阅系统实操
3.1 第一步:用上下文图锁住系统边界,先不急着画加工
四条规则保证单张图内部不错,但实际做需求分析时,DFD往往要画好几层。第一层叫上下文图,也叫顶层图,它最“抽象”,整张图通常只有一个加工,代表整个要开发的系统。外部实体围绕在周围,所有数据流都穿过系统和外部实体之间的边界。
上下文图最大的作用是确认系统边界。哪些角色会和系统交互?系统对外提供什么结果?哪些数据进、哪些数据出?如果这一步不画清楚,后面怎么分解都会乱。
我用一个常见的“图书借阅系统”来讲。一开始不要想“借书”要拆几个动作,先列外部实体。这个系统至少有两个外部实体:读者、图书管理员。读者发起借书、还书;管理员负责图书入库。此时上下文图里可以出这么几条流:读者发出“借阅申请”给系统,系统返回“借阅结果”;读者发出“还书信息”,系统返回“还书结果”;管理员提交“图书入库单”,系统返回“入库结果”。中心只需要画一个加工节点,名字写整个系统名“图书借阅系统”。
这一步做完,先别急着往下拆,用规则扫一遍:外部实体之间有没有直连?没有。外部实体有没有直连存储?上下文图里还没有存储,所以也不会有。中心加工是不是有进有出?有。上下文图已经在结构上符合DFD的基础规则了。
3.2 第二步:把上下文图分解成0层图,让数据流落到存储上
上下文图通过后,再把它内部的单个加工拆成一张0层图。0层图里才会出现多个加工、多个数据存储,并且每个加工要承担相对独立的功能。图书借阅系统的0层图可以画成三个主加工:1借书处理、2还书处理、3图书入库管理。
现在把存储设计出来。至少需要三个数据存储:D1读者档案、D2图书档案、D3借阅记录。借书处理时,从读者那里接收“借阅申请”,要读取D1判断读者资格、读取D2判断此书能否外借,然后向D3写入一条借阅记录,同时更新D2的在馆状态,最后把“借阅结果”返回给读者。还书处理则接收“还书信息”,读取D3找到对应借阅记录,更新记录状态,更新D2的库存状态,把“还书结果”返回给读者。图书入库管理接收管理员的“图书入库单”,写入D2,给管理员返回“入库结果”。
你可以用下面这张表检查0层图的数据守恒,不要只盯着视觉上有没有箭头:
| 加工 | 输入流及来源 | 读取存储 | 写入存储 | 输出流及去向 |
|---|---|---|---|---|
| 1 借书处理 | 借阅申请(读者) | D1、D2 | D2、D3 | 借阅结果(读者) |
| 2 还书处理 | 还书信息(读者) | D2、D3 | D2、D3 | 还书结果(读者) |
| 3 图书入库管理 | 图书入库单(管理员) | 无 | D2 | 入库结果(管理员) |
这张表不仅用来写文档,画完图后我会一行一行对着看。如果某行“读取存储”为空,但加工职责又需要判断什么,说明输入流可能漏了;如果“输出流”为空,但行尾没有存储写入,那就是一个妥妥的黑洞。0层图全表过完没有发现残缺,可以继续往下拆。
3.3 第三步:继续拆子图,并用父图与子图平衡做校验
当某一个加工仍然复杂,还值得继续细化时,就把它单独展开成一张新DFD。这里必须守住一条扩展规则:父图与子图的数据流必须平衡。更直白的说法是,父图中这个加工的所有输入流和输出流,必须在子图的边界上原样出现,不能多、不能少、不能悄悄改名。
比如“1 借书处理”这个加工,在0层图上输入是“借阅申请”,输出是“借阅结果”。我打算把1继续细化为三个子加工:1.1校验读者资格、1.2校验图书状态、1.3登记借阅。那么子图的外部边界上
