组合片段 opt/loop/par 怎么用:可选、循环、并行的画法与反例修正
opt/loop/par 是 UML 时序图里最常用的三个组合片段:分别表达可选、循环、并行。本文从研发/测试/架构/写文档视角讲清它们的语义、最小规范、常见反例与修正、检查清单与 FAQ,并给出可直接照抄的画法。
如果你在画 UML 时序图(Sequence Diagram)时纠结:
- “这段逻辑到底是 分支 还是 可选?”
- “重试/轮询算不算 loop?要写几次?”
- “异步消息是不是就等于 par 并行?”
最实用的记法是这三句:
- opt = 可能发生,也可能不发生(0 或 1 次)
- loop = 同一段交互重复发生(N 次)
- par = 两段(或多段)交互并发发生,顺序不保证(并行)
这篇文章会把 opt/loop/par 讲到能落地:每个片段的语义、最小规范怎么写、常见反例怎么改、以及一份“选哪种片段”的检查清单。目标是:你画出来的图能被研发、测试、架构、产品、写文档的人一致读懂,而不是“看起来像对的画”。
1. 先把“组合片段”这件事讲清:你在框的到底是什么
时序图的组合片段(Combined Fragment)本质上是在一段时间区间内,给某些消息交互加上“控制结构语义”。
alt:互斥分支(if / else-if / else)opt:可选(if,没有 else;条件成立才发生)loop:循环(重复发生)par:并行(同时发生,局部顺序不保证)
这篇只讲 opt/loop/par,但你会反复看到一个结论:
画对片段类型 ≠ 画得好。真正决定“可读性”的,是你能不能把 条件(guard)、重复规则、并发边界 写到“能落测试用例/能指导实现”的程度。
1.1 三个最常用的标注:guard / 迭代说明 / 并发约束
- guard 条件:写在分支或可选块顶部,常见形式
[condition] - loop 迭代说明:可以写成
loop [i=1..3]或loop [while retryCount < 3] - par 的并发块:每个并行块是一个 operand(并发分组),并列摆放
你写这些标注不是为了“像 UML”,而是为了让读图的人不用脑补。
2. opt 怎么用:可选交互(0 或 1 次)
2.1 opt 的一句话定义
opt(Optional)表达的是:
当条件成立时,发生一段交互;条件不成立时,这段交互完全不发生。
它对应代码里最接近的结构是:
if (cond) {
...messages...
}
注意:它不是“互斥分支”。如果你心里有“否则走另一套流程”,那你需要的是 alt,不是 opt。
2.2 什么时候该用 opt(研发/测试最常见的可选点)
典型的可选交互长这样:
- 可选校验/可选增强:用户填了发票抬头 → 额外做一次发票信息校验
- 可选埋点/可选审计:开启审计开关 → 额外记录审计事件
- 可选风控/可选二次确认:命中某规则 → 触发二次校验(短信/人机验证)
- 可选缓存:缓存命中 → 直接返回;缓存未命中 → 走正常路径(这里常和
alt搭配,见后文反例)
这些场景共同点:
- 主流程不依赖这段交互一定发生
- 可选交互发生与否由一个明确条件决定
- 测试可以把它拆成“开关开/关、条件满足/不满足”两类用例
2.3 opt 的最小规范(写到能落地的程度)
规则 1:guard 条件要可判定,不要写形容词。
- 好:
[feature.auditEnabled]、[needCaptcha == true]、[amount > 5000] - 差:
[需要]、[高风险]、[异常](到底谁判定?标准是什么?)
规则 2:opt 框住的消息要“完整”。
常见坑:只画“调用”,不画返回/结果,导致读者不知道:
- 失败时主流程是不是中断?
- 失败会不会降级?
- 返回码/错误码是什么?
如果 opt 内的动作失败会影响主流程,你要明确:
- 是“失败就终止主流程”(那就别用 opt 了,通常应改成
alt互斥分支) - 还是“失败忽略/降级继续”(opt 仍可用,但要把降级行为画出来)
规则 3:不要用 opt 偷偷表达“分支”。
很多图把“成功路径”放 opt 里,失败路径放 opt 外面不写,结果就是:
- 图上只有“成功才发生的动作”
- 失败发生什么全靠脑补
这会让测试用例与实现细节在评审时严重跑偏。
2.4 opt 的反例与修正(你可能真的画过)
反例 A:把互斥分支画成 opt
你想表达:缓存命中/未命中两种路径。
错误画法:
[cache hit]用opt框住“直接返回”- 未命中路径不画/散落在外面
为什么错:
- “命中”和“未命中”是互斥分支,不是“可选增强”
- 读者看不出未命中时是否一定会走 DB、是否会回填缓存
修正:用 alt:
[cache hit]→ 返回缓存结果[else]→ 查询 DB → 回填缓存 → 返回
反例 B:guard 写“成功/失败”
[成功]/[失败]这种 guard 属于“把结论写成条件”。
修正:把条件写成“可观察/可判定”的表达:
[riskScore >= threshold]、[verifyCode valid]、[authResult == OK]
3. loop 怎么用:循环交互(重复 N 次)
3.1 loop 的一句话定义
loop 表达的是:
同一段消息交互,在某个迭代规则下重复发生(可能是固定次数,也可能是“直到条件满足/超时”)。
它对应的不是“把同一条消息画三遍”,而是把“重复规则”写清楚。
3.2 什么时候该用 loop(别把重试画成一堆箭头)
以下场景基本都该考虑 loop:
- 重试:接口超时重试、消息发送失败重试
- 轮询:客户端轮询任务状态、服务端轮询外部网关结果
- 分页/批处理:循环拉取分页数据、分批写入
- 重复校验:直到通过或达到次数上限(例如验证码输错三次锁定)
这些场景共同点:
- “相似的一段交互”重复发生
- 重复次数/停止条件决定了行为边界(超时、限流、幂等)
- 测试非常依赖“最大次数、间隔、退避策略”的明确说明
3.3 loop 的最小规范:你至少要写清 4 件事
1) 循环条件/次数上限(最重要)
常见写法:
loop [i=1..3](固定次数)loop [while retryCount < 3 && !success]loop [until status == DONE or timeout 30s]
2) 每次循环之间有没有间隔/退避(否则读者默认“紧循环”)
loop [retry with backoff: 100ms, 300ms, 900ms]loop [poll every 2s, max 10 times]
3) 循环内失败如何处理(失败是继续还是跳出)
- 继续:记录错误 → 等待 → 下一次重试
- 跳出:达到上限 → 返回错误码 / 触发补偿
4) 循环的可观测行为(日志/埋点/告警)
对研发/运维来说,“重试了几次”是否能查到非常关键。你不必画得很细,但至少要体现:
- 是否记录重试次数
- 是否在上限触发告警或降级
3.4 loop 的反例与修正
反例 C:把重试画成 3 条相同消息
你画了三次 A -> B: call(),看起来直观,但问题很多:
- “为什么是 3 次?”你没写上限来源
- 是否有退避?是否每次都一样?
- 第 2 次成功后,第 3 次还会发吗?
修正:用 loop 框住“调用 + 返回 + 判定”,并写清迭代说明:
loop [max 3, exponential backoff]- 在 loop 内用 guard 或注释表达“成功则退出”
反例 D:循环条件写成“直到成功”
loop [until success] 这种写法太危险:
- 成功的判定是什么?
- 永远不成功怎么办?
- 超时/上限/熔断在哪里?
修正:把边界写完整:
loop [until success OR retryCount==3 OR deadline reached]
4. par 怎么用:并行交互(并发发生,顺序不保证)
4.1 par 的一句话定义
par(Parallel)表达的是:
多个交互片段在时间上并发执行,相互之间没有固定顺序约束(除非你额外标注)。
它解决的不是“异步消息怎么画”,而是“两个动作可能同时发生/独立推进”。
4.2 什么时候该用 par(以及什么时候你其实不该用)
适合用 par 的典型场景:
- 并行调用多个依赖:同时拉用户信息与权益信息(最终合并返回)
- 请求主流程 + 并行副作用:下单成功后并行:写审计日志、发 MQ、更新搜索索引
- 并行通知:支付成功后并行通知商户系统与内部风控系统
不适合(或需要谨慎)的场景:
- 仅仅是“异步消息”:A 发一个异步消息到 B,不代表“还有另一个片段并行”。
- 异步消息通常用消息箭头/返回方式表达即可;是否并行取决于“还有谁在同时做别的事”。
- 严格先后顺序:如果 B 必须等 A 结束才开始,那不是 par。
4.3 par 的最小规范:并发边界要写清楚
规则 1:par 内每个 operand 都要“自洽”,不要半截。
每个并行块最好都能独立读懂:
- 谁发起
- 调用谁
- 结果回到哪里
规则 2:如果存在“汇合点”(join),要表达出来。
很多并行最终要合并结果,比如:
- 聚合服务并行调用 A/B → 等两边都返回 → 组合响应
你可以用一条明确的消息/注释来表达“等待/合并”:
Aggregator: wait all(注释)- 或画一个“组合响应”的消息发生在两条并行链路之后
规则 3:并行块之间如果有共享资源/互斥要求,要说清楚。
典型:并行写同一条记录会产生竞态。时序图里你至少要提示:
- “这两条并行路径是否可能同时写同一资源?”
- “是否需要锁/幂等/去重?”
否则图在评审时会被误读成“并行也没关系”。
4.4 par 的反例与修正
反例 E:把“异步”画成 par
常见误画:
- 因为消息是异步的,就用
par框住“发送消息”和“后续处理”
为什么不严谨:
par强调的是“两个片段并发”,而不是“某条消息是异步”- 如果你的真正意图是“发送后不等待返回”,用异步消息箭头/不画返回更直接
修正:
- 如果只有“发送消息不等待”,就用异步消息表示
- 只有当“同时还有另一条链路在推进”时才用
par
反例 F:par 里漏掉“合并点”,导致读者误以为不需要等待
你画了并行调用 A/B,但返回给 Client 的响应也画在 par 里某个角落,读者会问:
- 响应到底依赖 A/B 的结果吗?
- 还是先返回、后台慢慢处理?
修正:
- 如果依赖:把“组合响应”画在两条并行链路都返回之后
- 如果不依赖:明确标注“fire-and-forget / eventual consistency”
5. 三种片段怎么选:一张“判断规则”就够用
你可以按下面的顺序问自己(建议直接贴进团队画图规范里):
- 这段交互是否一定发生?
- 不一定(0 或 1 次)→
opt
- 是否会重复发生?
- 会(N 次)→
loop
- 是否存在同时发生的两段交互?
- 有(并发推进,顺序不保证)→
par
- 如果不是以上三种,那它多半是“互斥分支”
- 互斥 →
alt(别用 opt 糊弄)
一个常见组合:
alt(命中/未命中)里面的“未命中分支”再套loop(重试/分页)loop里面某一步可能opt(可选审计/可选降级)- 主流程外侧再用
par(并行发通知/更新索引)
组合片段可以嵌套,但要记住:嵌套越深,越需要你把 guard/边界写清楚。
6. 从“文字交互”到“可交付时序图”:三个场景拆解(可直接照抄)
下面给你 3 个常见业务片段,分别对应 opt/loop/par 的正确使用方式。你可以拿去改成自己项目的版本。
6.1 场景一:登录时的“可选二次验证”(opt)
文字交互:
- 用户登录
- 如果命中风控规则,需要验证码
- 校验通过才发 token
推荐画法:
- 主流程:Client → AuthService:login
- 在“签发 token”之前加一个
opt [needCaptcha]- Client → CaptchaService:verify
- CaptchaService → Client:ok/fail
- 如果验证码失败会终止登录:这其实是互斥分支,建议用
alt表达“验证码通过/失败”,不要只用 opt。
一句话:
- “是否需要验证码”是可选 → opt
- “验证码通过/失败的后果”是互斥分支 → alt
6.2 场景二:接口超时重试 + 退避(loop)
文字交互:
- 服务 A 调用服务 B
- 超时就重试,最多 3 次,退避 100/300/900ms
- 3 次都失败则降级返回默认值,并打点
推荐画法:
- A → B:request
- B → A:timeout(或无返回 + 注释“deadline reached”)
- 用
loop [max=3, backoff=100/300/900ms]框住“调用 + 判定 + 休眠/等待” - loop 结束后用
alt表达:[success]→ 正常返回[else]→ 降级返回默认值 + 记录告警
注意:这里不要把“成功/失败”当 loop 的条件一句话带过;要把“上限 + 退避 + 降级”写清楚,测试才能据此设计用例。
6.3 场景三:下单后并行副作用(par)
文字交互:
- 下单成功返回给客户端
- 同时:
- 写审计
- 发 MQ 给库存系统
- 更新搜索索引
推荐画法:
- 主链路:Client → OrderService:createOrder → 返回 orderId
- 在“返回客户端”之后或旁边画
par,包含 3 个并行块:- OrderService → Audit:record
- OrderService → MQ:publish(OrderCreated)
- OrderService → SearchIndex:update
关键是加一句注释(非常值钱):
- “这些副作用为最终一致,不影响下单接口响应;失败会异步补偿/重试。”
否则读者会误以为:副作用都必须成功才能返回。
7. 实操:怎么把 opt/loop/par 画得又快又不乱(工具链建议)
很多团队把时序图画崩,原因不是不会 UML,而是“画图成本太高,于是只能画个大概”。解决方法通常是把“画图动作”变得更机械化。
如果你用的是在线时序图工具/生成器,比较高效的流程是:
- 在左侧编辑器里先点选并创建 生命线(参与者/对象):把关键模块先摆好
- 再点选 消息:先把主干链路(Happy Path)按顺序拉出来
- 然后点选 组合片段:分别选择
opt / loop / par,用鼠标框住相关的那段消息 - 在片段顶部补上 guard / 迭代说明 / 并发注释
- 右侧实时预览检查:对齐、间距、是否遮挡、是否存在“跨分支跳线”
- 出图交付:
- 文档/需求评审:导出 PNG/JPEG
- 需要高清缩放:导出 SVG
- 需要在 draw.io 二次编辑:导出 draw.io
如果你想把“从文字到图”的时间再压缩一点,最好用一次 AI 生成做底稿:把参与者列表、文字交互、分支条件/重试规则贴进去,让 AI 先生成初版,你再手工校对 guard、消息命名和并发边界。
你可以直接用这个链接打开工具开始画(带着本文这页的上下文更好复现):
8. 常见错误清单(20 条里最致命的 8 条)
下面这些错误,会让你的图“看起来有 opt/loop/par,但读起来不可信”。
- 用 opt 代替 alt:把互斥分支偷成“可选”
- loop 不写上限/停止条件:等于把风险外包给读者
- loop 不写退避/间隔:读者默认紧循环,性能/限流直接爆雷
- par 不写是否需要等待合并:读者不知道响应是否依赖并行结果
- guard 写“成功/失败/异常”:结论当条件,无法落测试
- 片段框住一半消息:关键动作跑到框外,语义变成“你猜”
- 并行路径共享资源不提示:并发写同一数据的竞态被掩盖
- 失败路径不画:只画“理想世界”,评审无法发现边界问题
如果你要在团队里推“画图规范”,这 8 条就够作为首批红线。
9. FAQ:你画 opt/loop/par 时最容易被问到的问题
Q1:opt 和 alt 到底怎么区分?
opt:条件成立才发生;不成立就什么都不发生(没有另一条分支交互)alt:互斥分支;条件不成立时会发生“另一套交互”(哪怕只是返回错误)
一个简单判断:
- 你能不能自然补出一个
[else]?能 → 多半是 alt。
Q2:loop 里要不要画“每次循环的返回消息”?
建议:
- 如果返回结果影响“是否继续循环”,就画(或用注释明确)
- 如果每次返回只是“OK”,且不会影响后续,只画一次也可以,但要在 loop 说明里写清楚“每次迭代都一致”
Q3:par 里的两条并行链路,能不能共享同一个参与者?
可以,但要小心:
- 同一个对象在两个并行块里“同时被调用”,实际实现可能需要并发安全(锁、队列、幂等、线程模型)
如果你不想在图里引入太多实现细节,至少加一句注释:
- “并发调用需要保证 X 的线程安全/幂等”。
Q4:异步消息到底怎么画?一定要用 par 吗?
不一定。
- “异步”强调的是:发送方不等待接收方立即返回
- “并行”强调的是:存在两段独立推进的交互片段
如果只有“发出事件消息”,通常用异步消息箭头 + 不画返回就够了。
只有当你需要表达“与此同时系统还在做别的事”,才用 par。
Q5:loop 能不能和 opt/par 嵌套?会不会太复杂?
能嵌套,但嵌套越深越容易读不懂。建议的做法:
- 先画“主干 + 关键控制结构”
- 把次要的并行副作用/可选埋点用注释或简化消息表示
- 超过 2 层嵌套时,考虑拆图(例如把“异步补偿”单独画一张)
Q6:我需要把每个 guard 写到代码级别吗?
不必写到每个字段的判断,但要做到:
- 团队内能一致理解
- 测试能据此设计用例
- 评审能据此发现边界
比如 [riskScore >= threshold] 就比 [高风险] 好太多。
10. 一份交付前检查清单(建议复制到 PR/评审模板)
在你把时序图放进需求文档/技术方案之前,快速过一遍:
- opt:guard 条件是否可判定?不成立时是否真的“什么都不发生”?
- loop:是否写清 最大次数/停止条件/超时?是否说明退避/间隔?
- par:是否写清“是否需要等待合并”?是否存在共享资源竞态提示?
- 片段框是否覆盖了相关消息的完整边界(调用 + 返回/结果 + 失败处理)?
- 是否至少画出一条关键失败路径(否则评审容易遗漏)?
- 消息命名是否是“动作 + 关键参数/结果”,而不是含糊的“处理/执行”?
- 读图的人能否据此拆出测试用例路径与可观测点(日志/埋点/告警)?
如果你想把这份检查清单直接套到你自己的场景里,最省事的方式是先生成一个可编辑的底稿:
生成后你可以:左侧点选生命线/消息/组合片段快速调整,右侧实时预览看排版;最终一键导出 SVG/PNG/JPEG 或 draw.io,放进你的方案评审材料里。
把 opt/loop/par 用对,你会明显感觉到:
- 需求评审争论少了(因为条件与边界写清了)
- 联调沟通快了(因为重试/并发/可选点一眼能看出来)
- 测试用例更完整(因为 else/上限/超时不再被“省略掉”)
下一次你看到“看起来很复杂的时序图”,先别急着加线条:先问自己——这到底是可选、循环、还是并行?把片段选对,再把边界写清,你的图就已经专业了一半。