自调用怎么画:递归/本地方法/内部流程在时序图里怎么表达
讲清时序图里的自调用(self call):什么时候该画、怎么画才规范、嵌套激活条与返回消息怎么处理,并附反例修正与检查清单。
如果你在画时序图时遇到过下面这些纠结,这篇就对上号了:
- “一个对象内部又调用自己,要不要画一条回到自己的箭头?”
- “递归/循环/内部私有方法到底该画到什么粒度?”
- “我画了自调用后,激活条一层套一层,图直接乱成麻花,怎么收?”
- “测试同学问:这条自调用是不是意味着又发了一次网络请求?我怎么解释?”
结论先给:
- 自调用(self call)是合法且常用的画法,用来表达“同一生命线上的执行中再次触发自身的同步调用/内部步骤”,典型场景是递归、模板方法、内部拆分步骤、状态机驱动的 next()、同步重入。
- 但它也最容易被滥用:把“内部实现细节”当作“交互”,或者用自调用假装表达“并发/异步”。
- 你要做的不是“能不能画”,而是先回答一个更重要的问题:读这张图的人想确认什么——接口边界?线程/进程边界?事务边界?失败与补偿?还是仅仅想看一次请求从入口到出口的链路?
下面按“定义 → 适用/不适用 → 规范画法 → 常见反例修正 → 检查清单 → FAQ”来把自调用讲透。
1. 自调用(Self Call)到底在表达什么?
在 UML 序列图/时序图里,“消息”表示在某个时间点发生的交互/调用。当发送方与接收方是同一个生命线(同一个参与者/对象实例/组件)时,就叫自调用。
你可以把它理解成:
- 该对象正在执行一段逻辑(激活条存在)
- 期间它又触发了自己的一次调用(通常是同步调用)
- 这次调用形成了嵌套的执行上下文(所以会出现嵌套激活条)
关键点:自调用不等于“自己想了一下”。它意味着“以调用的方式进入了另一个步骤/函数/分支”,并且对读者有信息价值。
2. 什么时候该画自调用?(适用场景)
面向产品/研发/测试/架构/写文档的人,建议把自调用只用在这些“读者真的需要确认”的场景:
2.1 递归(最经典、也最容易画错)
例如:解析树结构、遍历目录、分治算法、规则引擎匹配。
你画自调用的目的通常不是讲算法细节,而是强调:
- 会重复进入同一方法
- 有明确的终止条件
- 每层递归的输入/输出、可能的异常点
这时自调用非常合适,但要配合 loop 片段 或者用注释标注“递归直到…”。否则读者只会看到一根生命线自嗨。
2.2 内部步骤拆分(读者需要理解关键步骤顺序)
常见于:
- Controller 内部先
validate()再authorize()再execute() - 事务内先写库,再写 outbox,再发消息
- 写文档时想强调“入口方法内部会做哪些关键动作”
这里自调用的价值是:让关键步骤“显式化”。
但注意粒度:不要把每一个私有函数都画出来。你只需要画那些:
- 会导致不同结果(成功/失败/回滚/补偿)
- 会影响外部可观察行为(日志/事件/状态)
- 会改变性能/资源(缓存命中、批处理、锁)
2.3 模板方法/策略选择(突出同一对象内的分派)
比如:
send()内部根据类型选择sendEmail()/sendSms()calculatePrice()内部走不同计算路径
这种情况往往更适合用 alt 组合片段表示分支,然后在分支里用自调用标出关键内部步骤。
2.4 同步重入(Re-entrancy)/回调导致的“再次进入”
例如:某个方法持有锁,调用内部又触发了同对象的另一个同步入口(或者事件处理器),需要强调“重入风险”。
如果你在设计评审/排查死锁,这类自调用是非常有价值的:它提示读者关注锁、事务、线程上下文。
3. 什么时候不该画自调用?(不适用场景)
下面这些情况,自调用常常是“画出来更误导”:
3.1 你其实在表达“并发/异步”
很多人会用“自己给自己发消息”来表示异步队列或事件循环,这会让读者误解为“同一线程内的同步调用”。
如果是异步:
- 用 异步消息箭头(开箭头)
- 或者把队列/事件总线作为独立生命线(如
MQ/EventBus) - 并标注“另一个线程/进程/任务”
3.2 你在画“实现细节”而不是“交互”
时序图的强项是交互与顺序,不是代码结构。
如果你只是想表达“这个函数内部有 12 个私有方法”,那应该:
- 改画流程图/活动图
- 或者在时序图里只保留 3~5 个关键内部步骤
- 其余用注释:“内部细节见代码/设计文档”
3.3 你在“补齐返回值/状态变化”时用自调用凑
比如为了让图更丰满,硬塞一条 updateStatus() 的自调用,但外部并不关心这个细节。
如果你确实要表达状态变化,很多时候用状态标注(如在激活条旁写 state=PAID)更清晰。
4. 自调用怎么画才规范?(核心画法 + 细节规则)
下面用“最小规则集”把自调用画法说清楚。你不需要死背 UML 规范,但要确保读者不会被你误导。
4.1 自调用的基本形态
- 发送者与接收者是同一生命线
- 通常是同步消息:实心箭头 + 返回(可选虚线)
- 会形成嵌套激活条
文字版示意(仅示意,不是代码):
Service收到外部调用handle()Service自调用validate()Service自调用persist()- 返回给外部
这类画法的直觉:外部看到的是一次 handle();内部关键步骤被你显式展开。
4.2 激活条画多长?嵌套激活条怎么不乱
经验规则(对评审最友好):
- 外层激活条覆盖“外部调用的整个执行窗口”
- 自调用产生的内层激活条只覆盖该内部步骤执行时间
- 嵌套不超过 3 层;超过就说明粒度太细,应该抽象
常见的“乱”来自两件事:
- 你把每个内部步骤都画成自调用 → 激活条像千层饼
- 你画了自调用但没画清它的结束点 → 读者不知道什么时候返回到外层
解决方式:
- 只保留关键内部步骤(通常 3~5 个)
- 对重复/递归用
loop包起来,不要一层层手画 - 返回消息要么统一省略,要么统一在关键节点画出(不要有的画有的不画)
4.3 返回消息要不要画?
自调用的返回消息(虚线箭头)一般可以省略,尤其在你画的是“步骤拆分”时。
建议画返回消息的情况:
- 返回值会影响后续分支(例如校验返回
ok/err) - 你要强调“这里会抛异常/返回错误码/触发回滚”
- 测试用例需要从图里读出“哪里可能失败”
否则,省略返回消息 + 在消息名里体现结果(如 validate(): ok)反而更清爽。
4.4 递归怎么画:不要用“无限自调用”吓人
画递归的核心是:
- 标出终止条件
- 标出每次递归“输入收敛”的方向(例如
n-1、depth+1) - 用
loop或注释说明“重复直到…”
推荐画法(概念示例):
Parser -> Parser: parse(node)loop for each childParser -> Parser: parse(child)
end
这样读者明白:是“对子节点重复”,不是“同一节点无限调用”。
4.5 自调用 + alt/opt/loop 的组合用法
自调用经常需要和组合片段配合,否则图会变得含糊。
- alt:内部策略选择(如
encrypt()走AES或SM4) - opt:可选内部步骤(如命中缓存则跳过 DB)
- loop:重复执行内部步骤(批处理/分页/递归/重试)
注意:
- 如果你要表达“重试”,优先用
loop (retry < 3)包住那段调用,而不是画三条一模一样的自调用。 - 如果你要表达“失败后再走补偿”,补偿往往更像另一个步骤链路,必要时可用独立生命线(比如
Compensator)。
5. 反例:为什么你画的自调用“看起来对但不专业”
下面这些反例是评审里最常见的“争论点”。我会给出更推荐的修正方式。
反例 1:用自调用表示“异步事件”
错误画法:
Service -> Service: publishEvent()(看起来像同步调用)
读者会误解为:事件发布与消费都在同一调用栈里完成。
修正:
- 把
EventBus/MQ画成生命线:Service -> EventBus: publish(event)(异步) - 由
Consumer接收:EventBus -> Consumer: onEvent(event)
如果你确实只有单线程事件循环,也应该明确标注“事件循环/异步队列”,不要用模糊的自调用。
反例 2:自调用粒度太细,变成“代码缩进图”
错误画法:
handle()内部每个私有方法都画一条自调用:a()、b()、c()、d()…
读者看完只剩一个感受:信息量很大,但没有任何决策价值。
修正:
- 只保留“可观察、可失败、会分支、会跨边界”的 3~5 步
- 其余用一句注释:“内部还有若干纯计算/格式化步骤,省略”
反例 3:自调用导致返回路径不清晰
错误画法:
- 画了多个嵌套激活条,但没有标注关键返回点
测试/产品会问:“那失败到底在哪返回?返回给谁?”
修正:
- 对关键失败点画返回消息或错误消息(哪怕只有 1~2 条)
- 或在分支里写清:
return error/throw Exception
反例 4:用自调用表达“状态改变”,却没说状态是什么
错误画法:
OrderService -> OrderService: updateStatus()(但状态从哪到哪不写)
读者无法写测试用例,也无法对齐业务口径。
修正:
- 把关键状态写在消息上:
updateStatus(PAID)或status: CREATED -> PAID - 或直接在生命线旁标注状态变化:
[Order.status = PAID]
6. 一张“够用且专业”的自调用时序图,应该包含哪些信息?
面向落地,给你一个最实用的清单:
6.1 对研发/架构:边界与语义
- 自调用代表的是同步调用还是“内部步骤标注”?(图里一致)
- 是否跨线程/进程?如果跨,需要独立生命线,不要用自调用糊弄
- 是否涉及锁/事务?关键点是否标注(例如“持锁调用”“事务内”)
6.2 对测试:可测点与失败点
- 关键分支是否用
alt表达(例如校验失败/权限不足/库存不足) - 关键异常点是否明确(返回错误码/抛异常/回滚/补偿)
- 重试/幂等等行为是否用
loop+ 条件表达
6.3 对产品/写文档:可读性与业务口径
- 关键步骤是否用业务语言命名(如“风控校验”“生成订单号”)
- 图是否能在 1 分钟内讲明白“发生了什么”“失败会怎样”
- 是否避免了“代码名词堆砌”(例如
doBiz()process()这类含糊词)
7. 如何快速画出不乱的自调用(工具落地)
如果你是手动画图,最容易卡在两件事:排版(激活条挤在一起)、以及片段结构(alt/loop 画不齐)。
一个更省时间的做法是用在线生成器把“自调用”当作一类可选组件来插入:
- 左侧编辑器里,你可以点选 生命线 / 消息 / 组合片段(alt/opt/loop/par),把自调用插在正确的位置;
- 工具会自动帮你做基础排版(消息间距、激活条对齐),右侧实时预览,避免“画到一半才发现全挤了”;
- 画完可直接导出 SVG/PNG/JPEG 用于 PRD/技术方案,也能导出 draw.io 方便团队二次编辑;
- 如果你手里只有一段文字交互或接口说明,还可以用 AI 生成先出一个草图,再由你补上关键自调用与片段条件。
需要的话可以直接用这个链接打开模板页(文章内只放一处,不打扰阅读):
7.1 场景案例拆解:订单服务里的“自调用 + 分支 + 重试”怎么画
很多团队第一次把自调用画进交互链路,是在这种“看起来是内部实现,但其实影响测试与稳定性”的场景:
- 入口
createOrder()看起来就是创建订单 - 但内部会做一串关键步骤:风控、库存预占、价格计算、写库、发消息
- 其中有些步骤会失败,有些会重试,有些需要幂等
如果你不把这些关键内部步骤画出来,评审时经常会发生这种对话:
- 产品:失败时到底给用户什么提示?
- 测试:库存预占失败和支付超时要不要重试?重试几次?
- 架构:事务边界在哪里?消息一定发得出去吗?
这时你可以用“外部交互 + 关键自调用”的组合,把信息放在该放的位置。
建议画法(用词示意):
API -> OrderService: createOrder(req)(外部入口)OrderService -> OrderService: validate(req)(自调用:入参校验,失败直接返回)OrderService -> RiskService: check(user, req)(跨服务交互)alt 风控拒绝OrderService --> API: 403(或业务错误码)returnelse 通过OrderService -> InventoryService: reserve(sku, qty)
loop retry < 3 when timeout- (把“重试条件”写清:超时重试 vs 业务失败不重试)
OrderService -> OrderService: persistOrder(status=CREATED)(自调用:写库 + 明确状态)OrderService -> OrderService: writeOutbox(event=OrderCreated)(自调用:保证消息可达的关键步骤)OrderService --> API: orderId
你会发现,自调用在这里的作用不是“展示代码结构”,而是把评审里必须对齐的点显式化:
- 哪些失败点会直接返回?(validate / 风控)
- 哪些失败点会重试?(超时类)
- 哪些动作必须在事务内?(persist + outbox)
- 哪些动作是跨边界交互?(Risk/Inventory)
写文档时一个很实用的标准:如果一个内部步骤会改变“对外可观察结果”(返回码、状态、事件、补偿),它就值得在时序图里被看见。
7.2 自调用消息怎么命名才不含糊(给研发/测试看的“可执行语义”)
自调用最怕的不是画法,而是命名。
- 糟糕命名:
process()、doBiz()、handle()(读者无法写测试,也无法对齐业务语义) - 更可读命名:
validate(req)(校验什么?失败返回什么?)authorize(user, action)(鉴权/租户隔离)persistOrder(status=CREATED)(明确状态变化)calculatePrice(ruleVersion=2026-03)(参数影响分支就写)
如果你担心“消息太长”,可以用两层结构:
- 消息名保持短:
persistOrder() - 关键信息写在旁注/注释:
[status: DRAFT -> CREATED]、[tx]、[idempotent by orderKey]
这样测试同学能从图里直接抽出用例:
- 校验失败返回什么?
- 风控拒绝是否会落库?
- 预占超时是否重试?重试上限是什么?
- outbox 写入失败是否回滚?
7.3 用工具把“自调用”画得不挤:排版与导出小技巧
自调用一多,图最容易出现两个问题:
- 激活条挤在一起,读不清嵌套关系
- alt/loop 片段边框对不齐,视觉上很“业余”
如果你用的是在线时序图生成器,一般可以用这些方式快速变专业:
- 左侧编辑器里先把 生命线 和对外 消息 搭好,再逐个点选插入自调用;
- 自调用只保留 3~5 个关键步骤,其余折叠成注释;
- 需要给方案评审/PRD 用:优先导出 SVG(放大不糊);
- 需要团队协作二次编辑:导出 draw.io,让同事在现有框架上改,不用重画;
- 你只有一段“文字交互/接口说明”时,先用 AI 生成出骨架,再手工补齐 loop/alt 条件与自调用命名。
如果你想直接拿一个可编辑模板开始画(包含生命线、消息、片段占位),可以从这里打开:
8. 自调用画法检查清单(交付前 2 分钟自检)
交付给团队之前,按下面勾一遍,基本不会被挑刺:
- 这张图的读者是谁(研发/测试/产品/外部合作方)?自调用的粒度是否匹配?
- 自调用是否只用于“读者需要知道的关键内部步骤/递归/重入”?
- 是否存在“用自调用表示异步/并发”的误导?如果有,是否拆出
MQ/EventBus/线程生命线? - 嵌套激活条是否超过 3 层?超过则考虑抽象/折叠/改用 loop 说明
- 递归/循环是否有终止条件/循环条件(loop 条件写清)?
- 关键失败点是否能从图上读出来(alt 分支/错误返回/异常)?
- 命名是否用业务语义(避免
process/doBiz),必要参数/状态变化是否写明? - 返回消息的策略是否一致(要么关键点画、要么统一省略)?
- 图是否能导出到文档中保持清晰(SVG 优先),需要协作时是否提供 draw.io 版本?
9. FAQ:团队最常问的几个问题
Q1:自调用到底画“对象级”还是“类/模块级”?
看你的受众和目的:
- 讨论模块协作、接口边界:建议画“组件/服务”级生命线,自调用只表示关键内部步骤
- 讨论代码结构、重入/锁:可以画到“对象/类”级,但仍要控制粒度
一句话:时序图优先讲交互,不是 UML 类图的替代品。
Q2:自调用要不要画参数?
- 影响分支/结果的参数:建议写(例如
parse(child)、retry(count)) - 纯技术细节参数:可省略,否则图会很长
Q3:我能用自调用表示“内部状态机推进”吗?
可以,但要把“状态”写出来,否则读者无法复现:
next(state=INIT)→next(state=RUNNING)
如果状态很多,建议配合状态图/文字说明,不要把所有状态推进都塞进时序图里。
Q4:自调用和 loop 片段怎么选?
- 你要表达“重复发生” → loop 必须有
- 你要表达“每次重复的关键步骤是什么” → 在 loop 里放一个自调用(或一个关键调用)
不要用多条自调用堆出“看起来重复”,那会让图失去结构。
Q5:图里自调用太多,怎么降噪?
三招够用:
- 合并:多个内部步骤合并成一个业务动作(如“校验与鉴权”)
- 折叠:把细节写成注释或引用到子图(例如“见子图:校验流程”)
- 换图:真正想讲算法,就别硬塞时序图,改用活动图/伪代码/单元测试说明
如果你准备把“自调用 + 递归/重试/分支”画成团队可复用的模板,建议先用工具生成一个基础框架:生命线、消息、alt/loop 片段先搭起来,再把关键自调用插到正确的位置。这样既省时间,也更容易让评审对齐。