软件分析与设计笔记 21:UML:类图与顺序图
为什么需要一种共同的画法:类符号三格怎么画、五种关系的符号与设计含义、每种关系落到 Java 是什么写法,顺序图的三要素怎么摆;以及这三种最容易找不到的符号在 draw.io 里各搜什么词。
本文是 《软件分析与设计笔记》 系列的第 21 篇,共 28 篇
这是我在学习软件分析与设计时整理的系列笔记。概念部分是我按自己的理解写的,示例与图示都是自己编、自己画的,参考的是 UML 规范这类公开资料。理解有偏差的地方欢迎指出。
本篇沿用贯穿案例「片区家电维修派单系统」:工程师上门、仓管备料、客服派单。如果你是从 08 篇读过来的,角色和实体都是同一批。
0 为什么需要一种共同的画法
一个五人团队,如果每个人用自己的方式表达「一张工单有多条配件明细」,就会出现五种画法:有的画箭头、有的写文字、有的直接画表格。讨论的力气全花在「你画的这个箭头什么意思」上。
UML(Unified Modeling Language) 的价值就在这里:它是一套约定的符号,让「这张图在说什么」不再需要额外解释。
注意关键词是「符号」。UML 不规定你怎么做设计,只规定你怎么把设计画出来。
1 UML 在流程里的位置
1.1 各工作流常用哪些图
UML 的各类图并不是都用在同一个阶段,它们对应不同的工作流:
| 工作流 | 常用的图 |
|---|---|
| 业务建模(business modeling) | 用例图(use case diagram)、活动图 |
| 需求(requirements) | 用例图、活动图 |
| 分析(analysis) | 类图(class diagram)、顺序图 |
| 设计(design) | 类图、顺序图 |
| 实现(construction / implementation) | 类图(贴近代码的细化) |
| 测试(testing) | 用例图、顺序图(推导测试场景) |
1.2 同一张图会反复出现
看这张表能发现一件事:同一个类型的图会反复出现,只是细节程度不同。分析阶段画「工单」这个概念,设计阶段可能画到「工单表对应的实体类」。所以不要把「这张类图到底算分析还是设计」当成是非题。
1.3 这一篇的两个主角
类图管静态结构(有哪些类、彼此什么关系),顺序图管动态交互(某一次操作里,这些对象按什么顺序互相调用)。两者是一对:先画顺序图把交互想清楚,再从顺序图机械导出类图(笔记 22 会详细讲这个导出过程)。
2 类图:三格结构与可见性
2.1 类符号的三格
类图(class diagram)里最基本的元素是类符号(class symbol),它由三格组成:
- 类名;
- 属性列表(attribute list) —— 对象需要记住的状态;
- 操作(operation) —— 对象能提供的行为。
2.2 可见性在传达什么
属性前面可以标可见性(visibility):- 表示 private(只有自己可见)、+ 表示 public(所有类可见)、# 表示 protected(子类可见)。
这个符号看着是细节,但它其实在传达设计意图:一个字段被标成 +,等于宣布「任何人都能改它」 —— 这在多数情况下都不是想要的结果(见笔记 20 关于封装的讨论)。
一个具体的写法示例(贯穿案例里的工单类):
WorkOrder
- id : String
- status : WorkOrderStatus
- createdAt : Date
+ assign(engineerId : String) : void
+ close() : void
注意类型的写法是 名字 : 类型,冒号前后各一个空格;返回类型跟在参数括号后面再跟一个冒号。
画这一笔:draw.io 搜
class(UML 分类),拖入画布得到默认三格框,双击任意一格即可编辑。要让某个属性变成-,直接在格子里手打-。 三格之间的横线 draw.io 会自动生成,不用自己画。 官方型(«entity»、«boundary»、«control»)的加法是双击类名上方会出现一行«»,在里面填 entity。
3 五种关系
3.1 符号与含义
| 关系 | UML 符号 | 含义 |
|---|---|---|
| dependency | 虚线 + 开放箭头 | 临时使用 |
| association | 实线(可带重数 multiplicity) | 长期持有引用 |
| aggregation | 空心菱形(菱形在整体一侧) | 部分可独立存在 |
| composition | 实心菱形(菱形在整体一侧) | 部分随整体消亡 |
| inheritance | 空心三角,指向父类 | is-a |
五种关系里有三种在 draw.io 里特别难找(聚合、组合、继承),因为它们不在形状库里,而是改线的端点样式改出来的。这是画类图时最容易卡住的地方:
画这一笔:先连一条普通实线(拖形状边缘的小箭头),然后选中这条线,右侧格式面板会出现
Line一栏:
- 继承 →
Line Start或Line End选 block/open arrow 里的空心三角,并勾选filled=0;- 聚合 → 端点选 diamond,勾选
filled=0(空心);- 组合 → 端点选 diamond,
filled=1(实心)。找不到就在格式面板按
+展开Edit Style,直接打endArrow=block;endFill=0;。这一串记住比记菜单稳。
3.2 每种关系落到 Java 的写法
关键是每种关系落到代码里都不一样:
| 关系 | Java 里的典型写法 |
|---|---|
| Association | 字段持有集合,例如 List<Part> parts |
| Aggregation | 集合字段,但元素由外部传入(构造参数或设置方法) |
| Composition | 集合字段,元素在整体内部创建与管理,不对外暴露 |
| Inheritance | class UrgentOrder extends WorkOrder |
| Dependency | 出现在方法参数或局部变量里,不保留引用 |
贯穿案例里这五种都能找到对应:
- Composition ——
WorkOrder与PartUsage(工单里的配件消耗行)。配件消耗是随工单一起创建、一起删除的,单独存在没有意义; - Aggregation ——
Engineer与Skill(技能)。工程师会换技能,技能本身不依附于人; - Association ——
WorkOrder与Customer。工单长期持有客户引用; - Dependency ——
DispatchService与StockApi。只在方法里调一次,不保留引用; - Inheritance —— 不存在。这个案例里没有真正的 is-a 关系 —— 这一点本身就是个结论,硬要找继承反而会造出笔记 25 讲的「被拒绝的馈赠」。
3.3 符号不是装饰
这些符号不是装饰。把组合画成聚合,不只是图错了 —— 它会直接误导实现的人该怎么管理对象的生命周期。
举个具体的代价:如果 PartUsage 被画成聚合,开发会认为「配件消耗行可以独立存在、可以让多张工单共享」,于是他建了一张独立表和一张多对多关联表。等到要按工单核销的时候,就不得不写一堆中间逻辑把这些行重新归到工单下面。
4 顺序图:三要素
4.1 三个基本元素
顺序图(sequence diagram) 描述对象之间的交互,纵向表示时间。三个基本元素:
- 生命线(lifeline) —— 参与这次交互的每个对象,用竖直虚线表示;
- 消息(message) —— 对象之间的调用,用横向箭头表示,箭头方向就是调用方向;
- 激活条(activation) —— 表示某段时间这个对象正在处理(收到消息到返回结果之间)。
顺序图的符号也在上面那张图里,和类图共用一套箭头样式:
画这一笔:draw.io 搜
sequence,或直接从UML分类拖出一组预置的生命线(它是一个组合好的形状,拖入后自带虚线与激活条)。
- 改名:双击生命线顶部的矩形;
- 加消息:拖生命线边缘的小箭头连到对方,然后双击连线中间打字;
- 返回用虚线:选中消息线,
Line→ 选dashed;- 激活条如果没自动出现,右侧格式面板搜
activation,或直接画一个细矩形叠上去。
4.2 生命线的顺序是有讲究的
摆生命线不是随便排的。分析模型的顺序图有个约定俗成的顺序:
: Actor | : Boundary | : Control | : Entity…
从左到右对应「谁发起 → 界面 → 协调 → 数据」。原因很实际:消息箭头大部分是从左指向右的,按这个顺序摆,交叉线最少,图最好读。笔记 22 会展开这三种对象的职责。
4.3 它还能暴露职责错位
顺序图有一个容易被忽略的优点:它能暴露「谁该知道谁」这个问题。如果一张顺序图里,界面对象直接调用了数据对象、又自己做了业务判断,那这张图本身就在提示职责分错了位置 —— 这正是笔记 22 要展开的内容。
在贯穿案例里有个现成的例子:如果「派单」顺序图里出现 :DispatchForm 直接调用 :Engineer.getSchedule(),这张图就在告诉你:界面越过了控制器去拿数据。改法是中间补一个 :DispatchController。
5 对象图(object diagram)
类图描述「有哪些类」,对象图(object diagram) 描述「某一时刻具体有哪些对象、它们的引用关系如何」,例如 工单A1091、张伟(工程师)。
它用得比类图少,但在排查问题时很有用:当你怀疑「这两个对象之间到底有没有关联」时,画一个对象图比读代码快。
判断什么时候用对象图,就看一句话:你要回答的是「这一类东西之间什么关系」,还是「这两个具体的东西之间什么关系」?前者用类图,后者用对象图。
6 关于工具
建模工具(modeling tool)很多,从在线工具到桌面工具都有。选的时候只需要看两点:
- 能不能正确表达 UML 符号(尤其是四种菱形与三角);
- 改起来方不方便 —— 需求阶段改图是常态,工具如果改一次要重画,你就会开始少改。
工具本身不影响设计质量:用工具画得整齐,不等于关系画对了。
有一个很实际的检查方法:画完一张类图,让另一个人只看图来写代码。如果他写错了某个生命周期语义(比如把组合当聚合处理),那就是图没画对,不是你表达不清。
本节自测
- UML 规范规定了什么、又没有规定什么?
- 同一个类名在分析阶段和设计阶段,画的详细程度为什么不同?
- 聚合和组合在 Java 里的写法差别体现在哪?用贯穿案例里的工单与配件消耗举例说明。
- 顺序图的三个基本元素分别是什么?纵向代表什么?
- 什么情况下画对象图比读代码更有效?
- 在 draw.io 里画「空心三角」表示继承,你是怎么找到它的?为什么这个符号不在形状库里?
- 一张顺序图的四条生命线,按什么顺序摆?为什么?
- 如果「派单」顺序图里
:DispatchForm直接调用:Engineer.getSchedule(),这张图在提示什么问题?
参考答案
- 规定符号,不规定设计。 UML 只规定「怎么把设计画出来」,不管你怎么做设计 —— 用不用继承、要不要分层,都是自己的判断(§0)。
- 因为关注点不同。分析阶段画「工单」这个概念本身(业务语义);设计阶段要画到「工单表对应的实体类」(含字段类型、主键)。所以同一张图会反复出现,只是细节加深(§1.2)。
- 聚合:集合字段但元素由外部传入(构造函数/ setter);组合:元素在整体内部创建、不对外暴露。本例:
WorkOrder与PartUsage是组合(配件消耗行随工单创建与删除),如果把PartUsage画成聚合,开发会以为它可以独立存在、可被多张工单共享(§3.2 / §3.3)。 - 生命线、消息、激活条。纵向是时间,从上往下读(§4.1)。
- 当你要回答的是 「这两个具体对象之间有没有关联」 时。类图只能回答「这一类之间能不能关联」,对象图能直接画出这一份实例的引用关系(§5)。
- 它不是形状库里的独立形状,而是线的端点样式:选中连线 → 格式面板
Line→ 端点选 block/open arrow →endFill=0。也可以直接Edit Style打endArrow=block;endFill=0;(§3.1 的画这一笔)。 : Actor | : Boundary | : Control | : Entity。因为大部分消息箭头是从左指向右的,按这个顺序摆交叉线最少、图最好读(§4.2)。- 它提示职责错位:界面越过控制器直接拿数据。改法是中间补一个
:DispatchController(§4.3)。
术语表
| 术语 | 中文 | 一句话解释 |
|---|---|---|
| UML | 统一建模语言 | 一套约定的建模符号 |
| class diagram | 类图 | 描述类、属性、操作与关系的静态结构图 |
| object diagram | 对象图 | 描述某一时刻具体对象及其引用关系 |
| sequence diagram | 顺序图 | 用消息往来描述交互,纵向表示时间 |
| use case diagram | 用例图 | 描述参与者、用例与系统边界 |
| class symbol | 类符号 | 类图里表示一个类的矩形,通常分三格 |
| attribute list | 属性列表 | 类符号中间那一格,列出对象的状态 |
| operation | 操作 | 类符号下面那一格,列出对象的行为 |
| visibility | 可见性 | 用 +、-、# 表示成员的开放程度 |
| dependency | 依赖 | 临时使用另一个对象的关系 |
| association | 关联 | 长期持有对方引用的关系 |
| multiplicity | 重数 | 一端可以对应多少个实例 |
| aggregation | 聚合 | 部分可独立存在的整体—部分关系 |
| composition | 组合 | 部分随整体消亡的强聚合 |
| inheritance | 继承 | 子类复用并覆盖父类定义的关系 |
| lifeline | 生命线 | 顺序图里表示一个参与对象的竖线 |
| message | 消息 | 对象之间的一次调用 |
| activation | 激活条 | 表示对象正在处理的时间段 |
| endArrow / endFill | 端点样式 / 是否填充 | draw.io 里画菱形与三角的两个关键属性 |
| modeling tool | 建模工具 | 用来绘制 UML 图的软件 |