ARTICLE DETAIL

资讯详情

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

UML序列图从入门到精通:核心元素、实战推导与避坑指南

UML序列图从入门到精通:核心元素、实战推导与避坑指南 1. 序列图到底在解决什么问题很多人第一次接触UML都是从类图开始的。类图告诉你系统里有哪些对象、它们之间是什么关系但它有一个致命的短板——它是静态的。类图画得再漂亮你也不知道一个下单请求进来之后订单对象、库存对象、支付对象之间到底谁先跟谁说话、说了什么、按什么顺序说。序列图Sequence Diagram就是来补这个短板的。它属于UML中的动态结构图专门用来描述对象之间按时间顺序发生的交互。横轴是参与交互的对象纵轴是时间消息在对象之间传递从上往下读就是一次完整的交互过程。我自己的体会是如果你只能选一种UML图来跟开发同学对齐需求那大概率应该选序列图。因为它最接近代码的执行逻辑也最容易暴露谁该负责什么这种设计层面的分歧。用例图告诉你系统能做什么类图告诉你系统由什么组成而序列图告诉你系统到底是怎么跑起来的。这篇文章我会从零讲起把序列图的每一个元素、每一种消息类型、每一类控制结构都拆开讲透然后给出一套可以直接抄作业的画图流程最后聊聊我在实际项目里踩过的那些坑。不管你是准备软考中级、还是在做系统设计评审这篇内容都能直接用上。2. 序列图的构成元素逐个拆解2.1 参与者与生命线谁在参与这场对话序列图的最上方是一排矩形框每个框代表一个参与者Lifeline Participant。参与者可以是人比如用户、可以是系统比如订单服务、也可以是某个具体的对象实例比如order: Order。命名上有个约定俗成的规矩如果是对象通常写成对象名:类名的形式比如order:Order如果只关心类不关心具体实例直接写类名也行。每个参与者下方拖着一条垂直的虚线这条线叫生命线Lifeline。生命线代表这个对象在时间轴上的存在。注意生命线不是这个对象一直活着的意思而是在这段时间内这个对象参与了交互。如果某个对象在中途被创建出来它的生命线就从创建它的那一刻才开始画。这里有个新手特别容易犯的错误把参与者当成函数或者方法。不是的。参与者是对象是承担责任的实体。如果你发现自己在序列图里画了一个叫校验参数的参与者那说明你搞混了对象和动作。正确的做法是把校验参数作为某个对象收到消息后执行的动作而不是把它本身画成一个参与者。2.2 激活条对象什么时候在干活生命线上那些细长的矩形条叫激活条Activation Bar也叫执行规格。它表示这个对象在这段时间内正在执行某个操作。激活条的顶部对应收到消息的时刻底部对应操作完成、返回结果的时刻。激活条这个东西看起来简单但它承载的信息量很大。一条生命线上可以同时存在多个激活条表示嵌套调用激活条的堆叠方式直接反映了调用栈的深度。我在评审时经常通过看激活条来判断一个设计是不是合理——如果一个对象的激活条从头到尾就没断过那说明它承担了太多职责大概率是个上帝对象需要拆分。提示激活条不是必须画的。在强调消息顺序而不关心执行时长的场景下省略激活条能让图更清爽。但在做性能分析或者排查阻塞问题时激活条是必不可少的。2.3 消息对象之间到底说了什么消息是序列图的灵魂。一条消息就是从发送方生命线指向接收方生命线的一个箭头箭头上的文字描述了这个消息的内容。消息按性质可以分为几大类每一类的画法和语义都不一样这是最容易画错的地方。同步消息Synchronous Message用实线加实心箭头表示。发送方发出消息后会阻塞等待直到接收方处理完并返回结果才继续往下走。这就像你打电话给客服说完问题后必须等对方回复才能继续。大部分方法调用都是同步消息。异步消息Asynchronous Message用实线加开放式箭头就是那种只有两条线的箭头表示。发送方发出消息后不等待立刻继续执行自己的后续逻辑。这就像你发了一封邮件发完就干别的事去了对方什么时候回你你不知道也不关心。消息队列的发送、事件的发布通常用异步消息表示。返回消息Return Message用虚线加开放式箭头表示。它表示接收方处理完同步消息后把结果返回给发送方。返回消息可以标注返回值也可以省略不画省略时默认隐含返回。自调用消息Self Message是对象发给自己的消息箭头从生命线出发又回到同一条生命线。这通常表示对象内部的方法调用或者递归。消息类型线条样式箭头样式是否阻塞典型场景同步消息实线实心三角箭头是普通方法调用异步消息实线开放式箭头否消息队列、事件发布返回消息虚线开放式箭头-返回处理结果自调用消息实线实心三角箭头是内部方法、递归2.4 组合片段把复杂逻辑装进框里当交互逻辑不是一条直线走到底而是有分支、循环、并发的时候就需要用到组合片段Combined Fragment。组合片段的画法是一个带折角的矩形框左上角有一个关键字标明这个片段的类型。最常用的几种组合片段alt表示如果……否则……的分支。框内用虚线分成多个区域每个区域有一个监护条件guard condition比如[余额充足]和[余额不足]。同一时刻只有一个区域会被执行。opt表示可选执行相当于只有一个分支的 alt。监护条件成立就执行不成立就跳过。loop表示循环。可以在关键字后面标注循环次数或循环条件比如loop(1..n)或者loop [还有未处理订单]。par表示并发执行。框内多个区域可以同时进行没有先后顺序。break表示中断。通常用于异常场景满足条件时跳出当前交互。组合片段是可以嵌套的。一个 loop 里面可以套一个 alt一个 alt 里面可以再套一个 opt。但我要提醒一句嵌套层级不要超过三层。超过三层之后图的可读性会急剧下降这时候应该考虑把内层逻辑抽成独立的序列图用 ref 片段引用。2.5 引用片段与交互概览ref片段用来引用另一张序列图。当某个交互逻辑比较复杂、或者需要在多个场景中复用时就把它单独画成一张图然后在主图里用 ref 引用。这跟代码里抽函数是一个道理。交互概览图Interaction Overview Diagram是序列图和活动图的混血儿它用活动图的控制流节点来组织多个序列图片段。说实话这个图在实际项目中用得很少我做了这么多年项目真正画交互概览图的次数一只手数得过来。软考里偶尔会考了解概念即可。3. 从业务场景到序列图的完整推导过程3.1 先别急着画图把交互故事讲清楚我见过太多人一上来就打开画图工具拖几个框就开始连线结果画到一半发现逻辑不对推倒重来。正确的做法是先用自然语言把交互过程讲一遍。以电商下单为例你可以这样讲用户点击提交订单订单服务先校验库存库存服务返回校验结果。如果库存充足订单服务创建订单并调用支付服务发起支付支付服务返回支付结果订单服务根据支付结果更新订单状态并返回给用户。如果库存不足订单服务直接返回失败提示。这段话讲完参与者是谁、消息有哪些、分支在哪里全都清楚了。接下来才是把它翻译成序列图。3.2 识别参与者从动词的主语里找把上面的故事拆开每个动作的主语就是潜在的参与者。用户、订单服务、库存服务、支付服务这四个就是参与者。注意不要把所有名词都当成参与者。订单是一个数据实体它本身不发起也不接收消息所以不应该作为参与者出现除非订单对象有独立的行为逻辑。这里有个判断标准如果一个东西只是被动地存储数据、不主动参与交互那它就不该出现在序列图里。数据库通常也不作为参与者因为数据库操作是某个服务内部的行为不是服务之间的交互。3.3 排列消息顺序时间轴上的因果链参与者确定后把消息按时间顺序排列。排列的时候要问自己三个问题这条消息是谁发给谁的它是同步的还是异步的它有没有返回值返回值被谁用了以下单场景为例消息顺序是这样的用户 → 订单服务提交订单同步订单服务 → 库存服务校验库存同步库存服务 → 订单服务返回库存结果返回消息订单服务 → 订单服务创建订单自调用订单服务 → 支付服务发起支付同步支付服务 → 订单服务返回支付结果返回消息订单服务 → 用户返回下单结果返回消息第4步的创建订单是订单服务内部的行为用自调用消息表示。如果你觉得这个细节不重要也可以省略直接在第3步和第5步之间画一条从订单服务到支付服务的消息。3.4 补充分支与异常让图覆盖真实情况上面的顺序是一切顺利的主流程。真实系统里还有各种分支和异常需要用组合片段补上。库存不足的分支用 alt 片段包住第2到第7步分成两个区域[库存充足]走正常流程[库存不足]直接返回失败。支付超时的情况可以用另一个 alt 或者 break 片段处理。我个人的习惯是主流程图和异常流程图分开画。主流程图给产品和业务方看异常流程图给开发和测试看。把异常逻辑全塞进一张图里图会变得又大又乱谁都看不下去。3.5 用工具落地PlantUML 与画图工具的选择序列图的绘制工具大致分两类代码驱动型和拖拽型。代码驱动型的代表是 PlantUML 和 Mermaid。你写文本工具生成图。优点是版本可控、修改方便、容易嵌入文档。缺点是布局不能精细控制复杂图的排版可能不理想。拖拽型的代表是 Draw.io、ProcessOn、StarUML。优点是所见即所得布局灵活。缺点是修改麻烦版本管理困难。我自己的选择是日常沟通用 PlantUML正式交付文档用 Draw.io。PlantUML 写起来快改起来也快适合快速迭代。正式文档需要精细排版的时候再用拖拽工具调整。下面是一段 PlantUML 的序列图示例可以直接复制使用startuml actor 用户 participant 订单服务 as Order participant 库存服务 as Stock participant 支付服务 as Pay 用户 - Order: 提交订单 activate Order Order - Stock: 校验库存 activate Stock Stock -- Order: 返回库存结果 deactivate Stock alt 库存充足 Order - Order: 创建订单 Order - Pay: 发起支付 activate Pay Pay -- Order: 返回支付结果 deactivate Pay Order -- 用户: 返回下单成功 else 库存不足 Order -- 用户: 返回库存不足 end deactivate Order enduml这段代码生成的图参与者、激活条、同步消息、返回消息、alt 分支全都有了。你可以把它贴到任何支持 PlantUML 的编辑器里直接渲染。4. 那些年我在序列图上踩过的坑4.1 把序列图画成了流程图这是最最常见的错误。流程图描述的是一个操作内部的步骤序列图描述的是多个对象之间的交互。如果你画的序列图里只有一个参与者所有消息都是自调用那它本质上就是一张流程图用活动图画更合适。判断标准很简单序列图上至少要有两个参与者消息是在不同参与者之间传递的。如果做不到这一点说明你选错了图种。4.2 消息命名含糊不清我见过太多消息写着处理、操作、执行这种毫无信息量的词。消息名应该是一个明确的动作最好能对应到代码里的方法名。比如校验库存就比处理库存好发起支付就比支付操作好。还有一个细节消息名不要写成库存校验这种名词形式要用校验库存这种动宾结构。因为消息本质上是发送方请求接收方做一件事动宾结构更符合这个语义。4.3 激活条画得乱七八糟激活条的作用是表示对象正在执行操作。但很多人要么不画要么画得不对。常见的错误包括激活条没有和消息对齐、返回消息的箭头没有指向激活条的底部、嵌套调用的激活条没有正确堆叠。我的建议是如果画了激活条就一定要画对。激活条的顶部对齐收到消息的位置底部对齐返回消息发出的位置。嵌套调用时内层激活条应该画在外层激活条的右侧形成阶梯状。4.4 组合片段滥用导致图不可读组合片段是个好东西但用多了就是灾难。我见过一张序列图里套了五层组合片段alt 里面套 looploop 里面套 optopt 里面再套 alt看的时候得拿放大镜一层层剥。经验法则是一张序列图里的组合片段不超过三个嵌套层级不超过两层。超出的部分抽成独立的序列图用 ref 引用。这样每张图都保持在一个屏幕能看完的规模可读性大大提升。4.5 忽略异步消息的语义很多人画消息的时候不区分同步和异步全用实心箭头。这在简单场景下问题不大但在涉及消息队列、事件驱动的系统里同步和异步的区别至关重要。同步消息意味着发送方会阻塞等待如果接收方处理慢发送方也会被拖慢。异步消息意味着发送方发完就走两者解耦。如果你把异步消息画成了同步消息读图的人会误以为这两个服务是强耦合的可能导致错误的架构判断。注意在微服务架构的序列图里跨服务的调用默认应该考虑异步消息。只有明确需要同步等待结果的场景才画成同步消息。5. 序列图在软考和实际工作中的考查重点5.1 软考中级里序列图的出题套路软考中级比如软件设计师对序列图的考查主要集中在几个方面给定一段业务描述判断序列图里缺失的消息是什么给定序列图判断某个组合片段的语义区分同步消息和异步消息的画法。常见的题型是给你一张不完整的序列图让你从选项里选出正确的消息或者正确的组合片段类型。做这类题的关键是抓住时间顺序和因果关系谁先谁后谁依赖谁的结果。只要把因果链理清楚答案基本就出来了。还有一个高频考点是序列图和类图的对应关系。序列图里的参与者通常对应类图里的类序列图里的消息通常对应类图里的方法。如果题目同时给了类图和序列图要注意两者是否一致。5.2 实际工作中序列图的三类使用场景需求评审阶段用序列图跟产品经理对齐业务流程。产品经理看不懂类图但看得懂序列图。把主流程画出来产品经理一眼就能看出这里少了一个校验或者这个顺序不对。技术方案设计阶段用序列图跟开发同学对齐接口设计。消息名就是接口名消息的参数就是接口参数返回消息就是接口返回值。一张序列图画完接口定义基本就清楚了。问题排查阶段用序列图还原线上问题的调用链路。把实际发生的调用顺序画出来跟设计的序列图对比差异点往往就是问题所在。5.3 序列图与其他UML图的配合使用序列图很少单独使用它通常和其他UML图配合。用例图告诉你系统有哪些功能序列图告诉你每个功能是怎么实现的类图告诉你实现这些功能需要哪些类状态图告诉你某个对象在交互过程中的状态变化。一个完整的UML建模流程通常是用例图 → 序列图 → 类图 → 状态图。序列图处在中间位置承上启下。往上它把用例细化成具体的交互步骤往下它推导出类应该有哪些方法。6. 让序列图真正产生价值的几个实操习惯6.1 一张图只讲一个场景我见过有人试图用一张序列图覆盖所有可能的交互路径结果图画得跟蜘蛛网一样。正确的做法是一个场景一张图。正常下单一张图库存不足一张图支付超时一张图。每张图都聚焦一个明确的场景读起来清爽维护起来也方便。如果多个场景之间有共享的逻辑把那部分抽成独立的序列图用 ref 引用。这样修改共享逻辑的时候只需要改一处。6.2 消息粒度控制在接口级序列图里的消息应该对应到接口级别的方法调用而不是内部实现细节。比如校验库存是一个接口方法画出来没问题但连接数据库、执行SQL查询这种内部实现细节就不应该出现在序列图里。粒度太细图会变得冗长且不稳定内部实现经常变粒度太粗图又失去了指导编码的价值。接口级是一个比较合适的平衡点。6.3 给序列图加上前置条件和后置条件在图的顶部或者注释里标注前置条件和后置条件能让读图的人快速理解这个交互的上下文。前置条件说明什么情况下会触发这个交互后置条件说明交互完成后系统处于什么状态。比如下单场景的前置条件是用户已登录且购物车非空后置条件是订单已创建且支付已发起。这两句话加上去图的完整性就上了一个台阶。6.4 版本管理和变更记录序列图是设计文档的一部分应该纳入版本管理。每次修改都要记录改了什么、为什么改。我自己的做法是在图的下方加一个简单的变更记录表格标注版本号、修改日期、修改内容和修改人。版本日期修改内容修改人v1.02024-01-15初始版本覆盖正常下单流程张三v1.12024-02-03增加库存不足分支张三v1.22024-03-10支付服务改为异步消息李四这个习惯看起来不起眼但在项目交接和问题回溯的时候能救命。6.5 用序列图做代码评审的辅助工具代码评审的时候如果改动涉及多个对象的交互我会先让对方画一张序列图。图一画出来设计合不合理、有没有多余的调用、有没有循环依赖一目了然。比对着几百行 diff 看效率高多了。这个做法还有一个好处画图的过程会强迫作者重新审视自己的设计。很多时候画到一半作者自己就发现这个调用好像没必要或者这里应该用异步。序列图这个工具入门容易用好难。它的价值不在于画得好看而在于画的过程中逼你把交互逻辑想清楚。我自己的经验是一张序列图画完代码还没写设计上的坑已经排掉了一大半。这大概就是它最大的意义。
返回列表