ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ATM系统UML五张图设计:用例图到活动图的一致性校验实战

ATM系统UML五张图设计:用例图到活动图的一致性校验实战 1. 为什么要拿 ATM 练这五张图一次设计复盘的开场ATM 系统的用例图、类图、顺序图、协作图、活动图这套组合几乎是所有面向对象分析与设计课程、软考中级软件设计师真题、企业内部 UML 培训的标准练手题。原因不复杂ATM 的业务规模足够小一个人半天就能把需求读完但它的结构又足够完整有角色、有状态、有异常分支、有并发约束五张图各自能画出实打实的内容不像计算器那样画到第三张图就没东西可画了。我自己第一次画这套图是在做课程设计的时候当时最大的误区是把五张图当成五个独立的作业画完用例图画类图画完类图画顺序图前后几乎不回头看。结果答辩时老师只问了一句你这个withdraw()方法在类图里参数有几个顺序图里传了几个整套图就露馅了。后来带新人、帮同事审图我发现这个问题极其普遍——大家不是不会画某一张图而是不会让五张图互相咬合。这篇内容我想解决的就是这件事把 ATM 这个题目从需求边界开始一路推到五张图全部成型并且让它们彼此自洽。适合三类人看——准备软考或者课程设计、需要交一套完整图的学生刚转做需求分析、需要用 UML 跟开发对齐的工程师以及画过图但总觉得图是画完了心里没底的人。每张图我都会说清楚它回答什么问题、关键决策怎么定、以及我在实际操作里踩过的具体坑。先立一个贯穿全文的原则UML 图不是美术作品是沟通工具。判断一张图合格的标准不是好不好看而是开发看了能不能直接写代码测试看了能不能直接设计用例。带着这个标准去看很多纠结比如该不该加这个属性会立刻有答案。1.1 一台 ATM 真正的需求边界很多人一上手就画取款、存款、转账、改密码、查询余额画完就结束了。但如果你真的把这五张图画到底会发现边界问题会反复冒出来插卡、读卡、退卡算不算用例我倾向于不算它们是交互动作不是有业务价值的目标。用户的目标是取到钱插卡只是达成目标的一步。验密失败三次吞卡这是谁的用例它是取款用例的异常流不是独立用例。银行主机Bank Host是不是参与者是而且是很多人会漏掉的那个。它不坐在机器前面但它是系统外部、需要交互的对象正确定位是辅助参与者。加钞、清机、对账这些运维操作要不要进图如果是教学场景可以简化掉但如果是真实项目必须画因为它们是另一类参与者运维人员的核心用例。提示需求边界定不下来的时候就问一句谁从中获得了可观察的价值。有明确受益方、且结果是可观测状态变化的才是用例。我一般会先写一份不超过 30 行的需求清单把必须支持和明确不支持分开列再开始画图。这一步花十分钟能省掉后面三小时的返工。1.2 五张图各自在回答什么问题这是整套设计的骨架理清了就不会画重也不会画漏图类型回答的核心问题主要读者变化的频率用例图系统给谁用、提供哪些服务、边界在哪甲方、产品、测试极低需求稳定后基本不动类图系统内部有哪些对象、它们的属性和职责怎么分配开发、架构中随着设计细化会持续调整顺序图一次具体交互中消息按什么时间顺序流动开发、联调高每个重要场景一张协作图通信图同样的交互里对象之间的结构连接关系如何开发、评审与顺序图同步更新活动图业务流程的整体走向、判断分支与并发业务、测试、开发中流程变更时更新注意顺序图和协作图这两行它们描述的是同一次交互只是视角不同。UML 2.x 把协作图正式改名为通信图Communication Diagram但工程实践中协作图这个叫法依然通行软考真题里两种说法都出现过。它们不是画完一个再画另一个的关系而是同一份信息换一种表达。1.3 我习惯的推进顺序教科书一般的顺序是用例图 → 类图 → 顺序图 → 协作图 → 活动图。我实际的做法略有不同先写用例描述不是图把主流程和异常流用文字写清楚大概每用例 15 行。画用例图同时把用例描述当作文档挂上去。画活动图。是的我会先用活动图梳理业务流程因为活动图最接近人的直觉能快速发现这个流程有个分支我压根没想清楚。从用例描述和活动图里捞名词画类图。挑 3 到 5 个最重要的场景画顺序图全部选有交互、有分支的不选纯查询。对其中一到两个场景补充协作图用来验证对象之间的连接关系是否合理。这个顺序的好处是类图是在业务理清之后才画的返工概率低很多。坏处是看起来不那么标准如果交作业要求严格按顺序那就把活动图挪到后面不影响结果。2. 用例图把谁在用 ATM这件事拆到位2.1 参与者识别三类人加一个系统ATM 场景里的参与者我最终定下来是四个客户Customer主要参与者。取款、存款、转账、查询、改密。运维人员Operator辅助参与者。加钞、清机、打印流水、设置机器参数。银行主机Bank Host辅助参与者、外部系统。验证账户、记账、返回余额。管理员Administrator维护客户信息、处理吞卡、冻结账户。实操中的坑在于新手常常把银行卡也画成参与者理由是没有卡就不能操作。这是典型混淆了参与者和被操作对象。银行卡是系统处理的数据载体不是主动发起交互的一方。判据很简单——参与者必须能发起或响应交互卡做不到这两件事中的任何一件。另一个高频争议是银行主机要不要画。不画的话图会很干净但会丢掉一个关键事实ATM 不是孤立的它的核心业务都依赖远程验证。这个事实如果不体现在用例图上后面画顺序图时你会突然发现多出一条外部消息图就对不上了。所以我的建议是画并且用不同的颜色或加一个「外部系统」的标注区分开。2.2 用例粒度取款是一个还是三个取款这个用例我见过三种画法画成一个取款用例。最简但丢掉了验密出钞这些关键子过程。画成插卡、验密、输入金额、出钞、退卡五个用例。太碎每个都不具备独立业务价值。画成取款主用例 身份验证被包含 打印凭条被扩展。我的选择是第三种理由如下身份验证应该用 include包含。因为取款、存款、转账、查询、改密这五个用例都必然要验证身份没有例外。这种无条件、每次都发生的复用关系就是 include 的标准适用场景。画成一条带include的虚线箭头从各用例指向身份验证。打印凭条应该用 extend扩展。因为它只在用户选择打印时发生是可选行为。extend 箭头方向是从扩展用例指向基础用例很多人会画反记住一句话谁扩展谁箭头就从谁出发——打印凭条扩展取款所以箭头从打印凭条指向取款。吞卡我倾向于不作为独立用例而是作为身份验证的异常流写在用例描述里。因为用户不会主动去吞卡它是系统的响应不是用户目标。2.3 include 和 extend 的边界怎么划这是用例图里最容易被扣分的地方。我总结了一个三段式判断法实操中很好用提问被复用的那段行为是每次都执行吗是 → 大概率是 include。再问它是不是基础用例的可选、条件性行为是 → 大概率是 extend。最后问它自己单独触发时对用户有意义吗有 → 它可能是独立用例只是恰好被包含。按这个流程过一遍身份验证每次都执行没有独立触发场景 → include。打印凭条条件性执行不打印也不影响取款成功 → extend。转账有独立价值能单独触发 → 独立用例不挂 include。还有一个更隐蔽的坑include 可以链式但别链太长。我见过取款include身份验证身份验证include读卡读卡include检测卡状态四层下去图都没法看了。三层以内是我的舒适区超过三层就该考虑合并。2.4 一份可以直接抄的用例清单下面这张表是我每次画 ATM 用例图都会过一遍的基线你可以直接拿去改参与者用例说明与其他用例的关系客户取款从账户提取现金include 身份验证extend 打印凭条客户存款存入现金或支票include 身份验证extend 打印凭条客户转账账户间划转include 身份验证、include 账户校验客户查询余额查看可用余额include 身份验证客户修改密码变更卡密码include 身份验证运维人员加钞补充现金无运维人员清机对账核对账面与实物include 打印流水运维人员设置参数调整单笔限额等无管理员处理吞卡登记并归还卡片无管理员冻结账户风险处置无银行主机账户验证响应 ATM 的验证请求系统侧用例银行主机记账记录交易系统侧用例这张表画成图之后大概是四组关系、十二条用例连接。图不算复杂但每一个连接我都能说出理由这在评审时比图好看重要得多。3. 类图从名词清单到能落地的实体关系3.1 找类从用例描述里捞名词但要过滤标准做法是从用例描述里捞名词短语得到候选类列表再筛选。ATM 场景捞出来大概是这些客户、银行卡、账户、交易、ATM 终端、银行主机、现金、凭条、密码、金额、余额、日志。筛选规则我用三条它是系统需要持久化或维护状态的吗账户、卡、交易、日志 → 是。金额、余额 → 不是它们是属性。它有行为吗客户取款请求的发起有凭条基本没有行为只是数据 → 降级为属性或值对象。它在图里会不会只被当作参数传递会的话就不是类。筛完之后我留下的核心类是Customer、Card、Account、Transaction、ATM、BankHost、CashDispenser出钞模块、CardReader读卡模块。这里有个设计决策值得展开ATM 到底应该是一个类还是拆成多个硬件模块类如果只是交作业一个ATM类装下所有方法就够了。但如果你想让类图有说服力拆成模块类更合理因为读卡器、出钞机、凭条打印机的行为差异很大混在一个类里会导致这个类有几十个职责违背单一职责原则。我的折中方案是ATM作为门面类Facade内部持有CardReader、CashDispenser、ReceiptPrinter三个模块对象业务逻辑放在ATM里硬件细节委托给模块。这样画出来层次清楚也符合真实设备的软件结构。3.2 属性与方法的颗粒度新手最容易在两个极端之间摇摆要么只写类名不写成员要么把getCardNumber()、setCardNumber()这种显然是 IDE 自动生成的访问器全写上去。我的建议属性只写业务相关的。Account写accountNo、balance、accountType、status不写createTime、updateTime这类审计字段或者统一用注释说明审计字段省略。方法只写能体现职责的。CardReader.readCard()、CashDispenser.dispense(amount)、Account.debit(amount)。访问器全部省略。返回值和参数标注要跟顺序图对齐。这是后面交叉校验的基础现在偷懒后面一定返工。一个具体的例子。Account.debit(amount)这个名字我最初写的是withdraw(amount)后来改成了debit。原因是withdraw语义上是客户取款这个业务动作属于ATM层debit是从账户扣减这个会计动作属于Account层。把业务动作和会计动作分开命名看类图的人能立刻明白职责边界在哪。3.3 六种关系的区分与画法这是类图的核心考点也是软考真题里反复出现的内容。我把六种关系统一放在一张表里说清楚关系符号语义ATM 场景实例泛化继承空心三角 实线is-aSavingsAccount继承Account实现空心三角 虚线实现接口BankHost实现IAccountValidator关联实线长期结构关系Customer拥有Card聚合空心菱形 实线整体-部分可分离ATM与CardReader可替换组合实心菱形 实线整体-部分同生命周期Transaction与TransactionLog依赖虚线箭头临时使用ATM依赖BankHost完成验证聚合和组合的区分是提问率最高的。我的判断口诀是拆了整体部分还活不活活 → 聚合不活 → 组合。在 ATM 里ATM和CardReader机器报废了读卡器可以拆下来装到另一台机器上理论上所以是聚合。Transaction和它的明细记录交易被删除明细就没有存在意义了所以是组合。这个判断不依赖抽象标准只依赖你的业务假设所以在论文或者答辩时一定要把假设说出来否则会被追问。3.4 多重性与导航性多重性Multiplicity标在关联线两端表示参与方数量。ATM 场景里几个常见的Customer1 —— 0..*Card一个客户可以有多张卡这是最贴近现实的。有些教材简化成 1 对 1也别算错但要说明这是简化假设。Card1 —— 1Account一张卡绑定一个主账户。如果要支持一卡多账户比如借记卡可以挂多个币种账户就得改成 1 对 0..*。Account1 —— 0..*Transaction一个账户有多笔交易流水。ATM1 —— 1CashDispenser一台机器一个出钞模块。导航性Navigability用箭头表示谁能访问谁。很多人觉得这个可画可不画但我的经验是画了能显著减少歧义。比如Transaction指向Account的单向导航明确说明交易对象持有账户引用而账户不需要反向持有交易列表查询流水由另一个服务负责。这条信息对开发非常有用因为直接决定了要不要在Account里加ListTransaction。注意导航性箭头和依赖箭头都是虚线配箭头的形态容易混。区分方式是看箭尾——依赖关系查不到具体连接结构而导航性是已经有结构关系关联再加方向。实操中我会给关联线加方向箭头的同时保留实线避免和依赖混淆。3.5 账户与卡的分层一个反复调整的决策我第一版类图里Card和Account是合并的因为当时觉得卡号就是账号。后来发现三个问题卡可以被挂失、注销、更换但账户不受影响。生命周期不同。密码属于卡不属于账户。同一账户可以有多张卡各自密码可以不同。卡片有物理状态正常、挂失、冻结、过期账户有金融状态正常、透支、冻结。两类状态机的驱动源不同。所以我最终拆成了两个类Card持有cardNo、password、status、expireDateAccount持有accountNo、balance、accountType、status。两者之间是 1 对 1 关联简化版。这个拆法的价值在于当你画顺序图时验密这一步会自然地落在Card上而扣款会自然地落在Account上消息的接收者一目了然。如果合并成一个类你会纠结验密和扣款到底该不该放在同一个对象上顺序图也就跟着含糊了。再看一个具体的类定义用代码块示意成员结构不是真实代码只是 UML 文本化表达class Account { - accountNo : String - balance : Decimal - accountType : AccountType - status : AccountStatus getBalance() : Decimal debit(amount : Decimal) : Boolean credit(amount : Decimal) : Boolean } class Card { - cardNo : String - password : String - status : CardStatus - expireDate : Date verifyPassword(input : String) : Boolean isExpired() : Boolean }注意debit和credit都返回Boolean表示扣款/入账是否成功。这个返回值在顺序图里会作为返回消息出现是保证类图和顺序图对得上的关键细节。很多图对不上根源就是画类图时随手写了void。4. 顺序图把一次取款的时间线拉直4.1 生命线与消息的基本写法顺序图的横向是参与交互的对象纵向是时间。每个对象下面画一条虚线叫生命线Lifeline生命线上出现的长条叫激活条Activation Bar表示对象正在执行操作。对象命名有两种写法我推荐第二种匿名形式:ATM具名形式atm : ATM或a1 : Account具名形式虽然多打几个字但在有多个同类对象时会清楚很多。比如转账场景里有两个Account实例写成fromAccount : Account和toAccount : Account一眼就能看懂谁是谁。消息类型我用得最多的是三种同步消息实线 实心箭头。调用方会等待返回。返回消息虚线 开放箭头。表示控制权和数据返回。自调用消息指向自身的箭头。表示对象内部的方法调用。异步消息实线 开放箭头在 ATM 里用得少但有一个地方用得上ATM 向银行主机发起验证后不阻塞等待而是等主机回调。如果按同步建模会导致生命线画得很难看需要大量嵌套激活条。4.2 取款主流程逐步拆解下面这段是我最终定稿的取款主流程十一步客户 →atm : ATMinsertCard()atm→reader : CardReaderreadCard()reader→card : CardgetCardInfo()返回卡信息atm→customer : CustomerpromptPassword()客户 →atmenterPassword(pwd)atm→card : CardverifyPassword(pwd)card→atmreturn true/falseatm→host : BankHostvalidateAccount(cardNo)host→atm返回账户状态atm→customer : CustomerpromptAmount()客户 →atmenterAmount(amount)atm→host : BankHostauthorizeWithdraw(cardNo, amount)host→atm授权结果atm→dispenser : CashDispenserdispense(amount)dispenser→atm出钞结果atm→host : BankHostcommitTransaction(...)atm→printer : ReceiptPrinterprintReceipt()可选用 opt 片段包住atm→card : CardejectCard()这张图里最值得说的是第 12 步和第 16 步为什么拆开。授权authorize是冻结额度提交commit是真正记账。中间隔着一个出钞动作。如果出钞失败只需要调用rollback释放冻结而不需要冲正一笔已经落地的账。这个设计在分布式事务里是常规做法画在顺序图里能体现你对业务的理解深度。第二个值得说的是第 6 步的验密放在Card上而不是ATM上。这也是前面类图分层决策的直接结果。如果类图里Card没有verifyPassword方法顺序图这一步就只能落在ATM上会显得职责混乱。4.3 组合片段alt、opt、loop 的正确用法组合片段Combined Fragment是顺序图里的控制结构ATM 场景常用四种片段语义ATM 场景alt多分支条件互斥密码正确 / 密码错误opt可选条件成立才执行用户选择打印凭条loop循环密码重试最多三次break中断卡片过期时直接终止流程密码重试的写法容易出错。正确做法是用loop(min1, max3)包住提示密码 → 输入 → 验证三段然后在alt里判断结果。我见过有人用loop(3)直接把三次全部画出来图长得没法看而且无法表达中途成功就退出这个语义。alt的分支不要超过三条。如果条件复杂到这个程度说明这个场景该拆成两张顺序图或者该用活动图来补充分支逻辑。顺序图擅长表现时间顺序不擅长表现复杂条件网络硬塞会两败俱伤。4.4 顺序图最常见的两类翻车翻车一消息粒度太细。有人会画出getBalance()、setBalance()、log()这种纯实现细节甚至把 getter/setter 都画上去。顺序图是给人看的不是给编译器看的。我判定的标准是每条消息都应该对应一个能说清楚业务含义的动作。setBalance说不清楚业务含义debit能。翻车二生命线一开始就全部摆上去。一幅图里塞七八个对象横向拉得很宽中间的消息线互相交叉。我的习惯是按场景裁剪取款场景就只放Customer、ATM、CardReader、Card、BankHost、CashDispenser、ReceiptPrinter七个Account对象因为在BankHost内部不出现在这条生命线上它在主机侧的顺序图里出现。这样图宽控制在 A4 纸能看清的范围内。提示如果一幅顺序图在 A4 横向打印后看不清消息文字就该考虑拆图。我自己的红线是横向不超过 8 条生命线。5. 协作图通信图同一份信息的另一种视角5.1 顺序图和协作图到底差在哪两者语义等价可以互相推导这是 UML 规范的明确约定。差别在于强调的重点不同顺序图强调时间顺序消息的先后关系一目了然但对象之间的结构关系需要脑补。协作图强调结构连接对象之间的关联线清晰可见但消息的先后顺序要靠编号来读。我做过一个比喻顺序图像是剧本按场次顺序读协作图像是场景布置图先看清谁站在哪儿再看他们的对话顺序。同一个故事两种读法。在 ATM 这个场景下协作图能暴露一个顺序图掩盖的问题对象之间的耦合度。取款场景的协作图里ATM要连出五条线到CardReader、Card、BankHost、CashDispenser、ReceiptPrinter。这时候你会直观地发现ATM是个高度耦合的中枢——如果以后要加一个刷脸验证模块又得加一条线。这个观察会直接推动你把部分职责下沉或者引入中介者模式是设计上的收益。5.2 编号规则很多人编号方式错了协作图用编号表示消息顺序编号规则有两个层次第一层是消息序号1:、2:、3:。表示执行顺序。第二层是嵌套序号1.1:、1.2:用点号表示在 1 号消息执行过程中发出的子消息。取款场景的编号大致是这样1: insertCard() 1.1: readCard() 1.2: verifyPassword(pwd) 2: validateAccount(cardNo) 3: enterAmount(amount) 3.1: authorizeWithdraw(cardNo, amount) 3.2: dispense(amount) 3.3: commitTransaction(...) 4: ejectCard()常见错误有两种。一种是全部用平铺编号1、2、3、4、5……十几条这样看不出哪些消息是嵌套在某个调用内部的。另一种是点号层级混乱1.2后面接1.1时间顺序就乱了。我的习惯是编号的层级跟顺序图里的激活条嵌套深度保持一致。这样两张图对照着看一眼就能核对。5.3 什么情况下协作图比顺序图划算不是所有场景都值得画协作图。我的判断标准有三条满足任意一条就画对象数量在 4 到 6 个之间。太少没必要两条线的事太多协作图会变成蜘蛛网。需要评审对象之间的耦合关系。比如评估一次重构的必要性。软考或课程明确要求交协作图。这个理由虽然功利但确实是最常见的驱动因素。反过来说如果一个场景有复杂的时间逻辑多层嵌套循环 多个并发分支顺序图能表达得更清楚硬用协作图会非常痛苦因为编号会变成1.2.3.1这种形态没人愿意读。还有一个实操细节协作图和顺序图必须同步维护。我在项目里见过最糟糕的情况是两张图各改各的最后谁也不知道哪个是最新的。我的做法是只保留一份真源通常是顺序图因为工具支持好协作图用同一份数据切换视图生成。StarUML 不支持这种正反向同步所以我会在协作图下方加一行注释标明它对应顺序图的版本号。6. 活动图把业务流程和并发关系摊开6.1 泳道怎么分按角色还是按模块泳道Swimlane划分是活动图的第一道决策。ATM 场景我有两种分法按角色分客户、ATM、银行主机。优点是业务流程走向清楚谁做了什么一目了然适合给业务方看。按模块分客户交互层、业务逻辑层、硬件控制层、主机接口层。优点是贴近实现适合给开发看。我默认用第一种原因是活动图的主要用途是跟业务方确认流程不是技术设计。如果团队需要技术视角我会另画一张按模块分的但不会把两种混在一张图里——混着分泳道是活动图里最典型的错误会导致同一个动作不知道该落在哪条泳道。还有一个规模问题泳道别超过五条。超过五条之后横向宽度会失控打印出来看不清。如果业务确实涉及五个以上角色我会考虑拆成两张活动图一张讲主流程一张讲异常处理。6.2 取款流程的活动图结构取款活动图的骨架大致是初始节点→ 客户插入银行卡→ 系统读取卡号→ 校验卡片状态判断节点卡片过期/挂失 → 退卡 → 结束正常 → 继续→ 提示输入密码→ 验证密码判断节点错误且次数 3 →回到提示输入密码形成循环错误且次数 3 → 吞卡 → 结束正确 → 继续→ 展示功能菜单分叉节点 fork如果考虑并发这个位置其实可以不做并发取款流程本质是串行的→ 客户选择取款→ 输入金额→ 校验金额判断节点超过单笔限额 → 提示重输超过账户余额 → 提示重输机器现金不足 → 提示并结束通过 → 继续→ 主机授权→ 出钞动作节点→ 判断出钞是否成功失败 → 冲正 → 提示 → 结束成功 → 记账→ 询问是否打印凭条判断 可选分支→ 退卡→ 结束节点判断节点Decision Node用菱形一个输入多个输出。合并节点Merge Node也是菱形多个输入一个输出。这两个符号长得一样但语义相反画的时候一定要想清楚当前是分流还是汇流。并发分叉Fork用粗黑线。取款主流程其实不需要并发但有两个地方可以考虑一是出钞和打印凭条可以并行客户拿钱和拿凭条互不依赖二是记账和更新余额在主机侧可以并行处理。如果你把这两个地方画成分叉记得要配一个对应的汇合节点Join否则活动图在语义上是不完整的。6.3 活动图和流程图的区别很多人觉得活动图就是流程图这个说法在简单场景下勉强成立但有三处关键差异必须清楚对比项传统流程图UML 活动图并发表达一般不支持或需要额外约定原生支持 fork/join泳道可选非标准标准化表示语义明确对象流不支持支持用对象节点表示数据流转令牌语义无有明确的令牌流动语义令牌语义Token Semantics是活动图比较独特的部分它决定了流程的执行规则一个动作节点只有在所有输入流都收到令牌时才会执行分叉节点会为每条输出流生成一个令牌汇合节点会等待所有输入流都收到令牌再输出一个令牌。理解这一点你就明白为什么 fork 和 join 必须配对——只 fork 不 join会产生多个并发的结束节点流程语义上就成了多条独立流程而不是你以为的一个流程的并行分支。实操中我见过的一个典型错误画了 fork 分叉成出钞和打印凭条两条线但只把出钞接到了结束节点打印凭条那条线悬空了。这在 UML 里是非法模型工具一般会报错但手绘时经常被忽略。6.4 异常分支不画这张图就是假的这是我带新人时反复强调的一句话。只画 happy path 的活动图没有价值因为开发真正需要知道的是失败了怎么办。ATM 场景里必须画出来的异常至少包括验密失败可重试验密失败达上限吞卡卡片过期或已挂失余额不足超出单笔限额机器现金不足主机通信超时出钞失败机械故障用户超时未操作用户中途取消十条里画全六条这张图就有实用价值了。剩下的可以在用例描述里用文字补充。还有个排版技巧把异常分支尽量统一画在流程主轴的同一侧我习惯放右侧主流程一路向下走。这样读者顺着主轴读需要看异常时再往右看认知负担小很多。混着排会让图非常难读。7. 五张图的交叉校验一致性怎么保证画完五张图最危险的状态是每张都好看合在一起对不上。下面是我实际用的三层校验方法。7.1 用例到类每个用例都要有类能接住做法很简单把用例清单列出来逐个问这个用例在类图里由哪些类协作完成。取款→ATM、Card、Account、BankHost、CashDispenser修改密码→ATM、Card加钞→ATM、CashDispenser如果某个用例找不到承接的类说明类图缺了东西。我第一次画 ATM 类图的时候就漏了CashDispenser做这一步校验才发现的——因为画类图时脑子里想的是客户和账户没往硬件方向想。反向校验同样有用类图里每个类都应该至少被一个用例用到。如果有个类哪个用例都接不上要么它是纯技术基础类比如日志类要么就是多余的。7.2 类到顺序图方法签名必须逐字对齐这一步最容易出问题也最容易出成果。方法就一个动作把顺序图里出现的每一条消息逐个到类图里找对应的方法比对方法名、参数个数、参数类型、返回值。举几个我实际改过的例子顺序图写atm.dispense(amount)但类图里dispense定义在CashDispenser上而不是ATM上 → 要么改消息的接收者要么在ATM上加一个转发方法。我选了前者把消息接收者改成dispenser。顺序图写card.verify(pwd)类图写verifyPassword(input)→ 名字统一我按类图改顺序图。顺序图里debit有返回值被用于判断类图里debit返回void→ 改类图返回值类型。提示这一步不要靠眼睛扫要列表格逐条过。我一般会建一个两列的表左边写顺序图消息右边写类图方法签名对不上的标红。十条消息大概五分钟能过完比事后返工便宜太多。7.3 顺序图到活动图分支条件必须一致顺序图里的alt分支条件和活动图里的判断节点条件必须是同一件事。我见过的不一致例子顺序图里alt判断密码错误次数是否达到 3 次活动图里判断密码是否正确。表面看差不多实际上分支逻辑反了。这种错误在评审时最容易被抓因为评审人往往同时看两张图。另外还有一处容易忽略活动图里的循环结构在顺序图里应该对应loop片段。如果活动图画了回到提示输入密码的循环而顺序图里只做了一次线性流程两张图就是矛盾的。7.4 我常用的自检清单最后附一份我每次交图前都会过一遍的清单检查项检查方法通过标准用例是否有遗漏参与者对照需求清单逐个问谁发起每个用例都有明确发起方include/extend 方向看箭头起点include 从基础指向被包含extend 从扩展指向基础聚合/组合判定问整体没了部分还活吗假设明确且前后一致多重性对照业务假设与用例描述里的数量描述一致方法签名顺序图与类图逐条比对名称、参数、返回值完全一致分支条件顺序图alt与活动图判断节点比对条件语义一致方向不反fork/join 配对数 fork 和 join 的数量一一对应异常分支覆盖对照异常清单至少覆盖六条主要异常图宽可读性打印或按 A4 预览顺序图生命线不超过 8 条版本一致性检查顺序图与协作图编号对应编号层级一致这张表我用了很多次抓出来的问题大致分布是方法签名不一致占一半分支条件不一致占四分之一剩下的是符号用法错误。也就是说大部分返工都发生在图与图之间而不是单张图内部。这个观察也印证了前面的判断画 UML 的难点不在符号在一致性。8. 工具选择与出图细节别在工具上浪费太多时间8.1 StarUML 画类图的几个效率技巧StarUML 是学生和教学场景里用得最多的工具类图支持完善正向工程也能用。几个实测有效的技巧用 Model 面板建类再拖到画布。直接在画布上双击创建容易建出匿名的临时元素后面很难管理。属性用 name : Type的简写格式回车自动解析。不用一个个点开弹窗填。开启自动布局。类多了之后手动拖线非常耗时自动布局能省掉一半时间代价是排布不那么美观可以再手工微调。用好模板Template。如果一张图里有十几个类都要加status属性用模板比复制粘贴快。导出图片时选 PNG 而不是 JPG。UML 图里有大量细线和文字JPG 压缩后文字边缘会糊。需要提醒的一点是StarUML 的include和extend需要在关联上手动添加构造型Stereotype默认没有快捷按钮。很多人画出来的箭头光秃秃的就是因为漏了这一步。8.2 Visio 画 UML 类图的坑用 Visio 画类图最大的问题是它本质上不是 UML 工具是通用绘图工具。带来的具体麻烦类之间的关联线没有语义移动类的时候线不会自动跟随也无法自动维护多重性标注位置。大部分 UML 形状没有构造型字段include得手动打文字。无法做一致性校验也无法导出模型供其他工具使用。但如果公司环境只允许用 Visio也不是画不了。我的应对方式是用 Visio 的UML 类模具Stencil而不是普通矩形这个模具自带属性区和操作区的分隔线视觉上规范得多。另外把每个关联的多重性做成文本标注并锁定到线上减少拖动后错位。8.3 IDE 生成类图能直接用吗现在的 IDE 大多支持从代码生成类图。这个东西有用但用途要分清看现有代码结构非常有用。快速了解一个陌生模块的类关系几分钟就能建立整体认知。当成交付文档基本不行。生成的图通常平铺所有属性方法包含大量实现细节而且不会有参与者视角的分组可读性差。我的实际做法是用 IDE 生成初稿导出后导入绘图工具很多工具支持直接导入然后做三件事——删掉访问器和审计字段、按业务分组重排布局、补上类图需要的注释。关于反向工程还有一点要提醒如果代码里的继承层次很深自动生成的图会把所有父类都画出来很容易变成一张巨大的图。这时候要主动裁剪只保留跟当前讨论相关的层次。工具的选择我会这样排序如果目标是模型驱动和一致性校验用支持模型库的工具一份模型出五张图改动自动同步这是最省心的方案。如果目标是快速出图交作业StarUML 足够。如果目标是跟非技术方沟通有时候直接用活动图加文字的 PPT 反而更有效——这一点我是在一次给业务方讲 ATM 流程时才意识到的业务方真正关心的只有活动图和用例图类图和顺序图他们一眼都不看。图给谁看决定用什么画也决定画多细这大概是这套 UML 设计里最容易被忽略的一条经验。
返回列表