从DFD到SC:变换分析、事务分析与结构化设计方法
很多软件工程课程设计和毕业设计做到需求分析阶段都能画出一张像样的数据流图数据从哪个外部实体来、经过哪些加工、存到哪个数据存储、最后送到哪里去都梳理得很清楚。但一旦进入“软件设计”章节很多人就开始卡壳数据流图DFD画完了程序结构图SC到底怎么从里面“变”出来这两张图之间的转换是结构化设计方法Structured Design的核心内容也是软件工程这门课里老师最看重的能力之一。这篇内容会从判断DFD类型开始把变换分析、事务分析的完整推导过程一步步拆开结合课堂设计和毕业设计中最常见的案例场景讲清楚DFD如何导出一张可直接指导编码的程序结构图。我指导过不少学生的课程设计和毕业设计发现大家同一个困惑反复出现DFD画得很完整SC却画得像“凭空想象”模块划分跟DFD中的加工过程对不上。这其实不是绘图能力的问题而是没有掌握从DFD推导SC的分析方法。这篇文章适合正在做软件工程课程设计、需要写毕业设计“概要设计”章节、或者准备软件工程考试的同学参考我会把我实际操作中会用到的判断技巧和避坑经验也一并写出来。1. 为什么“画完DFD”才是问题的开始分析模型到设计模型的跨越1.1 DFD和SC在软件工程中根本不属于同一个阶段先理清一个基本概念为什么不能直接跳过DFD去画程序结构图因为DFD和SC在软件开发过程中承担的职责完全不同。数据流图属于需求分析阶段的产物它回答的是“系统要做什么”。在DFD中我们看到的是数据从外部实体流入系统经过某些加工处理后流向数据存储或输出到外部实体但图中不会出现任何关于“用什么函数、分几个模块、谁调用谁”的暗示。程序结构图属于概要设计阶段的产物它回答的是“系统怎么组织”。在SC中模块是基本单元箭头表示调用关系旁边的标注表示模块之间传递的数据。SC更像建筑图纸每个模块将来都要对应到代码里的一个具体函数、类或者服务。从DFD到SC的过程就是要从“对问题的描述”过渡到“对解决方案的描述”。我见过有的学生直接把DFD中的加工原封不动平铺成SC中的模块这样做表面上像“导出”实际上只是换了张图画同一个东西根本没有进行设计决策后面的详细设计和编码阶段会处处难受。1.2 为什么结构化设计强调“可推导”结构化设计理论有个核心主张好的程序结构应该能从分析模型中系统地推导出来而不是凭借设计者的灵感拍脑袋。这背后的逻辑是需求分析阶段我们已经对数据流做了反复核实如果设计阶段能沿着同样的数据流线索延伸需求和设计之间的一致性就有保障。反过来想如果SC是凭空画出来的你怎么证明每个模块都有对应的需求来源当评审老师问“这个模块为什么要存在”“那个模块的数据从哪来”你可能答不上来。而通过变换分析或事务分析导出的SC每个模块都可以追溯到DFD中的某个加工或某条数据流这样就建立了一条“需求到代码”的完整追踪链这也是软件工程强调可追溯性的具体体现。这个阶段你还会碰到一个概念信息流类型。DFD的形态决定了推导方法的选择所以下一步必须先判断你的系统属于变换型还是事务型。2. 先定类型再动手变换型DFD与事务型DFD的判断方法2.1 两种信息流的本质区别软件工程的经典教材里会把信息流分成变换流和事务流但很多学生看定义觉得抽象我换种说法解释。变换流是一条“单行道”数据从外部进入系统经过格式转换、校验之后进入核心加工区域加工完成再转换成外部形式输出。整条数据流沿着一条主路径向前推进每个加工都在为下一个加工做准备像流水线一样。事务流是一棵“分叉树”数据从外部进入后会先到达一个类似“调度室”的位置系统根据数据携带的类型标记或条件信息从多条处理路径中选择一条去执行。不同路径的处理逻辑差异很大彼此之间几乎没有数据共享像快递分拣中心一样货物到了分拣口系统根据目的地把它分配到不同的运输通道。判断你的DFD属于哪一类核心就看数据在“中心位置”是继续线性加工还是出现了多选一的分派。这里给出一个实操性很强的三步骤判断法第一步从DFD的物理输入端出发沿数据流向内部追踪找出“逻辑输入”的边界。物理输入指原始进入系统的数据逻辑输入指经过格式校验、预处理之后真正进入业务逻辑的数据。第二步从物理输出端出发逆向追踪找出“逻辑输出”的边界。逻辑输出指加工完成、尚未进行界面展示或外部格式转换前的数据。第三步观察逻辑输入和逻辑输出之间留下的部分。如果这部分是一条连续的加工链每个加工都必须在链上依次执行属于变换中心如果这部分存在一个明显的“分派点”后面跟着多条互不相同的分支处理路径属于事务中心。用一个对比表格可以看得更清楚判断维度变换型DFD事务型DFD中心形态一条线性加工链一个分派点加上多条平行处理路径分支特征无分支或分支很快就合并分支后各走各的长期不合并触发方式单一类型的数据连续流入不同类型/条件的请求到达中心典型系统图书借阅、成绩统计、订单审核选课系统、银行柜面系统、网站后台菜单管理导出方法变换分析事务分析2.2 判断错了会有什么后果我批改过不少课程设计最常见的结构性错误就是判断错了类型还要硬套方法。比如一个选课系统学生提交的请求有选课、退课、查询、打印课表四种这明显是事务流但有的同学用变换分析硬把它拆成“输入控制、处理中心、输出控制”三段结果处理中心里要塞下四种完全不同的业务逻辑这个中心模块变得无比臃肿内聚性极差。如果拿不准你就看分支路径的数量和差异性。数据到了某个点之后后面跟着的处理路径在功能上完全不同、互相之间没有先后依赖关系那它就是事务流。反过来说如果数据流只是临时分头检查几个条件检查完又汇总到同一条主链路上继续加工那还是变换流。3. 变换分析完整推演从图书借阅管理系统的DFD到SC3.1 先在一个DFD上画出输入输出边界我用一个典型的毕业设计题目“图书借阅管理系统”来演示变换分析的完整过程因为它的主流程很清晰适合用来理解方法。图书借阅功能简化后的DFD大致如下读者提交借阅申请系统接收请求并检查格式检查读者是否有效检查图书是否可借登记借阅记录更新图书库存计算应还日期最后生成借阅回执返回给读者。在DFD图上做分析时我会先用两条虚线把这个流程切成三段[物理输入区] [逻辑输入] [变换中心] [逻辑输出] [物理输出区] 读者输入借阅申请→ 格式校验后的 → 读者资格校验 → 借阅结果和 → 前端界面显示 借阅请求 图书库存检查 应还日期等 借阅回执 借阅记录登记 加工结果数据 库存状态更新 应还日期计算左边“读者输入借阅申请”到“格式校验后的借阅请求”之间属于物理输入和逻辑输入边界右边“借阅结果和应还日期等加工结果数据”到“前端界面显示借阅回执”之间属于逻辑输出和物理输出边界中间这块由五个加工构成的线性链条就是变换中心。这里有个很容易犯的错误有的同学把“格式校验”也划进变换中心或者把“生成回执”也划进去。我的经验是判断标准看加工是否改变数据的“业务本质”。“格式校验”只是让数据变得规范没有改变它“借阅申请”的含义“生成回执”只是把结果包装成展示形式也没有产生新的业务语义。真正发生业务语义变化的是资格校验、库存检查、登记、更新、计算日期这五个步骤所以它们才是变换中心。3.2 第一级分解建立“输入-处理-输出”三叉骨架确定变换中心之后第一级分解就有了明确依据。顶层模块对应整个图书借阅管理系统它向下分成三个子模块借阅请求输入控制模块、借阅事务处理模块、借阅结果输出控制模块。用文本结构图表示就是这样图书借阅管理系统主模块 / | \ 借阅请求输入控制 借阅事务处理 借阅结果输出控制这一步看起来简单但它背后的设计决策很重要输入控制模块负责接收和预处理外部数据输出控制模块负责把内部数据转换为对外展示的形式真正的业务逻辑全部集中到事务处理模块中。这样将来如果要换一个界面框架或者把输入方式从表单改成扫码枪只需要改输入控制模块如果要改业务规则只动事务处理模块不需要牵动输入输出部分。3.3 第二级分解展开变换中心细化输出与输入第二级分解的重点是变换中心。根据DFD中变换中心的五个加工把它们映射成事务处理模块的下层模块图书借阅管理系统主模块 / | \ 借阅请求输入控制 借阅事务处理模块 借阅结果输出控制 | | | 请求接收与格式校验 读者资格校验 结果格式化与回执生成 图书库存状态检查 借阅记录登记 库存数量更新 应还日期计算注意这里“应还日期计算”我单独拆成了一个模块。有的同学会问不过只是一个日期加上一个借阅周期为什么要单独成模块因为它在DFD中就是一个独立的加工节点而且在真实系统中借阅规则经常变化——比如不同读者类型有不同的借阅周期不同图书类型有不同的借阅天数把它独立出来之后规则变更只影响这一个模块这正符合“高内聚、低耦合”的设计原则。从这棵SC中已经能看出将来代码的骨架// 示意代码由SC导出的借阅流程骨架 void HandleBorrowRequest(BorrowRequest rawRequest) { var validRequest ValidateAndNormalize(rawRequest); // 输入控制模块 var result ProcessBorrowTransaction(validRequest); // 事务处理模块 RenderResultToUser(result); // 输出控制模块 } BorrowResult ProcessBorrowTransaction(BorrowRequest request) { CheckReaderEligibility(request.ReaderId); // 读者资格校验 CheckBookAvailability(request.BookId); // 图书库存状态检查 var record CreateBorrowRecord(request); // 借阅记录登记 UpdateBookStock(request.BookId, decrease: true); // 库存数量更新 var dueDate CalculateDueDate(request.ReaderType); // 应还日期计算 return new BorrowResult(record, dueDate); }这段代码不是为了实现功能而是为了说明SC和代码之间的对应关系是直接的。每个底层模块最终都会对应一个函数或方法模块间传递的数据就是函数参数和返回值。3.4 变换分析的“由粗到细”节奏整个变换分析过程可以概括成一句话先全图看边界再顶层画骨架然后逐层展开。这样操作的节奏感很重要不少学生一上来就想把SC画到最底层结果第一版就画了二三十个模块看着很完整实际上逻辑混乱根本说不清模块之间的调用关系。我自己的习惯是第一版只画到第一级或第二级先确认顶层骨架正确再往下画。画完变换中心相关模块后回头看输入控制模块是否需要继续拆。如果输入控制只需要“接收请求”和“格式校验”那拆一层就够了不需要为了凑层级硬拆出五个子模块。4. 事务分析完整推演以学生选课系统的多分支处理为例4.1 事务型DFD的形态判断第二个经典场景是学生选课系统它比图书借阅系统复杂因为系统的入口接收的请求不止一种。学生在平台上可以选课、退课、查询成绩、导出课表这四类请求在业务逻辑上几乎没有交集处理路径完全不同。在DFD中这四类请求最初都会经过一个“请求接收与类型识别”的加工系统读取请求中的操作类型字段然后分发到选课处理、退课处理、成绩查询、课表导出这四个加工路径。这个“请求接收与类型识别”的加工就是事务中心。判断方法和前面一样从物理输入端往里走找到逻辑输入边界也就是请求类型识别完成后的那个点往上看事务中心后面是四个平行分支分支之间没有数据交汇所以这是明显的事务流。4.2 顶层设计获取模块加调度模块事务分析的第一级分解和变换分析不同它不采用“输入-处理-输出”三段式而是采用“获取-调度-执行”结构。顶层模块“选课子系统主控”下面先有一个事务获取模块负责接收原始请求、验证请求合法性、解析出请求类型再有一个事务调度模块负责根据请求类型将控制权分派给对应的处理模块。选课子系统主控模块 | 事务获取模块接收并预检请求 | 事务调度模块按请求类型路由 / | | \ 选课处理 退课处理 成绩查询处理 课表导出处理这里有一个非常重要的原则事务调度模块本身不应该包含任何具体的业务计算它只做路由。判断一张SC画得好不好可以看调度模块有没有被塞进额外逻辑。有的同学会把“判断选课时间是否截止”写进调度模块这就是错误的设计——时间判断属于选课业务规则应该放在选课处理分支内部。调度模块越纯粹将来新增一种业务类型就越容易。4.3 分支内部的进一步分解四个分支内部的复杂程度不一致选课处理的分支最深。选课时要先检查学生是否满足先修课程要求再检查课程容量是否已满容量充足的才能登记选课记录同时更新课程已选人数。退课处理相对简单确认该学生确实选了这门课执行退课登记然后回补课程容量。成绩查询处理一般比较单纯但很多系统会加权限检查学生只能查自己的成绩。课表导出则需要从选课记录中汇总课程信息按时间排序再生成导出文件。把这些分支展开后完整的SC如下选课子系统主控模块 | 事务获取模块 | 事务调度模块 / | | \ 选课处理 退课处理 成绩查询处理 课表导出处理 / \ | | | 先修课程检查 容量检查 退课登记 成绩读取 数据汇总排序 \ / | | | 选课记录登记 回补容量 权限核对 导出文件生成 | 更新课程容量4.4 事务分析的核心价值在于可扩展性事务分析得到的SC有一个很大的优点可扩展性。加入一种新的请求类型比如学生可以申请缓考只需要在事务调度模块后面增加一个新的处理分支不动已有的任何模块。这是软件工程里“开放封闭原则”在结构设计阶段的体现。所以画事务型SC时你应该刻意留出扩展空间。我在实际做设计时会在事务数据模型里设计一个统一的请求类型字段并且在调度模块中采用配置表或者策略模式来实现路由将来增加分支时不改调度代码只增加新策略即可。5. 导出的SC长什么样才算合格接口、耦合与形态检查5.1 用映射表审核DFD和SC的一致性SC画完之后不能只看孤立的图要拿它和DFD逐项对照。我在评审课程设计时会对照检查DFD和SC之间的对应关系是否完整DFD中的元素SC中应该有的体现检查要点加工/处理处理模块加工是否都能在SC中找到对应模块数据流模块之间传递的数据参数数据名能否在SC调用边上找到数据存储数据管理模块或持久化接口对存储的读写是否集中封装外部实体输入/输出控制模块实体的数据入口出口是否完整设计事务中心事务调度模块分派逻辑是否独立实际做出来的SC如果有一个DFD中的加工找不到对应位置说明设计有遗漏如果SC中的模块在DFD中找不到来源说明这个模块可能是你凭空想象出来的需求依据不足。5.2 内聚和耦合的检查方法软件工程里讲高内聚、低耦合到了SC阶段这句话要落到具体模块上。高内聚意味着一个模块内部的各个组成部分应该属于同一类功能职责选课处理模块里不应该出现课表导出的逻辑。低耦合意味着模块之间尽量只通过清晰的数据参数传递信息尽量不要通过全局变量或共享存储暗中通信。在SC图中模块调用边上的数据标注就是耦合的体现。如果发现两个模块之间要传递七八个参数通常意味着这两个模块的职责划分有问题或者中间缺少一个数据封装。你可以考虑把这几个参数封装成一个请求对象这样既能减少参数个数又让结构更清晰。5.3 用“树形指标”快速发现结构问题SC本质上是一棵调用树有几个形态指标可以快速帮你发现结构是否合理。扇出指一个模块直接调用的下层模块数量扇出过大说明这个模块承担了太多职责或者抽象层级不对扇入指一个模块被多少个上层模块调用扇入高的模块往往是公共基础模块设计良好时应该被多处复用。树的深度如果太深模块调用链过长会增加理解难度如果深度太浅又可能说明每个模块过于庞大没有做好职责细分。我检查SC时会专门看有没有异常的“倒三角形”结构。如果一个模块的下层模块全部是单个叶子模块而且这个模块的扇出超过了七个通常说明它需要进一步分组在它和叶子模块之间插入一层中间控制模块。5.4 从SC迈向代码骨架的过渡SC的最终用途是指导编码所以画到每个底层模块时都应该能给出它对外提供的接口描述。做毕业设计时我会要求自己先根据SC写出一份模块接口清单每个底层模块对应一个方法签名包括函数名、输入参数、返回结果这样后续编码阶段就只是填充方法体基本不会遇到无法下笔的问题。接口清单的形式很简单比如“选课处理模块enrollCourse(studentId, courseId)执行流程为先修检查、容量检查、登记、更新容量返回选课结果对象”。SC加接口清单这一套组合下来代码骨架就自动浮出水面了概要设计到详细设计的过渡也顺理成章。6. 课程设计与毕业设计中的高频错误清单6.1 把SC当成DFD的拷贝最常犯的错误就是SC只是DFD的换个画法加工的排列顺序和DFD一模一样没有任何抽象层级。DFD是数据流向图SC是模块调用关系图即使加工在DFD里是顺序排列的在SC里也要根据调用关系组织可能出现一个上层模块调用多个按顺序执行的子模块也可能出现多个上层模块共享同一个公共模块。SC的树形结构是对模块化设计的表达不是对数据流的复制。6.2 忘记给数据存储设计对应模块不少学生的SC只有和对DFD加工对应的处理模块完全没有考虑数据存储怎么访问。DFD中的数据存储将来要对应到数据库或文件SC里必须设计数据管理模块来封装对存储的读写操作否则这些操作只能散落在各个业务模块中将来数据库改动时会牵连一堆模块。6.3 控制流和数据流混画在SC上SC中的调用箭头旁边应该标注的是数据参数。有的同学把箭头当成控制流来用在图中画上类似“如果成功则调用下一个模块”的指令这其实是把SC和程序流程图搞混了。SC表达的是静态调用结构分支判断逻辑属于详细设计阶段的程序流程图。如果你发现SC中要画很多条件判断才能表达清楚说明你画错了抽象层级。6.4 事务调度模块不纯粹事务型系统中调度模块是最容易变“脏”的地方。因为负责路由写代码很方便顺手就在调度函数里加了各种业务判断比如“如果是教师用户则跳过选课时间检查”之类的逻辑。这些规则应该下沉到对应的业务处理模块内部调度模块只负责根据请求类型找到正确的处理模块并调用它。6.5 深度不足或过度展开有的同学为了展示工作量把SC画得很深一个“登录模块”也要拆到“接收用户名”“接收密码”“比对数据库”这种细粒度。这会导致SC失去概要设计的意义。SC的层次应该停在“一个函数/一个服务能够独立完成的职责”这个粒度低于这个粒度的细节留给详细设计。反过来的问题也存在SC只有两三层底层模块还是几行都说不清的大型模块这就等于没设计。判断标准是每个底层模块都应该能用一句话说清楚职责并且能够在代码里用一个函数或方法独立实现。6.6 不标注模块之间传递的数据很多课程设计里SC画得很漂亮但调用边上一个字都没有。没有数据的SC只能看出调用层级看不出模块之间如何协作评审时很难判断设计的合理性。建议你把每个模块的输入输出数据流全部标在调用边上如果一张图太密集宁可拆成多个局部SC也不要省略数据标注。7. 我的一些实操体会我在做自己的项目设计时很少把DFD导出SC当成一次性的“作业”而是把它当作一个反复验证设计的过程。第一版SC画出来后我会拿着它反过来检查DFD如果某个模块的结构很别扭通常不只是SC的问题而是DFD中加工划分得不合理。SC是一面镜子能照出DFD里隐藏的问题。比如我之前做过一个库存盘点的小系统第一版SC里发现盘点数据核验模块要同时处理三种类型完全不同的数据怎么看都很别扭。回头看DFD才意识到这三个数据类型在导入阶段就已经混在同一个加工里了正确的做法是在DFD阶段就拆成三条独立的数据流。因为画SC发现了分析阶段的错误这比等代码写了一半才发现要好得多。如果你现在正在做课程设计或毕业设计我的建议是拿到DFD后不要急着画SC先在DFD上花半小时把逻辑输入、逻辑输出、变换中心或事务中心的边界画清楚。这半小时省不了边界画对了后面的SC基本就是机械推导边界画错了后面返工的成本远远大于这半小时。另外一个小技巧是画图和调整结构的阶段建议用draw.io或Visio这种可以快速拖拽的工具不要一开始就用代码生成工具或者LaTeX画因为前几版结构大概率会改快改快迭代比追求版本精美重要得多。结构稳定之后再花时间整理最终版本放进课程设计报告里。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →