ATM系统UML五视图建模实战:从需求分析到用例图、类图、顺序图、协作图与活动图 📅 发布时间:2026/9/18 1:25:10 👁 浏览次数: 1. 为什么我建议拿 ATM 系统练手 UML 五视图ATM 系统的用例图、类图、顺序图、协作图、活动图设计几乎是每一本面向对象分析与设计教材都绕不开的经典案例也是软件设计师考试里反复出现的建模题型。它好在哪好在业务边界足够清楚——插卡、验密、取款、存款、转账、查询、退卡一圈走下来闭环完整又好在异常分支足够丰富——密码错三次吞卡、余额不足、钞票不足、通讯超时、客户中途取消这些分支恰好能把顺序图里的条件消息、活动图里的判定节点、用例图里的 extend 关系全部逼出来。更关键的是ATM 不需要你懂金融业务才能理解任何人都用过机器读者看到图能立刻判断你画得对不对。我带过几届学生的课程设计也帮朋友改过软考中级软件设计师的真题答案。一个很普遍的现象是五张图画了但每张图都在讲同一件事用例图里塞了对象类图里写了操作顺序顺序图画成了流程图。这种“图会画、语义不对”的问题根子不在工具而在没有想清楚每张图各自的职责边界。这篇就按我自己实际做的顺序从需求梳理一路讲到五张图落地把每个取舍背后的理由、工具的实操细节、以及踩过的坑都摊开说。1.1 一张图说不清的事五张图刚好分完面向对象建模的本质是“多视角切片”。一个系统太复杂任何单一视图都无法同时表达清楚“谁在用”“系统有什么”“对象怎么协作”“消息什么顺序”“流程怎么走”。UML 的价值就在于它提供了若干正交的视角每个视角只回答一类问题合起来才构成完整描述。我通常用一个类比来解释盖一栋楼你会看到总平面图谁从哪进、有哪些功能区、结构图梁柱板墙怎么配筋、水电图管线怎么走、施工进度图先干什么后干什么。没人会拿结构图去说明消防通道在哪。UML 五视图也是这个道理用例图是“总平面”类图是“结构”顺序图和协作图是“管线走向的两种画法”活动图是“施工进度与分支决策”。所以第一件要建立的心智是不要让一张图承担它不该承担的职责。类图里出现“先插卡再验密”这种时序描述就是越界顺序图里画一堆类之间的静态继承关系也是越界。判断方法很简单——问自己这张图回答的核心问题是什么凡是超出这个问题的信息删掉。1.2 五张图的分工边界对照把边界写成表格最直观我给学生改图时基本就按这张表逐条核对。图名核心回答的问题主要元素静态/动态典型交付阶段用例图谁在用系统、系统对外提供哪些功能参与者、用例、系统边界、关系静态偏需求需求分析类图系统内部有哪些类、类之间是什么关系类、属性、操作、六种关系、多重性静态设计初期顺序图一次交互中对象按什么时间顺序发消息生命线、激活条、消息、组合片段动态详细设计协作图一次交互中对象之间通过哪些链传消息对象、链、带序号的消息动态详细设计活动图业务流程的控制流与并发、分支怎么走动作、判定、分叉汇合、泳道动态偏流程需求/设计均可这张表里最容易被忽视的一行是协作图。很多教材和工具已经把它改叫“通信图”Communication DiagramUML 2.x 之后这个名字更常见但含义是一样的。它和顺序图是同构的——表达的是同一次交互只是侧重点不同顺序图强调时间轴协作图强调对象之间的连接结构。这一点后面单独拿一节讲转换规则。有一点我要提醒如果是课程设计或考试答题题目里明确写了“协作图”那就按协作图画不要擅自换成通信图的名字虽然语义一致但阅卷或答辩时可能被认为没按题目要求来。名字上可以标注“协作图通信图”两边都照顾到。1.3 工具选型我为什么优先推荐 StarUML 与在线工具工具这件事我用过的排列组合大概是这样StarUML、Visio、draw.io现名 diagrams.net、IDEA 与 Eclipse 的插件反向生成、以及纯手绘白板。各自适用场景差别挺大。StarUML 的优势是 UML 语义最纯正它会强制你区分 Association、Aggregation、Composition、Dependency、Generalization 这些关系类型画错了在模型树里一眼能看出来。缺点是老版本界面偏旧导出图片的分辨率需要手动调。Visio 的优势是排版自由适合最终的文档插图因为它对齐、连线、样式控制都很舒服。坑在于 Visio 的 UML 模板给的是“图形”不是“模型”——你把一个矩形拖过去它并不知道那是个类还是一个动作所以语义全靠自己把关。用 Visio 画 UML 类图的时候一定要手动选择正确的箭头类型默认的“关联”箭头很容易被当成泛化用出去。draw.io 是我现在最常用的理由是免费、跨平台、可以直接存到云盘、导出 SVG 无损。它的 UML 形状库足够全虽然不会帮你做语义校验但对绝大多数课程设计和文档场景完全够用。至于 IDEA 或 Eclipse 反向生成类图那是另一条路适合从已有代码倒推结构做代码走查或者给老系统补文档。Eclipse 里更多是靠插件实现IDEA 的 Diagrams 功能在 Ultimate 版本里比较顺手右键一个类或包就能生成类图还能选择显示字段、方法、构造器、依赖关系。注意反向生成的类图通常“信息过载”。一个中等规模的服务类可能带出几十条连线直接放进文档会让人抓不住重点。我的做法是生成后手动裁剪只保留核心领域类和它们之间的主要关系把工具类、日志类、异常类全部隐藏。2. 需求梳理把 ATM 拆成可建模的颗粒度动手画图之前我会先花时间做一件事写一份不超过两页的需求清单。这不是形式主义而是因为 UML 五张图本质上是对同一份需求的五次翻译需求本身不清晰翻译出来的五张图必然互相打架。ATM 这个题目看着简单其实细节不少比如“转账”要不要手续费、“查询”是否允许打印凭条、“密码错误”是三次锁定还是三次吞卡这些细节会直接影响用例关系与顺序图中的条件分支。2.1 系统边界与参与者的划定参与者Actor的识别原则是站在系统外部与系统交互的角色或外部系统。ATM 系统的参与者我一般划为四类第一类是客户也就是持卡人。注意不要把“客户”和“银行卡”混为一谈卡是客户用来交互的载体它是被系统处理的对象属于实体类不是参与者。第二类是银行主机系统也就是后台的核心账务系统。它是外部系统ATM 与它之间通过报文交互完成余额查询、扣账、记账。这一条特别重要因为如果只画了客户一个参与者你的顺序图就只有一条生命线画不出真正的交互也没法解释“通讯超时”这个异常分支从哪来。第三类是运维人员负责加钞、清机、取凭条、日常维护。这部分用例常被忽略但加上它之后用例图会立刻显得完整而且加钞这类用例天然带出“余钞量”这个类属性和后面的类图能对上。第四类是时间作为触发者。定时对账、定时上传交易日志这类用例触发源不是人而是时间。UML 里可以用一个带 «actor» 构造型的时钟图标表示或者在用例描述里注明触发方式。系统边界的画法是一句话把所有用例框在一个矩形里参与者全部画在矩形外面。这条规则看着简单但我见过太多人把“银行卡”画在矩形里面当类又把“银行主机”画在矩形里面当子系统边界就彻底糊了。2.2 用例清单与优先级排序我整理的 ATM 用例清单大概是这个结构按业务主线分组卡片相关插卡识别、密码验证、吞卡处理、退卡查询相关余额查询、交易明细查询、凭条打印现金相关取款、存款转账相关行内转账、跨行转账维护相关加钞、清机对账、日志上传、故障上报优先级我按“主成功场景覆盖度”排序插卡识别、密码验证、余额查询、取款、退卡这五个必须有存款、转账次之维护类用例在课程设计里属于加分项在软考真题里基本不考可以简化。这里有一个经验性的取舍如果一个用例在顺序图里连三条消息都凑不齐说明它的颗粒度太细了应该合并。比如“屏幕显示欢迎语”就不该是一个独立用例它是插卡识别这个用例的一个步骤。反过来如果某个用例在顺序图里画了十几个对象、二十几条消息那就要考虑拆。比如“取款”如果同时包含了跨行手续费计算、异地取款限额校验、日累计限额校验那就该拆成“基础取款”和“限额与费用处理”两个用例至少在上层用例图里分层表达。2.3 主流程与异常流程的写法用例描述我用的是一个简化模板写起来快也方便后面直接翻译成顺序图和活动图字段说明取款用例示例用例名动宾结构取款参与者主要参与者客户、银行主机前置条件进入本用例前的系统状态卡片已插入且密码验证通过后置条件成功成功后系统状态账户已扣账、出钞完成、交易日志已记录后置条件失败失败后系统状态账户未扣账、已出钞需冲正、提示已下发主成功场景无异常时的步骤序列1 选择取款金额 2 系统校验余额与限额 3 主机扣账 4 出钞 5 打印凭条 6 退卡扩展场景每一步可能的分支2a 余额不足 / 2b 超单笔限额 / 3a 主机超时 / 4a 出钞失败这张表是后面所有动态图的“剧本”。顺序图是主成功场景加若干扩展场景的画法活动图是把主场景和扩展场景合在一张图上用判定节点串起来。我个人的习惯是先把这张表填满再开工具画图能省掉大量返工。3. 用例图谁在用系统系统能做什么3.1 参与者之间的泛化关系参与者可以有泛化关系这在 ATM 里非常自然客户可以派生出本行客户和他行客户。本行客户的卡在本行 ATM 上取款免手续费他行客户要收手续费本行客户可以查询更长的交易明细他行客户可能只允许查最近若干笔。用泛化箭头空心三角指向父参与者表示本行客户和他行客户自动继承客户的所有用例而各自的特殊用例单独连到子参与者上。这个建模手法有两个好处。一是避免重复连线——如果不用泛化你就要把“余额查询”这个用例分别连到本行客户和他行客户两条线上图面会很乱。二是把“差异点”显性化评审时一眼能看出业务规则的分布。缺点是有些老师或阅卷人会觉得多此一举因为实际系统里往往是一个统一的客户对象加一个身份判定逻辑并不真的存在两个参与者类。所以我一般会说明泛化是为了表达业务规则的差异属于分析层面的抽象不一定要落到实现层。3.2 include、extend 与泛化三种关系别用混用例之间的关系是最容易出错的地方我按自己的判断标准逐条说清楚。include包含表达的是“必然发生的、被抽出来的公共步骤”。判断标准是基用例每一次执行都必然执行被包含用例且被包含用例是完整的一段可复用行为。ATM 里最典型的例子是“密码验证”被“取款”“存款”“转账”“查询”共同包含。这里我见过最多的误用是把“密码验证”画成 extend理由是“密码验证是可选的吗不是”。只要是必然走的公共步骤就一定是 include箭头方向是从基用例指向被包含用例虚线加实心箭头标注 «include»。extend扩展表达的是“在特定条件下才发生的、对基用例的增强或补充”。关键词是“条件性”和“非必然”。ATM 里最标准的三个 extend 是这样的吞卡处理扩展密码验证条件是连续错误次数达到上限凭条打印扩展取款条件是客户选择打印凭条超时处理扩展任意交易类用例条件是与主机通讯超过阈值时间。箭头方向是从扩展用例指向基用例虚线加实心箭头标注 «extend»。这个方向和 include 正好相反是高频失分点。泛化generalization用在用例上表示“子用例是一种特殊的父用例”。ATM 里可以这样用行内转账和跨行转账泛化自转账。箭头从子用例指向父用例空心三角。判断标准是“is-a”关系能否读通行内转账是一种转账读得通密码验证是一种取款读不通所以不能用泛化。把这三条整理成速查判断关系判断问句箭头方向典型例子include基用例每次执行都必须做吗基用例 → 被包含用例取款 → 密码验证extend只在特定条件下才发生吗扩展用例 → 基用例吞卡处理 → 密码验证泛化能读成“子是一种父”吗子 → 父跨行转账 → 转账3.3 实操画法与两个高频踩坑用 StarUML 画用例图步骤是新建 Use Case Diagram从工具箱拖 Actor 和 Use Case先放系统边界矩形再把用例放进去最后连关系。注意系统边界在 StarUML 里叫 Boundary它是一个矩形框可以调整大小把用例包住参与者在框外。用 draw.io 画的时候左侧形状库搜 “UML” 就能找到 Use Case 那一组圆角椭圆是用例。我建议把参与者统一用火柴人不要用带 «actor» 构造型的方框除非是外部系统。两个高频坑我列一下第一个坑把外部系统画成参与者但用了类的图标。银行主机是外部系统标准画法是火柴人形状加 «actor» 标注或者用一个带构造型的矩形。我在一些课程设计里看到有人把它画成了一个普通类还给它加了属性和方法这就混淆了内外部边界。第二个坑用例名写成名词。用例名必须是动宾短语“余额查询”虽然勉强能读但更规范的是“查询余额”“取款”比“取款交易”好。名词化之后评审的人会下意识把它理解成业务实体进而误以为它应该出现在类图里。提示如果你的用例数量超过十五个先别急着画图回头做一次合并。ATM 这种规模的系统主用例控制在十个左右最舒服多了说明你把系统内部的步骤当成对外功能了。4. 类图把名词变成可实现的骨架类图是五张图里信息密度最高、也最容易画错的一张。它要同时回答有哪些类、每个类有什么属性和操作、类之间是什么关系、关系的多重性是多少。我一般按“找名词 → 定职责 → 连关系 → 补细节”四步走。4.1 从需求文本里抽取候选类并分三层抽取方法很朴素把需求描述和用例描述里的名词全部圈出来然后逐个过滤。ATM 场景下圈出来的词大概是这些客户、银行卡、账户、ATM 终端、读卡器、出钞模块、凭条打印机、交易、取款交易、存款交易、转账交易、交易日志、银行主机、收据、钞票。过滤规则有三条第一同义词合并客户和持卡人是同一个概念合并为客户第二去掉系统边界外的外部实体银行主机不进类图或者以接口的形式出现第三去掉纯属性比如“金额”“时间”是属性不是类。过滤之后我习惯用边界类、控制类、实体类这三个层次来组织边界类Boundary负责与外部交互的界面或接口。ATM 场景下有ATMUI屏幕与键盘交互、CardReader读卡、CashDispenser出钞、ReceiptPrinter打印凭条。边界类的特点是操作多为输入输出属性很少。控制类Control负责协调一次用例的执行通常一个用例对应一个控制类。ATM 场景下有TransactionController总控管理会话与状态、WithdrawController取款流程协调、TransferController转账流程协调。控制类通常没有持久化属性生命周期短用完即弃。实体类Entity需要被持久化的业务数据。ATM 场景下有Card卡号、卡类型、状态、Account账号、余额、状态、Transaction流水号、类型、金额、时间、结果、Customer客户编号、姓名可选。这套分层的价值不只是好看。等你画顺序图的时候消息的流向天然是“边界类 → 控制类 → 实体类 → 外部接口”不会再出现 UI 直接操作数据库这种反模式。这也是为什么我坚持先画类图再画顺序图顺序图的质量很大程度上取决于类图的分层是否清晰。4.2 六种关系的箭头画法与区别辨析这是被问得最多的地方也是软考和答辩时的高频考点。我按“耦合强度从弱到强”排一下顺便说清判断标准。依赖Dependency虚线加普通箭头从使用方指向被使用方。含义是“一个类的变化会影响另一个类但这种影响是临时的、局部的”。典型场景是方法参数或局部变量。比如TransactionController的某个方法接收Card作为参数就构成依赖。判断问句这个类是否只在某个方法里用到了对方且不持有对方的引用作为字段关联Association实线可带普通箭头表示导航性也可不带箭头表示双向。含义是“一个类持有另一个类的引用作为字段”。比如Customer持有多个Account这是一条带多重性的关联。判断问句这个类是否有一个字段类型是对方聚合Aggregation实线加空心菱形菱形在“整体”那一端。含义是“整体与部分但部分可以独立存在”。ATM 场景里的例子不多我一般用一个偏牵强的ATM聚合CashDispenser因为出钞模块可以从这台机器上拆下来装到另一台机器上生命周期不绑定。组合Composition实线加实心菱形菱形在“整体”那一端。含义是“整体与部分部分不能脱离整体存在”。ATM 场景下的例子是Transaction组合TransactionDetail交易明细行交易记录不存在了明细行也就没有意义。泛化Generalization实线加空心三角从子类指向父类。ATM 里可以这样设计Transaction是父类WithdrawTransaction、DepositTransaction、TransferTransaction是子类继承交易流水号、时间、金额、状态这些公共属性各自扩展自己的特殊属性比如转账交易需要对方账号。实现Realization虚线加空心三角从实现类指向接口。ATM 里可以定义IBankHost接口声明queryBalance()、debit()、credit()等方法然后由BankHostAdapter实现它。这样做的理由是隔离外部系统的变化——如果哪天主机报文协议变了只需要改适配器控制类不受影响。聚合和组合怎么区分我给一个特别粗暴但好用的判断法问“如果整体被销毁部分还有没有独立存在的意义”。有就是聚合没有就是组合。很多人纠结“出钞模块到底算不算部分”其实这类问题没有标准答案只要你在文档里说明自己的判断依据就行建模是表达意图的工具不是数学证明。4.3 属性、操作、可见性与多重性属性写法是可见性 名称: 类型 默认值比如- balance: BigDecimal 0.00。金额这类字段我强烈建议用定点小数类型浮点数在金融场景下会产生精度问题这是实际开发中的教训建模时把这个类型选择写进类图能体现你对业务的理解。可见性符号公开-私有#保护~包内可见。类图的属性一律私有操作按需公开这是封装的基本要求也是答辩时容易被问的点。操作写法是可见性 名称(参数: 类型): 返回类型比如 withdraw(amount: BigDecimal): TransactionResult。多重性写在关联线的两端表达数量关系位置含义ATM 场景示例1恰好一个一张卡对应一个账户简化假设0..1零或一个一次交易最多对应一条冲正记录1..*一个或多个一个客户拥有至少一个账户0..* 或 *零个或多个一个账户有多条交易流水多重性不是装饰它直接影响实现。1..1意味着一对一外键1..*意味着一对多集合0..1意味着可空字段。把这些写清楚后面写代码或者建库表的时候基本不用再想。4.4 完整类图清单与代码映射把上面的内容收敛成一张清单方便直接抄作业类名层次关键属性关键操作ATMUI边界screenStateshowMenu()、readInput()、showMessage()CardReader边界hasCardreadCard()、ejectCard()、captureCard()CashDispenser边界cashLeveldispense(amount)、checkCash()ReceiptPrinter边界paperLevelprint(receipt)IBankHost接口-queryBalance()、debit()、credit()BankHostAdapter实现类hostEndpoint实现上述三个方法TransactionController控制sessionIdstartSession()、verifyPin()、endSession()WithdrawController控制-withdraw(amount)Customer实体customerId、name-Card实体cardNo、cardType、statusisLocked()、lock()Account实体accountNo、balance、statusgetBalance()、debit()、credit()Transaction实体txnId、type、amount、time、result-WithdrawTransaction实体-继承 TransactionTransferTransaction实体targetAccountNo继承 Transaction映射到 Java 代码大致是这样我在文档里通常附一小段方便读者理解类图不是纸面游戏public abstract class Transaction { private String txnId; private BigDecimal amount; private LocalDateTime time; private String result; // SUCCESS / FAILED / REVERSED public abstract void execute(); } public class WithdrawTransaction extends Transaction { private String cardNo; Override public void execute() { // 校验余额、调用主机扣账、触发出钞 } } public class Account { private String accountNo; private BigDecimal balance; public boolean hasEnough(BigDecimal amount) { return balance.compareTo(amount) 0; } public void debit(BigDecimal amount) { this.balance this.balance.subtract(amount); } }这段代码和类图是一一对应的抽象类对应泛化关系hasEnough对应类图里的操作BigDecimal对应前面说的类型选择。我建议在文档里做这个映射答辩时被问“你这个类设计怎么落地”直接把代码翻出来比解释十分钟都有用。5. 顺序图与协作图同一交互的两种视角顺序图和协作图最容易让人困惑因为它们描述的是同一件事。我理解它们的关系就像同一段对话的“录音时间轴”和“通话关系图”——录音能看出谁先说话、每句话之间隔了多久关系图能看出谁和谁在通话、一共传了几轮消息。表述角度不同信息量并不重复。5.1 顺序图的四大必备元素生命线Lifeline是一个矩形加一条向下的虚线矩形里写对象名格式是对象名:类名比如:WithdrawController表示匿名对象。生命线上的对象要么是在这次交互开始前就存在的要么是中途被创建出来的。顶部第一个对象通常是参与者的代表比如客户这个角色我在图里一般用:Customer或者直接写客户加火柴人。激活条Activation Bar是生命线上的窄矩形表示对象在这段时间内正在执行操作。它是可选的但加上之后图面的层次感会好很多。我个人的习惯是控制类一定画激活条实体类只在执行具体方法时画短的激活条边界类可以不画。判断方法很实用——激活条的高度大致对应方法执行耗时越长的说明这个方法越重设计时值得关注。消息Message分四种画法要分清消息类型画法含义ATM 示例同步调用实线加实心箭头调用方等待返回UI 调 withdraw()异步消息实线加开放箭头调用方不等返回日志异步上传返回消息虚线加开放箭头返回值或控制权余额返回给控制类自调用折线加实心箭头调用自身方法重试计数自增组合片段Combined Fragment是表达条件与循环的关键常用的有alt条件分支、opt可选、loop循环、par并发。ATM 里最常用的是alt比如余额校验那一步写成alt [余额充足] ... [余额不足] ...把两条路径画在一个片段里比画两张图清楚得多。5.2 协作图的消息编号与链协作图通信图的元素只有三种对象、链、消息。对象画法跟顺序图一样链是两个对象之间的实线消息沿着链的方向标注格式是序号 消息名(参数)。编号规则是协作图的灵魂标准做法是使用嵌套编号表达调用层次1 插入卡片 1.1 读取卡号 1.2 返回卡信息 2 输入密码 2.1 请求密码校验 2.1.1 查询账户状态 2.1.2 校验密码 2.2 返回校验结果 3 选择取款 3.1 校验余额 3.2 请求扣账 ...这个编号体系的好处是2.1.1一眼能看出它是2.1调用内部发出的第一个消息。加了序号之后协作图能表达严格的时间顺序这一点很多人不知道以为协作图只能看结构。条件消息用一对方括号包住条件比如3.1 [余额充足] 请求扣账。循环用*前缀比如*[每张待出钞票] 检测钞票状态。这些符号在 StarUML 里直接在消息名里写就行工具不会校验你的语法但评审的人看得懂。5.3 顺序图转协作图的四条规则这个转换我做过很多次可以总结成四条机械规则照着做基本不会错。第一对象集合完全一致顺序图上出现的每一个生命线协作图上都必须有对应的对象一个不能多、一个不能少。第二顺序图上的每一条消息对应协作图上沿链的一条标注消息名称和参数不变。第三用嵌套编号取代垂直位置来表达时间顺序。顺序图靠“谁在上面谁先发生”协作图靠编号。转换时按顺序图从上到下遍历消息遇到调用嵌套就加一级编号。第四控制流语义一致组合片段需要拆解。顺序图里的alt片段在协作图上通过给消息加条件标记来表达做不到完全等价所以协作图更适合表达主成功路径复杂条件分支还是留给顺序图。我用一个更直观的方式说明取款这条主路径顺序图的读法是“从最上面的消息往下读”协作图的读法是“从编号 1 开始遇到带小数点的就把父编号的调用展开”。两者描述的是同一批消息只是索引方式不同。5.4 取款成功与余额不足两条路径实录用文字把顺序图重新组织一遍方便你对照着画。主成功路径客户向ATMUI提交取款金额ATMUI调用TransactionController.withdraw(amount)。控制器先向Account发起hasEnough(amount)校验返回 true 后调用IBankHost.debit(accountNo, amount)。主机返回成功后控制器依次调用CashDispenser.dispense(amount)和ReceiptPrinter.print(receipt)最后调用CardReader.ejectCard()并向ATMUI返回成功结果。余额不足路径分支发生在hasEnough返回 false 的那一刻。控制器走alt的第二个分支直接构造一条失败结果调用ATMUI.showMessage(余额不足)不触发出钞也不调用主机扣账但会记一条失败流水。这里有个细节值得注意失败流水也要记录很多同学只顾着画成功路径漏了这条导致活动图里的日志动作和顺序图对不上。主机超时路径IBankHost.debit调用后用alt判定是否在超时时间内返回。超时的情况下控制器要做两件事一是查询交易状态防止“扣账成功但返回超时”的经典问题二是根据查询结果决定是冲正还是继续出钞。这条路径能体现你对分布式系统一致性的理解是答辩的加分项。同样的三条路径画成协作图编号会是这样1 取款(金额) 1.1 余额校验(金额) 1.2 [余额充足] 请求扣账(账号, 金额) 1.2.1 [扣账成功] 出钞(金额) 1.2.2 [扣账成功] 打印凭条() 1.3 [余额不足] 提示余额不足()能看出来协作图在表达分支时确实不如顺序图直观但它把“谁调用谁”画得更清楚尤其是当对象数量多、调用关系交叉的时候协作图的链能让人一眼看出耦合关系。6. 活动图把业务规则画成流程活动图是我在需求沟通阶段用得最多的一张图因为它最接近业务人员能看懂的语言。但要注意活动图容易画成流程图从而失去 UML 语义。6.1 活动图与流程图的三个本质区别第一活动图有分叉与汇合Fork / Join表达并发流程图通常只有分支。ATM 的加钞流程里“打印加钞凭条”和“更新余钞记录”是可以并发的用粗黑线分叉再汇合到下一步这在流程图里表达不了。第二活动图有对象流Object Flow可以把“取款交易记录”这个对象作为数据在动作之间传递用虚线箭头指向对象节点表示。这让活动图不只是控制流还能表达数据流。第三活动图可以有泳道Swimlane把动作按负责的主体分组。ATM 取款流程可以分三条泳道客户、ATM 终端、银行主机。每条泳道只放属于自己职责的动作跨泳道的连线就是一次交互。这比纯粹的流程图多了一层“责任归属”的表达。6.2 泳道划分与常见画法错误泳道划分的原则是按角色或系统组件分不按步骤分。我见过有人把泳道命名为“第一步、第二步、第三步”这就完全错了那是时序不是责任。ATM 取款的活动图我用三条泳道动作分布是这样客户泳道插入银行卡、输入密码、选择取款金额、取走现金、取走凭条ATM 终端泳道读卡、显示菜单、校验输入格式、出钞、打印凭条、退卡、记录交易日志银行主机泳道校验账户状态、校验余额与限额、扣账、返回结果注意“校验余额”这个动作放在主机泳道因为余额数据在主机侧ATM 本地不持有权威数据。这个划分是有实际意义的——如果余额校验放在 ATM 侧那分布式一致性问题就没法解释。答辩时被问到“为什么余额校验在主机做”这个泳道划分就是你的答案。判定节点用菱形一个入口、多个出口出口上标注条件。取款流程里的判定节点至少有四个密码是否正确、余额是否充足、是否超单笔限额、扣账是否成功。这四个节点如果漏画活动图就变成了一条直线失去了建模价值。分叉汇合用粗黑线。出钞和打印凭条这两步可以并行用分叉线分出去两者都完成后汇合再执行退卡。汇合线上不能标条件这是 UML 规则因为汇合是同步等待不是条件选择。终止节点要区分两种普通结束节点空心圆套实心圆表示这条路径结束活动流还可以有别的路径在跑活动终止节点实心圆外套空心圆表示整个活动结束其他路径全部终止。吞卡场景就适合用活动终止节点因为卡被吞了这次会话就彻底结束了。6.3 取款全流程活动图逐节点拆解我把取款流程按顺序过一遍标注每个节点的类型和关键说明。起点是活动开始节点位于客户泳道。第一个动作是“插入银行卡”这是一个普通动作节点。接着在 ATM 终端泳道执行“读取卡片信息”这里可以挂一个对象节点输出Card对象。然后进入判定“卡片是否有效”无效则走“退卡并提示”路径有效则继续。接下来是“输入密码”进入判定“密码是否正确”。错误路径上有一个计数器用loop语义表达重试达到三次进入“吞卡处理”路径走向活动终止节点。正确则进入“显示功能菜单”客户选择取款后执行“输入取款金额”。金额输入后在银行主机泳道执行“校验账户状态与余额”这里是一个判定有两个出口“余额不足”走向提示路径并回到菜单“余额充足”继续。紧接着是第二个判定“是否超限额”逻辑同上。通过校验后主机执行“扣账”这里是关键的第三个判定“扣账是否成功”。失败则提示并回到菜单成功则进入 ATM 终端泳道用分叉线同时启动“出钞”和“打印凭条”两个动作然后汇合。汇合后判定“客户是否取走现金”如果超时未取触发“回收现金并冲正”路径这条路径需要回到主机泳道执行冲正操作再记录交易日志。最后是“退卡”“记录交易日志”走向普通结束节点。日志记录这个动作放在最后但如果是吞卡场景日志要提前记录这属于异常路径的处理顺序差异需要在图里体现出来。注意活动图里的每一个判定节点都要能在顺序图里找到对应的alt片段也要能在用例描述表格里找到对应的扩展场景。三张图对不上的地方一定是某一张图画错了。7. 常见问题与排查速查表这一节是我这几年被问得最多的内容整理成速查表遇到问题直接对照。7.1 语义层面的六个典型错误问题现象根因修正方法用例图里出现类混淆了“系统做什么”和“系统有什么”把名词性节点移出改成动宾短语用例include 和 extend 箭头方向反了记混了依赖方向include 是基用例指向被包含extend 是扩展指向基用例类图里画了消息时序混淆静态与动态时序信息移到顺序图类图只保留结构与关系顺序图没有返回消息漏画返回值每个同步调用配一条虚线返回除非明确无需返回协作图消息没有编号无法表达时序使用嵌套编号父调用用整数子调用用小数活动图只有一个判定节点把流程画成了直线逐个检查扩展场景每个分支都要有判定我再补充一个高频问题多重性标注方向搞反。多重性标在关联线的两端标注的是“对面那个类有多少个实例和当前类的一个实例相关”。Customer 1 —— 0..* Account这行读法是“一个客户对应零到多个账户”不是“一个账户对应零到多个客户”。读法搞反了方向自然就反了。7.2 工具使用层面的坑StarUML 的类图里聚合和组合的菱形方向经常被画反。记住原则菱形永远在整体容器那一端。Transaction组合TransactionDetail菱形画在Transaction这一侧。Visio 画 UML 类图时默认的连线是“关联”要改成泛化必须手动在形状库里换箭头。我见过整张图所有关系都是普通关联的评审时直接被判不合格。draw.io 导出图片时建议选 SVG 或者高分辨率 PNG否则打印出来线条发虚。另外它的自动路由有时候会把连线绕得很奇怪可以手动拖动锚点调整。Eclipse 查看类图通常需要装 ObjectAid 或者 eUML2 之类的插件装完之后右键类文件可以生成。要注意的是生成的图默认会显示所有字段和方法大型类会非常大建议在插件设置里关掉 getter/setter 和私有字段。IDEA 生成类图在项目树里选中包或类右键选择 Diagrams可以选显示字段、方法、构造器、属性、继承关系。生成后可以手动把不需要的类从图里剔除剔除不会影响代码只是视图过滤。这个功能做代码走查特别有用尤其是接手一个老项目的时候先看类图能快速建立整体认知。7.3 软考与课程设计的答题要点软考中级软件设计师里UML 相关题目常见形式是给一段需求描述让你选择正确的图或补全图中的空缺。答题时有两个技巧。第一看关系类型的排除法。题目里出现“必须”“每次都要”这类词基本对应 include出现“可选”“在某种情况下”“如果需要”这类词基本对应 extend。这个对应关系非常稳定能解决大部分选择题。第二看箭头的方向和线型。实心箭头配虚线是 include 和 extend 的通用线型方向靠语义判断空心三角配实线是泛化配虚线是实现菱形在整体端空心的聚合、实心的组合。把这些记牢画图题基本不丢分。课程设计答辩时老师最爱问的三个问题是为什么这个用例用 extend 而不是 include、聚合和组合在这个系统里怎么区分、顺序图里那个 alt 片段的两个分支在代码里怎么实现。前两个用前面的判断法回答第三个建议直接拿出代码或者伪代码比讲理论有效得多。8. 我自己的几点实操体会最后说几个不太上教材、但实际做的时候很有用的经验。关于画图顺序我强烈建议按“用例图 → 活动图 → 类图 → 顺序图 → 协作图”来做而不是教材上的顺序。原因是活动图能最快帮我把业务规则梳理清楚尤其是那些异常分支用泳道图走一遍比写文字快得多活动图画完了类图要抽哪些类和操作基本就定了因为活动图里的每个动作和数据对象都是候选顺序图的篇幅最大放到后面画前面的基础打好了画起来就是机械翻译。关于协作图要不要画我的看法是如果是为了课程作业拿分题目要求了就必须画如果是实际项目文档协作图的价值在对象数量多、调用关系交叉的场景下才明显一般业务系统用顺序图就够了别为了凑页面数硬画。但有一种情况例外——如果你需要在一张图里同时展示多个场景的对象连接关系协作图确实比画五张顺序图省事。关于类图的粒度我的经验是“一个类里的操作数量控制在五到八个之间”。超过十个说明这个类承担了太多职责考虑拆成控制类和实体类少于三个说明它可能只是别的类的一个属性考虑合并。ATMUI这种类容易膨胀因为它要处理所有屏幕交互实际设计里可以按功能拆成WithdrawUI、QueryUI、TransferUI但课程设计层面保持一个ATMUI加参数化方法也说得过去关键是在文档里说明你的取舍。关于顺序图里要不要画边界类我一开始也觉得边界类是废话直接画控制类调实体类不就行了。后来发现不行因为参与者的消息必须落在某个边界对象上否则生命线从哪开始说不清楚。而且边界类恰恰是变更最频繁的部分界面改版、换硬件把它显式建出来变更影响分析才有依据。这个道理在真实的维护工作里体会更深。还有一个细节我在画顺序图时习惯给每条消息加编号虽然 UML 标准里顺序图不强制标号但加了之后和协作图对照非常方便评审的人也能顺着编号读。这个小习惯帮我省了很多解释时间。如果你打算把这套图作为作品集或者课程作业提交建议再补两样东西一是每个用例的用例描述表二是关键类的代码骨架。图是抽象代码是落地两样放一起说服力完全不一样。我当年就是因为补了代码骨架答辩时被问到的每个设计决策都能落到实处。