ATM系统UML五视图建模实战:从需求分析到用例图、类图、顺序图、协作图与活动图
2026/9/18 1:25:08 网站建设 项目流程

1. 为什么我建议拿 ATM 系统练手 UML 五视图

ATM 系统的用例图、类图、顺序图、协作图、活动图设计,几乎是每一本面向对象分析与设计教材都绕不开的经典案例,也是软件设计师考试里反复出现的建模题型。它好在哪?好在业务边界足够清楚——插卡、验密、取款、存款、转账、查询、退卡,一圈走下来闭环完整;又好在异常分支足够丰富——密码错三次吞卡、余额不足、钞票不足、通讯超时、客户中途取消,这些分支恰好能把顺序图里的条件消息、活动图里的判定节点、用例图里的 extend 关系全部逼出来。更关键的是,ATM 不需要你懂金融业务才能理解,任何人都用过机器,读者看到图能立刻判断你画得对不对。

我带过几届学生的课程设计,也帮朋友改过软考中级软件设计师的真题答案。一个很普遍的现象是:五张图画了,但每张图都在讲同一件事,用例图里塞了对象,类图里写了操作顺序,顺序图画成了流程图。这种“图会画、语义不对”的问题,根子不在工具,而在没有想清楚每张图各自的职责边界。这篇就按我自己实际做的顺序,从需求梳理一路讲到五张图落地,把每个取舍背后的理由、工具的实操细节、以及踩过的坑都摊开说。

1.1 一张图说不清的事,五张图刚好分完

面向对象建模的本质是“多视角切片”。一个系统太复杂,任何单一视图都无法同时表达清楚“谁在用”“系统有什么”“对象怎么协作”“消息什么顺序”“流程怎么走”。UML 的价值就在于它提供了若干正交的视角,每个视角只回答一类问题,合起来才构成完整描述。

我通常用一个类比来解释:盖一栋楼,你会看到总平面图(谁从哪进、有哪些功能区)、结构图(梁柱板墙怎么配筋)、水电图(管线怎么走)、施工进度图(先干什么后干什么)。没人会拿结构图去说明消防通道在哪。UML 五视图也是这个道理:用例图是“总平面”,类图是“结构”,顺序图和协作图是“管线走向的两种画法”,活动图是“施工进度与分支决策”。

所以第一件要建立的心智是:不要让一张图承担它不该承担的职责。类图里出现“先插卡再验密”这种时序描述,就是越界;顺序图里画一堆类之间的静态继承关系,也是越界。判断方法很简单——问自己这张图回答的核心问题是什么,凡是超出这个问题的信息,删掉。

1.2 五张图的分工边界对照

把边界写成表格最直观,我给学生改图时基本就按这张表逐条核对。

图名核心回答的问题主要元素静态/动态典型交付阶段
用例图谁在用系统、系统对外提供哪些功能参与者、用例、系统边界、关系静态(偏需求)需求分析
类图系统内部有哪些类、类之间是什么关系类、属性、操作、六种关系、多重性静态设计初期
顺序图一次交互中对象按什么时间顺序发消息生命线、激活条、消息、组合片段动态详细设计
协作图一次交互中对象之间通过哪些链传消息对象、链、带序号的消息动态详细设计
活动图业务流程的控制流与并发、分支怎么走动作、判定、分叉汇合、泳道动态(偏流程)需求/设计均可

这张表里最容易被忽视的一行是协作图。很多教材和工具已经把它改叫“通信图”(Communication Diagram),UML 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是父类,WithdrawTransactionDepositTransactionTransferTransaction是子类,继承交易流水号、时间、金额、状态这些公共属性,各自扩展自己的特殊属性(比如转账交易需要对方账号)。

实现(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实体-继承 Transaction
TransferTransaction实体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这种类容易膨胀,因为它要处理所有屏幕交互,实际设计里可以按功能拆成WithdrawUIQueryUITransferUI,但课程设计层面保持一个ATMUI加参数化方法也说得过去,关键是在文档里说明你的取舍。

关于顺序图里要不要画边界类,我一开始也觉得边界类是废话,直接画控制类调实体类不就行了。后来发现不行,因为参与者的消息必须落在某个边界对象上,否则生命线从哪开始说不清楚。而且边界类恰恰是变更最频繁的部分(界面改版、换硬件),把它显式建出来,变更影响分析才有依据。这个道理在真实的维护工作里体会更深。

还有一个细节,我在画顺序图时习惯给每条消息加编号,虽然 UML 标准里顺序图不强制标号,但加了之后和协作图对照非常方便,评审的人也能顺着编号读。这个小习惯帮我省了很多解释时间。

如果你打算把这套图作为作品集或者课程作业提交,建议再补两样东西:一是每个用例的用例描述表,二是关键类的代码骨架。图是抽象,代码是落地,两样放一起,说服力完全不一样。我当年就是因为补了代码骨架,答辩时被问到的每个设计决策都能落到实处。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询