1. 对象图不是拿来背的它是某一刻的业务切片带过几批新人之后我发现UML对象图是整套图里最容易被“背废”的一块。考试能默写“对象是类的实例链是关联的实例”一到真实项目里让他画一张订单对象图立刻就卡住对象名写不写、下划线画在哪、属性槽填什么、链要不要标多重性全是问号。问题不在记性在于教材把对象图从业务场景里抽离出去了只剩一堆符号规则谁背谁忘。这篇就拿航空购票系统当例子把UML对象图里的对象和链讲透。你会在里面看到一个订单从下单到支付成功之后的瞬时状态看到乘客、订单、客票、航班、座位、支付记录这些对象怎么用链连起来也会看到对象图怎么反过来帮你抓出类图里漏掉的关联。适合两类人看正在学UML、被对象图符号绕晕的学生以及工作里要写设计文档、但一直把对象图画成“第二张类图”的开发者。先把话说在前面所谓“5分钟搞懂”5分钟指的是画图动作前提是类图已经在你脑子里成型了。对象图从来不是凭空造出来的它是类图在某个时刻的实例化快照。类图是图纸对象图是按图纸造出来、并且正在运行的那台机器。你不可能跳过图纸直接造机器也不可能背几个符号就学会看图纸。所以这篇会先用一大段把航空购票系统的类结构理干净再动手画对象图最后聊踩过的坑。整个节奏跟真实项目里的做法一致你照着走一遍比对着教材抄十遍定义管用。2. 先把类图打底没有类对象图就是无根之木2.1 从购票业务里捞“名词”和“动词”建模最朴素的办法还是找名词。把“乘客在App上选航班、选座位、提交订单、完成支付、拿到客票”这条主线拆开能捞出的名词有乘客、航班、订单、客票、座位、支付记录。动词则是下单、占座、支付、出票、退改签。名词大概率对应类动词大概率对应方法或者状态流转。这里有个取舍要先定订单和客票是不是两个东西。很多初学者图省事把订单当成“一次购买行为的容器”客票当成“一张票”这个拆分是对的。一个订单可以包含多张客票比如两位同行旅客一起下单生成两张客票、共用一个订单号、共用一次支付。如果合并成一个类后面退票只退其中一张的时候你就会发现状态没法表达。这个判断不是拍脑袋是业务规则逼出来的支付是订单级行为乘机是客票级行为两者生命周期不同必须分开。再一个取舍座位要不要独立成类。如果只是记录“32A”这个字符串确实可以塞进客票属性里。但真实系统里座位有座位类型靠窗、过道、紧急出口、有可用状态、有选座锁定还要防止两个客票选中同一个座位。这些信息和客票的生命周期不完全一致独立成类更稳。对象图阶段你会更直观地感受到这一点当两张客票的链指向同一个座位对象时问题一眼就暴露了。2.2 六个核心类的属性与方法定稿类图不用画得多漂亮但属性类型得定清楚不然对象图的属性槽没法填。下面这张表是我在这个示例里用的类结构属性只保留跟对象图相关的部分方法列出来是为了后面讲状态对应关系。类名关键属性关键方法乘客乘客编号、姓名、证件号、联系方式、常旅客等级查询行程、累积里程订单订单号、下单时间、订单状态、总金额提交、取消、发起支付客票票号、舱位等级、票价、税费、客票状态、旅客姓名出票、退票、改签航班航班号、起飞地、降落地、计划起飞时间、机型、剩余座位数查询余票、锁定座位座位座位号、座位类型、座位状态锁定、释放支付记录支付流水号、支付金额、支付渠道、支付状态、支付时间发起支付、查询结果属性名和类型必须定死比如“总金额”是金额类型、“下单时间”是日期时间类型、“订单状态”是枚举。这一步偷懒后面画对象图时就会出现“属性槽里写了个类型名”的经典错误我在第5节会专门列出来。2.3 关联和多重性决定后面链能画几条对象图里的每一条链背后都必须有一条类图里的关联撑着。所以关联和多重性得先定而且要多问一句“业务上真的允许这样吗”。乘客 1 — 0..* 订单一个乘客可以下多单一个订单必须有一个下单人。订单 1 — 1..* 客票一个订单至少一张客票否则订单没有意义。客票 * — 1 航班每张客票必须挂一个航班一个航班可以被大量客票引用。客票 1 — 1 座位一张客票对应一个座位一个座位在同一航班同一时刻只能被一张客票占用。订单 1 — 0..1 支付记录一个订单最多一条有效支付记录未支付时为零条。注意“客票 1 — 1 座位”这条很多人的第一版类图会写成* — *觉得“一个航班很多座位、一个座位历史上有过很多客票”。这是把当前占用和历史记录混在一起了。当前时刻一个座位只能被一张有效客票占用历史客票通过航班和日期区分不应该体现在这条关联的多重性上。这个细节在对象图里会变得非常直观你画两条链指向同一个座位对象就说明当前快照违反了业务约束。还有一条容易漏的关联乘客和客票之间的“旅客”关系。如果同行两位旅客一起下单下单人只有一个但两张客票上各有一位旅客。常旅客积分要记在谁头上里程累积给谁如果只保留“乘客—订单”这一条关联第二位旅客的信息就无处安放。这个漏洞正是我在画对象图时发现的后面第6节会详细说。3. 五分钟画出一张对象图以“两人同行下单并支付成功”为快照3.1 选快照时刻先定问题再定图对象图最忌讳“我想把整个系统画下来”。它本质是一张照片你得先决定拍什么。我给自己定的问题是两位旅客同行、同一个订单、支付已完成、客票已出票这个瞬间系统里有哪些对象它们怎么连快照时刻定为2024-05-12 09:20。这个时间点很关键因为订单状态、支付状态、客票状态必须是同一时刻的不能一个写“已支付”、另一个写“待支付”。很多对象图画出来自相矛盾根因就是没有统一快照时刻把不同时间点的状态拼在了一张图上。对象选择上做减法只画跟这个问题相关的六个类对应的实例。乘客画两个下单人和同行旅客订单一个客票两张航班一个座位两个支付记录一条。一共八个对象。超过十个对象图就开始糊评审时没人看得下去。3.2 建对象命名、下划线、属性槽三件套对象框的写法是固定的对象名:类名整体加下划线表示这是一个实例而不是类。对象名用业务上有意义的短标识别用 obj1、obj2评审的时候没人记得住。属性槽写成属性名 值值是具体数据不是类型。这一点是对象图和类图最本质的区别类图写“票价: 金额”对象图写“票价 680.00”。下面是我这张图里的对象清单用表格展示比贴图更清楚你在工具里照着填就行。对象标识类属性槽节选p1:乘客乘客乘客编号 P-1001姓名 乘客甲常旅客等级 银卡p2:乘客乘客乘客编号 P-1002姓名 乘客乙常旅客等级 普通o1:订单订单订单号 OD20240512001下单时间 2024-05-12 09:13订单状态 已支付总金额 1560.00tk1:客票客票票号 TK-8801舱位等级 Y票价 680.00税费 80.00客票状态 已出票旅客姓名 乘客甲tk2:客票客票票号 TK-8802舱位等级 Y票价 720.00税费 80.00客票状态 已出票旅客姓名 乘客乙f1:航班航班航班号 CA-1520起飞地 甲城降落地 乙城计划起飞时间 2024-05-20 08:30剩余座位数 118st1:座位座位座位号 32A座位类型 靠窗座位状态 已占用st2:座位座位座位号 32B座位类型 中间座位状态 已占用pay1:支付记录支付记录支付流水号 PM20240512001支付金额 1560.00支付渠道 银行卡支付状态 成功支付时间 2024-05-12 09:20这里有个金额计算要交代清楚不然读者会问 1560 是怎么来的。税费我按每张票固定 80 元算机建 50 燃油 30那么 tk1 的含税价是 680 80 760tk2 是 720 80 800合计 760 800 1560与订单总金额、支付金额三者一致。这个对账动作看着琐碎但它是对象图最有价值的地方之一数字对不上说明类图缺属性或者业务规则没建模。后面第5节的排查案例就是从这个对账开始的。剩余座位数写 118 也有依据航班初始可售 120 座本单锁定 2 座快照时刻剩余 118。对象图里的每个数字最好都能推出来推不出来的数字就是隐患。3.3 连链把关联一条条落到实例上链是关联的实例化。类图里有一条关联对象图里就可能出现若干条链。画链的时候我习惯按关联清单逐条过过完打勾避免漏。链端点 A端点 B对应关联角色标注L1p1:乘客o1:订单乘客 1 — 0..* 订单下单人L2p2:乘客tk2:客票乘客 1 — 0..* 客票旅客L3o1:订单tk1:客票订单 1 — 1..* 客票所属订单L4o1:订单tk2:客票订单 1 — 1..* 客票所属订单L5tk1:客票f1:航班客票 * — 1 航班承运航班L6tk2:客票f1:航班客票 * — 1 航班承运航班L7tk1:客票st1:座位客票 1 — 1 座位占用座位L8tk2:客票st2:座位客票 1 — 1 座位占用座位L9o1:订单pay1:支付记录订单 1 — 0..1 支付记录支付凭据这里能看出链和关联的本质差别类图里“订单—客票”只画一条线标上1..*对象图里 o1 伸出两条链分别连 tk1 和 tk2每一条链就是一个具体的事实。链上不需要再标1..*因为这个多重性描述的是“一个订单可以有多少张客票”这种规则已经由类图表达了到了实例层面有几条链就是几条事实本身不需要再标范围。链上标角色名是我强烈建议的习惯。L1 标“下单人”、L2 标“旅客”这两个角色名一标p1 和 p2 的区别立刻清楚了一个是付钱的人一个是坐飞机的人。不标角色名看图的人会以为画重复了。还有一个细节p2 和 o1 之间没有链。这不是漏画而是业务规则决定的——同行旅客不一定是下单人订单归属只认下单人。如果业务要求“每位旅客都能在自己的账户里看到这个订单”那就要么补一条 p2—o1 的链要么在查询逻辑里走“乘客—客票—订单”的路径。这个选择要在设计文档里写清楚不能靠看图的人猜。3.4 校验多重性、金额、状态三道对账图画完不算完得校验。我固定做三道第一道多重性对账。o1 有 2 条客票链满足1..*tk1、tk2 各挂 1 个航班满足*—1里“每张客票一个航班”tk1、tk2 各挂 1 个座位且座位对象不同满足“一座一票”o1 挂 1 条支付记录满足0..1p1 挂 1 个订单满足1—0..*。全部通过。第二道金额对账。客票含税价之和 760 800 1560等于订单总金额等于支付金额。通过。这一步如果不等先别改对象图回头查类图是不是漏了优惠、手续费、退改签差价这些属性。第三道状态对账。订单状态“已支付”、支付记录“成功”、客票状态“已出票”、座位状态“已占用”四个状态在同一时刻是自洽的支付成功触发订单状态变更进而触发出票和占座。如果这里出现“订单已支付但客票未出票”那是正常的中间态但你得确认快照时刻是不是选在了中间态上别把中间态和终态混着画。三道对账做完这张对象图才算能用。整个过程熟练之后确实五分钟能画完但前面类图的功夫省不掉。4. 对象图与邻家图的分工别画串了4.1 对象图 vs 类图一个是“图纸”一个是“成品”这两个图被混淆的概率最高。区分方法特别简单看属性栏。类图写的是类型票价: 金额对象图写的是值票价 680.00。看名字栏类图写客票对象图写tk1:客票并且加下划线。看关系栏类图写关联名和多重性对象图只画链一条链对应一个事实。还有一个更本质的区别类图描述的是所有可能对象图描述的是某一个确定。类图说“订单可以有 1 到多张客票”对象图说“o1 这个订单现在有 tk1 和 tk2 两张客票”。前者是规则后者是证据。用对象图验证类图是我最常用的一招。你按类图实例化一遍如果发现某个业务事实画不出来——比如同行旅客的积分没地方挂、优惠金额没属性存——那就说明类图缺东西。这个思路比盯着类图空想有效得多因为实例化会逼你把每个属性填上具体值填不出来的地方就是设计缺口。4.2 对象图 vs 顺序图截面和过程顺序图讲的是“消息按时间顺序怎么走”横轴是对象纵轴是时间重点在交互过程。对象图讲的是“某一刻这些对象长什么样、怎么连”没有时间轴是一条横截面。两者经常配合使用先用顺序图画“下单—支付—出票”的消息流再从流里挑一个关键节点用对象图把这个节点的状态定格下来。比如支付回调这个节点顺序图能告诉你系统收到了支付成功通知、更新订单、调出票服务对象图则告诉你回调处理完之后订单、客票、支付记录、座位这四个对象各自是什么状态、链有没有变。一个管过程一个管结果谁也不能替代谁。4.3 对象图 vs 状态图群体快照和单体生命周期状态图盯的是一个对象自己的生命周期比如订单从“待支付”到“已支付”再到“已完成”或者“已取消”。对象图盯的是一群对象在某一刻的关系。拿订单举例状态图会画出“待支付—已支付—已出票—已完成”的迁移条件对象图只会告诉你此刻 o1 处在“已支付”这个状态上以及它连着哪些客票和支付记录。有人问那对象图里能不能体现状态迁移不能也不该。你要表达“支付成功后订单从待支付变成已支付”那是状态图或者顺序图的活。对象图只负责定格不负责演绎。把这个边界守住图就不会画得四不像。5. 踩坑实录对象图里最容易翻车的八种情况5.1 错误速查表下面这张表是我在评审里见过、自己也犯过的错误按出现频率排序。对着查一遍能省不少返工。现象根因修正做法对象框写成“订单: o1”对象名和类名位置搞反固定写法o1:订单整体加下划线属性槽写“票价: 金额”直接把类图属性抄过来改成票价 680.00填具体值链上标了1..*把关联的多重性搬到链上链就是一条事实多重性留在类图同一个订单画了两个框以为“被引用两次就要画两次”一个实例一个框多条链连同一个框订单“已支付”、支付记录“待支付”快照时刻不统一状态拼接先定时刻再逐对象核对状态客票没连航班漏链没按关联清单逐条过画链时对着关联表打勾两张客票连到同一个座位对象座位分配逻辑缺唯一约束检查占座规则补数据库唯一索引一张图塞了二十个对象想用一张图讲完整个系统按问题拆图单图控制在十个对象内表格里第八条值得多说两句。对象图的信息密度很高每个对象都带属性槽画多了就是灾难。我的经验是单图不超过十个对象、不超过十五条链超过就拆。拆图的依据不是类是问题这张图回答“支付完成后的状态”那张图回答“退票后的状态”各自独立互不干扰。5.2 三个排查过程都是对象图救的场第一个金额对不上。最初的对象图里两张客票含税价合计 1560订单总金额也写 1560但支付记录金额我随手写了 1500。三道对账一跑就露馅。回头查业务发现这位乘客用了一张 60 元的优惠券。问题来了类图里订单只有“总金额”没有“优惠金额”和“实付金额”的区分。补法有两种简单的是给订单加“优惠金额”属性实付金额 总金额 - 优惠金额规范的是把优惠券独立成类订单和优惠券建立关联这样能追踪券的来源和使用规则。我们选了后者因为业务上还要做优惠券核销和对账。这个缺口靠盯类图是发现不了的是对象图的对账逼出来的。第二个座位重复分配。有一次画另一张快照两张客票的链指向了同一个座位对象座位号都是 32A。第一反应是画错了检查发现不是——是占座服务的并发控制没做好两个请求同时锁到了同一个座位。对象图上一条链指向同一个框视觉上极其刺眼比看代码里的 if 判断容易发现多了。后来补了座位状态的原子更新和唯一约束再画一遍两条链分别指向 st1 和 st2问题闭环。第三个同行旅客查不到行程。这就是前面提过的 p2 没有链的问题。测试提了个缺陷同行旅客登录自己的账户看不到这趟行程。查下来行程查询走的是“乘客—订单”这条关联而 p2 和 o1 之间压根没有链。根因在类图乘客和客票之间缺一条“旅客”关联。补上之后p2 通过p2—tk2—o1就能查到订单。这个案例让我养成一个习惯画对象图时把每个对象的“查询入口”都想一遍谁能查到我、我通过谁被查到想不通的地方就是关联缺失。5.3 工具层面的操作要点工具选择上我分两种场景。日常设计评审、需要进版本库的用文本化的方式描述对象和链好处是能 diff、能 review、改一行就是改一个属性值冲突好解。给业务方讲解、需要直观展示的用图形化工具拖拽对象框、下划线、链都是现成的注意别开自动布局把链绕成一团。不管用哪种有几个操作细节得注意。对象名统一用业务编号前缀p开头乘客、o开头订单、tk开头客票、f开头航班、st开头座位、pay开头支付评审时一眼能对上。属性槽只填跟当前问题相关的别把二十个属性全列上图会撑爆。链的角色名要标尤其是同一个类出现多个实例的时候不标角色名等于没画。最后图上加一行注释写清楚快照时刻和这张图要回答的问题过两周你自己回来看都得靠这行字。6. 我自己的用法把对象图当“设计验算纸”6.1 用对象图反查类图漏洞类图画完别急着评审先挑三个典型场景各画一张对象图正常下单支付、退掉其中一张票、改签到一个新航班。画的过程中你会发现各种填不进去的值、连不上的链。填不进去的值说明类缺属性连不上的链说明类缺关联链的数量违反多重性说明约束没定对。这三类问题在类图阶段都是隐形的一实例化全现形。我现在的习惯是类图评审材料里必须附一张对象图。评审的时候大家对着对象图吵架比对着类图吵架有效率得多因为对象图上的每个值都是具体的没法含糊。有一次评审两位同事为了“订单要不要记录旅客联系方式”争了半小时我把对象图一画发现同行旅客的联系方式无处安放争议当场就有了结论要么客票带联系方式要么乘客实体独立维护二选一别再空谈。6.2 用对象图生成测试数据对象图里的属性槽天然就是一组测试数据。把 p1、p2、o1、tk1、tk2、f1、st1、st2、pay1 这些值直接导成数据库的 insert 语句就是一套可用的集成测试数据集。金额之间的等式关系客票合计 订单总额 支付金额还能直接写成断言。测试同学最喜欢这种图因为不用自己编数据而且编出来的数据是自洽的。要注意脱敏。对象图里填的姓名、证件号、手机号一律用虚构值证件号写成ID-8821****这种带掩码的形式别把真实数据贴进设计文档。这不是小节是底线。6.3 几条实操心得第一条先写一句图注再动笔。这句话的格式是“本图展示某时刻某场景下哪些对象及其关系”写不出来说明你还没想清楚要画什么。第二条属性槽填“像真的”的值。别写“票价 0”或者“姓名 测试”假数据会掩盖问题。你填 680 和 720才会去做加法、才会发现金额对不上你填 0什么问题都发现不了。第三条画完从任意一个对象出发把链走一遍。从 p1 出发能不能走到 f1路径是 p1—o1—tk1—f1。从 pay1 出发能不能走到 p2pay1—o1—tk2—p2。走不通的地方要么是关联缺失要么是业务上本来就不该通两种结论都得写进文档。第四条别在对象图上做推演。看到链就想着“如果这里加个条件会怎样”那是顺序图和状态图的活。对象图是静态的守住这个定位图才干净。第五条对象图会过期。业务规则一改类图变了旧对象图就成了错误示范。我的做法是把对象图跟类图放在同一个版本库里类图改动时同步检查受影响的快照图改不动就删掉别留着一张过期的图误导后来的人。这一点比画图技巧重要得多毕竟一张画错的图比没有图危害更大。