ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

通信图实战指南:从语法到评审,掌握UML协作拓扑设计

通信图实战指南:从语法到评审,掌握UML协作拓扑设计

1. 为什么通信图在今天依然值得花时间学

很多人画UML,画到类图、时序图就停了,通信图(Communication Diagram)往往被当成"时序图的另一种画法"顺手带过。我在带团队做系统设计评审时,见过太多这种情况:一份时序图能画满三屏,消息从上往下排得整整齐齐,但一旦要回答"这几个对象之间到底谁跟谁有直接联系、消息沿着哪条链路走"这类问题,图上看不出来,得靠嘴讲。通信图恰好补的就是这块——它把对象之间的连接关系摆在最显眼的位置,消息挂在连接线上,一眼就能看出协作结构。

通信图在UML 1.x时代叫协作图(Collaboration Diagram),UML 2.0之后统一改称通信图。它和时序图在语义上是等价的,两者可以互相转换,但侧重点完全不同:时序图强调时间顺序,通信图强调空间结构。这个区别不是学术上的文字游戏,它直接决定了你该在什么场景下用哪张图。当你需要向别人解释"这个功能涉及哪些对象、它们怎么串起来的",通信图比时序图直观得多;当你需要说清楚"第几步先做、第几步后做、有没有并发分支",时序图更合适。

这篇文章面向的是需要做系统设计表达的人——不管是写设计文档的开发者、做架构评审的技术负责人,还是准备软考或课程作业的学生。我会从通信图的构成元素讲起,把语法、画法、和时序图的取舍、实际项目里的落地技巧、以及新手最容易踩的几个坑都摊开说。文中所有示例都是虚构的模拟场景,代码和工具操作基于常见实践补充,你可以直接照着复现。

先说结论:通信图不是时序图的廉价替代品,它解决的是"协作拓扑"这个独立问题。把它用对地方,评审效率能明显提升;用错地方,就是白费功夫画了一张没人看的图。

2. 通信图的构成元素与语法规则拆解

2.1 对象、链接与消息:三件套的准确含义

通信图的基本元素只有三类,但每一类的语义边界很多人没搞清楚。

对象(Object)用矩形表示,里面写对象名:类名的格式,比如orderService:OrderService。对象名可以省略,只写:OrderService表示匿名对象;类名也可以省略,只写orderService表示一个未指定类型的实例。实际画图时我建议至少保留类名,因为通信图的价值就在于展示类型之间的协作,光有对象名读者不知道它是什么东西。对象下方可以加下划线表示这是一个实例而非类,这是UML的规范细节,很多工具默认就带下划线,不用刻意纠结。

链接(Link)是两个对象之间的连线,表示它们之间存在关联关系,消息只能沿着链接传递。这一点是通信图的灵魂——没有链接的两个对象不能直接发消息。链接可以是单向的也可以是双向的,用箭头或实线表示。这里有个容易混淆的点:链接表达的是"运行时可通信",它不一定对应类图里的关联关系,但通常是一致的。如果你发现两个对象需要通信却没有链接,那说明设计上缺了一根关联,这本身就是通信图帮你发现的问题。

消息(Message)挂在链接上,用带箭头的线段加文字标签表示,格式通常是序号: 消息名(参数列表)。消息的箭头方向表示调用方向,序号表示执行顺序。通信图里消息的编号是强制的,因为图上没有垂直方向的时间轴,顺序完全靠编号表达。

把这三者串起来理解:对象是节点,链接是边,消息是边上流动的内容。通信图本质上画的是一个带标签的图结构,而不是一条时间线。

2.2 消息编号的嵌套规则:1、1.1、1.2 到底怎么排

消息编号是通信图里最需要讲清楚的部分,也是最容易画错的地方。UML支持两种编号风格:顺序编号和嵌套编号。

顺序编号就是简单的 1、2、3、4……按消息发生的先后依次递增,不管消息属于哪个对象的处理过程。这种风格简单,但丢失了调用层次信息。

嵌套编号用小数点表达调用层级,格式是外层编号.内层编号。比如对象A调用对象B的doWork()是消息1,B在处理过程中又调用了C的query(),那这条消息编号就是1.1;如果B还调用了D的save(),就是1.2。如果1.1里C又调用了E,那就是1.1.1。这种编号方式把调用栈的嵌套关系直接编码进了编号里,读图时能清楚看出谁在谁的上下文里执行。

我个人的习惯是:只要调用层次超过两层,就用嵌套编号。顺序编号在扁平的单层协作里够用,但一旦出现"处理过程中再调用"的情况,嵌套编号的信息量优势非常明显。下面这个虚构的下单场景可以说明问题:

1: placeOrder(order) 1.1: checkStock(itemId, qty) 1.1.1: queryInventory(itemId) 1.2: calculatePrice(order) 1.3: createPayment(amount) 1.3.1: reserveFunds(amount) 1.4: confirmOrder(orderId)

从编号就能读出:placeOrder是入口,它依次触发了库存检查、价格计算、创建支付、确认订单;库存检查内部又查了一次库存明细;创建支付内部又做了资金预占。这种层次感是顺序编号给不了的。

注意:嵌套编号里同一层级的多条消息,编号必须唯一且递增,不能出现两个1.1。有些工具允许你手动改编号,改完一定要通读一遍,确认没有重号或跳号。

2.3 迭代、条件与并发消息的标注方式

通信图支持在消息标签上加约束,表达循环、条件分支和并发。语法上通常用方括号或星号:

  • 条件消息:[condition] 序号: 消息名(),表示满足条件才发送。比如[stock > 0] 1.1: deductStock()。
  • 迭代消息:序号: 消息名()前面加*或[for each item],表示对集合中每个元素重复发送。比如*[for each item] 1.2: validateItem(item)。
  • 并发消息:多条消息用相同的序号前缀但不同后缀表示并发,或者用1a、1b这种字母后缀。不同工具支持程度不一,画之前先确认工具能力。

这里有个实操经验:条件和迭代标注不要堆太多。通信图的可读性本来就依赖图面清爽,如果每条消息都挂一串约束,图就变成代码了。我的做法是只标注影响主流程走向的关键条件,次要的约束放到图下方的说明文字里。图是给人快速抓结构的,不是给编译器看的。

2.4 一个完整的语法示例

把上面的元素组合起来,一个虚构的"用户登录校验"通信图大致是这样组织的:参与者client:Client链接到authController:AuthController,后者链接到userService:UserService和tokenService:TokenService,userService再链接到userRepo:UserRepository。消息编号如下:

1: login(username, password) 1.1: authenticate(username, password) 1.1.1: findByUsername(username) 1.1.2: verifyPassword(password, hash) 1.2: [authSuccess] issueToken(userId) 1.2.1: generateToken(userId) 1.3: returnToken(token)

读这张图,你能立刻看出:认证逻辑集中在userService,它依赖仓储查用户、自己做密码校验;令牌签发是独立职责,由tokenService承担;控制器只做编排。这种职责分布的直观性,正是通信图在评审场景下的杀手锏。

3. 通信图与时序图:同一语义下的两种表达,怎么选

3.1 语义等价但信息重心不同

前面提过,通信图和时序图在UML语义上是等价的,理论上任何一张通信图都能转成时序图,反之亦然。但"能转"不等于"该转",两者的信息重心差异决定了适用场景。

时序图的重心是时间轴:消息按垂直方向排列,生命线从上到下延伸,激活条表示对象处于处理状态。它天然适合表达"先做什么、后做什么、哪里等待、哪里并发"。缺点是对象之间的链接关系被弱化了——生命线之间的箭头只表示这一次消息传递,不代表两者之间存在持久关联。

通信图的重心是结构拓扑:对象和链接构成一张网,消息挂在网上。它天然适合表达"这个协作涉及哪些角色、谁依赖谁、消息沿哪条路径流动"。缺点是时间顺序要靠编号脑补,读图时需要一定的"解码"成本。

我做过一个粗略的观察:在评审会上,如果问题是"这个流程的步骤对不对",大家更愿意看时序图;如果问题是"这个设计里对象职责划分合不合理、有没有多余的耦合",通信图更容易引发有效讨论。原因很简单,通信图把耦合关系画在了明面上,多余的链接一眼就能看出来。

3.2 什么场景优先用通信图

基于实际使用经验,我总结了几类通信图明显更占优的场景:

第一类是协作结构说明。当你要向新加入的同事解释"这个模块由哪些对象组成、它们怎么协作完成一个功能",通信图比时序图快得多。因为读者第一眼看到的是对象网络,而不是一长串消息。

第二类是耦合关系审查。做架构评审时,我经常让团队先画通信图,专门看链接数量。一个对象如果挂了七八条链接,那它大概率承担了过多职责,是个重构信号。时序图看不出这个,因为生命线再多也不显得"拥挤"。

第三类是消息路径追踪。当排查"一个请求到底经过了哪些对象"这类问题时,通信图的路径感更强。沿着链接走,消息编号告诉你顺序,比在时序图里上下翻找更省力。

第四类是文档篇幅受限的场合。同样一个中等复杂度的协作,时序图可能因为消息多而拉得很长,通信图在二维空间里铺开,往往更紧凑。当然这取决于工具排版,不是绝对规律。

3.3 什么场景老老实实用时序图

反过来,有些场景硬用通信图就是给自己找麻烦:

  • 强调时序和并发:涉及异步回调、超时处理、多线程并发的场景,时序图的激活条和垂直布局表达力强得多。通信图里表达并发要靠编号技巧,读者理解成本高。
  • 消息数量多且线性:一个流程有二十几步顺序调用,时序图从上到下一目了然,通信图编号会排到1.15这种,读起来累。
  • 需要展示对象生命周期:创建和销毁在时序图里有明确表示(消息指向生命线顶端、生命线末端打叉),通信图表达这个很别扭。
  • 和外部系统交互的协议说明:比如和第三方接口的调用约定,时序图更接近"对话脚本"的直觉。

我的实际做法是两张图配合用:设计文档里放一张通信图讲清协作结构,再放一张时序图讲清关键流程的时序。两者不是重复,是互补。如果只能选一张,看你的读者最需要回答哪个问题——结构问题选通信图,时序问题选时序图。

3.4 转换时的注意事项

从时序图转通信图(或反向)时,有几个细节容易丢:

  • 链接关系要补全:时序图里两条生命线之间有消息,不代表它们之间有持久链接。转成通信图时,你得判断这条消息是沿已有链接走的,还是需要新增一条链接。这个判断过程本身就是在审视设计。
  • 编号要重排:时序图的消息顺序是位置隐含的,转成通信图必须显式编号,嵌套关系要重新梳理。
  • 激活期信息会丢失:时序图的激活条表示对象处于处理中,通信图没有直接对应物,只能靠编号层级间接表达。如果激活期信息重要,别转。
  • 返回消息的处理:时序图里返回消息常用虚线箭头表示,通信图里返回消息通常省略或用编号体现,转换时别把返回消息当成独立调用。

提示:如果你用的建模工具支持自动转换,转完一定要人工核对链接和编号。自动转换工具对"消息沿哪条链接走"的判断经常出错,尤其是当一个对象和多个对象都有链接时。

4. 从零画一张通信图的完整操作链路

4.1 建模前的准备:先想清楚协作边界

动手画之前,最该做的一件事是确定这次协作的边界。通信图画的是一个特定场景下的对象协作,不是整个系统的对象全景。边界没定好,图会越画越大,最后变成一张谁也看不懂的蜘蛛网。

我的做法是先写一句话描述这个协作:"用户提交订单后,系统完成库存扣减、支付创建和订单确认。"这句话里的动词就是消息,名词就是候选对象。然后问三个问题:这个协作的触发者是谁?参与者有哪些?到哪里算结束?答案就是图的边界。

候选对象列出来后,做一次职责归并:功能太碎的对象合并,职责重叠的对象拆分。比如"库存检查"和"库存扣减"如果都由同一个服务承担,就画一个对象;如果分属两个服务,就画两个。这一步决定了图的粒度,粒度太细图会爆炸,太粗又失去信息量。经验值是一张通信图控制在5到9个对象,超过这个数就该考虑拆成多张图,每张讲一个子协作。

4.2 工具选型:不同工具的通信图支持差异

通信图的工具支持参差不齐,选工具时重点看三件事:链接是否好画、消息编号是否支持嵌套、排版是否可控。

工具类型代表能力通信图支持情况适用场景
通用绘图工具自由拖拽、样式灵活需手动维护编号和链接,无语义校验快速草图、文档配图
专业建模工具语义模型驱动、可转换支持嵌套编号、链接语义、时序图互转正式设计文档、评审
代码化建模文本描述生成图语法严格,编号自动生成版本管理、CI集成

通用绘图工具上手快,但编号全靠手打,改一处要连带改一串,图一复杂就容易出错。专业建模工具语义严谨,但学习曲线陡,而且不同工具对通信图的细节支持差异不小,比如有的工具不支持并发消息的字母后缀。代码化建模适合把图纳入版本管理,文本 diff 比图片 diff 有用得多,但语法门槛在那儿,团队得统一接受。

我个人的组合是:草图阶段用通用绘图工具快速对齐思路,定稿用专业建模工具或代码化建模保证语义准确。如果团队有文档即代码的习惯,代码化建模的长期收益最高,因为图能跟着代码一起 review。

4.3 分步绘制:从对象布局到消息标注

具体画法按这个顺序走,能少返工:

第一步,摆对象。把触发者放左上或顶部,主要参与者围绕它铺开,被调用方往外围放。布局原则是让消息流向大致从外向内或从上到下,减少连线交叉。这一步不用追求完美,后面还会调。

第二步,连链接。只连确实需要通信的对象对。连的时候问自己:这两个对象之间是持久关联还是临时调用?如果是临时调用(比如工厂创建对象后就不管了),链接可以省略或用虚线标注。链接画完,先通读一遍,看有没有"绕远路"的路径——A通过B才能到C,但A和C其实可以直接连,这种间接路径往往是设计可以简化的信号。

第三步,标消息。从入口消息开始,按调用顺序编号。每标一条,确认它挂在哪条链接上、方向对不对、参数是否必要。参数列表我建议只写关键参数,全写会让标签过长,图面拥挤。

第四步,加约束。把影响主流程的条件和迭代补上,次要约束放图注。

第五步,排版微调。调整对象位置减少交叉,统一消息标签的对齐方式。这一步花的时间往往比前面加起来还多,但值得——一张排版清爽的图,读者愿意多看两眼。

4.4 一个可复现的完整示例

用一个虚构的"文件上传与转码"协作来走完整流程。场景:客户端上传文件,服务端存储后触发转码,转码完成后通知客户端。

对象清单:client:Client、uploadController:UploadController、storageService:StorageService、transcodeService:TranscodeService、notifyService:NotifyService。

链接关系:client—uploadController,uploadController—storageService,uploadController—transcodeService,transcodeService—notifyService,notifyService—client(通知回推)。

消息编号:

1: upload(file) 1.1: saveFile(file) 1.1.1: writeToDisk(file) 1.2: requestTranscode(fileId) 1.2.1: enqueueJob(fileId) 1.3: returnUploadResult(fileId) 2: [transcodeDone] pushNotification(fileId, status) 2.1: sendToClient(fileId, status)

读图要点:上传是同步链路(1.x),转码是异步触发(1.2.1 入队后即返回),通知是转码完成后的回调(2.x)。这里2的编号独立于1,表示它是另一个触发源(转码完成事件),不是上传调用的直接延续。这种"多入口"的编号处理是通信图的一个细节——不同触发源的消息序列用不同的顶层编号,避免读者误以为 2 是 1 的后续。

注意:异步消息在通信图里没有专门的图形符号(不像时序图有开放箭头),只能靠编号和文字说明区分。我通常会在异步消息标签后加(async)标注,图注里再说明触发条件。

5. 实际项目中的落地技巧与常见坑

5.1 图面拥挤的破解:分层与拆分

通信图画到六七个对象以上,连线交叉就开始失控。我踩过最惨的一次是画一个订单履约协作,硬塞了十二个对象,最后图上一半的线是交叉的,评审时没人看得下去。

破解办法有两个。一是分层:把协作按阶段拆成多张图,比如"下单阶段""支付阶段""履约阶段"各一张,图之间用对象复用衔接。二是抽象:把一组紧密协作的对象打包成一个"子系统"框,框内细节另图展开。UML的通信图本身不直接支持子系统框,但可以用一个对象代表整个子系统,图注说明"内部协作见附图"。

还有一个排版技巧:把链接最多的对象放中心,其余对象呈放射状布局。这样连线最短、交叉最少。如果中心对象链接太多(超过五条),那本身就是个设计信号——它可能是个"上帝对象",值得在评审时提出来。

5.2 编号维护的自动化思路

手改编号是通信图维护的最大痛点。加一条消息,后面所有编号可能要顺延,漏改一处就出错。我的应对策略是尽量用工具自动编号,专业建模工具和代码化建模都支持插入消息时自动重排。如果只能用通用绘图工具,那就养成"先定结构再标号"的习惯,结构稳定后再统一编号,减少返工。

代码化建模在这方面优势明显,因为编号由渲染引擎根据消息的嵌套结构生成,改结构不用手动改号。下面是一个虚构的文本化描述片段,展示思路(具体语法因工具而异):

client -> controller: submit(data) controller -> service: process(data) service -> repo: save(data) controller -> notifier: notify(result)

渲染时工具会自动把process编为1.1、save编为1.1.1、notify编为1.2。这种"描述结构、编号自动生成"的方式,长期维护成本最低。

5.3 通信图在评审中的正确用法

通信图在评审会上最大的价值不是"展示我画得多好",而是暴露设计问题。我通常引导评审关注三个点:

链接数量异常。某个对象链接数明显多于其他,追问它是不是承担了过多职责。这是重构的强信号。

消息绕路。A要调C,却必须经过B转发,追问这个转发是否必要。如果B只是透传,那A和C可以直接连,少一层耦合。

缺失的链接。两个对象在流程里明显需要通信,图上却没有链接,说明设计里缺了一根关联,或者它们通过全局状态通信——后者往往是坏味道。

把通信图当成"设计体检表"来用,比当成"文档配图"来用价值高得多。我在实际评审中见过好几次,一张通信图画出来,团队自己就发现了循环依赖的问题,根本不用别人指出。

5.4 新手最常踩的四个坑

坑一:把通信图画成时序图的横版。表现是所有消息都从同一个对象发出,其他对象只是被动接收,图上看不出对象间的相互协作。根因是没理解通信图表达的是"协作网络"而非"调用链"。修正方法是强迫自己找出对象之间的双向或交叉调用,如果确实没有,那这个场景可能更适合时序图。

坑二:链接画成消息箭头。有人把每条消息都画成一条独立的带箭头连线,结果两个对象之间出现好几条平行线。正确做法是一条链接承载多条消息,消息以标签形式挂在链接旁。这个错误在通用绘图工具里特别常见,因为工具不会阻止你多画线。

坑三:编号层级混乱。表现是1.1和1.2之间插了个1.1.1,或者同一层级出现重号。根因是边画边编号、结构变动后没重排。修正方法是结构定稿后统一编号,并用工具校验。

坑四:对象粒度失控。要么把整个系统画成一个对象(信息量为零),要么把每个方法都画成对象(图爆炸)。粒度应该对齐"职责单元"——一个对象承担一组内聚的职责,而不是一个类或一个方法。

提示:画完通信图后,做一次"反向朗读"——从入口消息开始,按编号顺序把每条消息读出来,读的过程中检查:这条消息的接收者是否真的有这条链接?编号是否连续?条件是否合理?这个习惯能抓出大部分低级错误。

6. 通信图与其他UML图的配合,以及它的能力边界

6.1 和类图、时序图、活动图的分工

通信图很少单独存在,它通常和另外几张图配合,各管一段。

类图管静态结构:有哪些类、类之间什么关系。通信图管动态协作:这些类的实例在某个场景下怎么协作。两者的衔接点是链接——通信图的链接通常对应类图的关联,如果对不上,要么类图缺关联,要么通信图多画了链接,都值得查。

时序图管时间顺序:前面说过,和通信图语义等价、重心不同。我的文档习惯是通信图在前讲结构,时序图在后讲关键流程,读者先建立结构认知,再看时序细节,理解成本更低。

活动图管业务流程:活动图关注"做什么步骤、什么条件分支",不关注"谁来做"。通信图关注"谁和谁协作"。一个业务流程可以用活动图讲清步骤,再用通信图讲清每步由哪些对象承担,两者互补。

这四张图的分工可以用一句话概括:类图看静态、活动图看流程、时序图看时序、通信图看协作拓扑。选图时先问自己要回答哪个问题,答案就出来了。

6.2 通信图表达不了的东西

通信图有明确的能力边界,硬用它表达超出边界的内容,图会变得别扭且低效。

它表达不了精确的时序。编号能表达先后,但表达不了"消息A发出后等待多久才发B""A和B是否严格串行"。涉及超时、延迟、竞态的场景,时序图或状态机更合适。

它表达不了对象生命周期。对象的创建和销毁在通信图里没有标准表示,只能靠文字说明。如果生命周期是关键信息,别用通信图。

它表达不了复杂的并发控制。虽然可以用字母后缀标注并发,但锁、信号量、线程池这些并发机制,通信图无能为力。这类场景用时序图配合文字说明更清楚。

它对消息内容的表达有限。消息标签能写参数名,但写不下完整的消息契约(前置条件、后置条件、异常)。这些应该放在接口文档或契约图里,通信图只做索引。

认清边界不是贬低通信图,恰恰相反——知道它擅长什么、不擅长什么,才能在对的地方用它,让它发挥最大价值。我见过有人硬用通信图表达一个带超时重试的异步流程,图画得极其复杂,读者看完还是一头雾水,换成时序图加一段文字说明,问题立刻清楚。工具没有好坏,用对地方才是本事。

6.3 一个组合使用的实例

用一个虚构的"退款审批"场景说明组合用法。活动图先讲业务流程:用户申请退款→系统校验资格→人工审批→执行退款→通知用户。然后通信图讲"人工审批"这一步的协作:approvalController接收审批请求,链接到approvalService(审批逻辑)、refundService(退款执行)、auditLog(审计记录)。最后时序图讲"执行退款"内部的调用顺序和异常处理。

三张图各司其职,读者先看流程全貌,再看协作结构,最后看关键时序。这种组合比单张大图有效得多,也符合"一张图讲一件事"的原则。我在实际文档里坚持这个结构,评审时读者提问的定位效率明显更高——问流程看活动图,问职责看通信图,问顺序看时序图,各取所需。

最后分享一个我在实际使用中的体会:通信图的价值不在于"画得规范",而在于"画的过程逼你把协作关系想清楚"。很多时候图画到一半,自己就发现某个对象的职责边界模糊,或者某两个模块耦合过紧。这种"画图即设计"的收益,比图本身作为交付物更有意义。所以别把画通信图当成文档任务,把它当成一次设计自检,收获会大不一样。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进