简介:UML系统分析与设计复习试题是面向软件工程、计算机相关专业学生及自学者的备考资料,覆盖关联多重度、类图关系、交互图对比、高内聚度、四种UML关系等核心考点,可用于课程期末复习、考研复试或软考准备。压缩包共1个doc文档,大小132KB,内容精炼、便于离线浏览与打印。目前已有405人学习使用,具有较好的参考价值。文档以选择题、简答题和设计题形式组织,涉及客户与订单的数量约束、顺序图与协作图的优缺点、概念类联系绘制、用例图设计、对象间可见性、领域模型等具体问题,并附参考答案与解析;其中还对顺序图与协作图、类图与用例图等易混淆知识点作了辨析,并涵盖GRASP模式、FURPS需求分类等内容,能帮助读者系统梳理知识框架、熟悉题型与解题思路,提升UML建模与应试能力。
1. UML 复习题里最容易被当成“常识题”的是关联多重度
UML 复习题里最容易被当成“常识题”的是关联多重度:问“一个类的实例能够与另一个类的多个实例相关联”,答案给的是 B。但真正把它放进客户和订单的例子里,能一次把方向、数字、生命周期全部答对的人并不多。多重度不是“关系的数量描述”,而是 UML 类图上最底层的引用约束机制,决定了对象之间允许有多少条连接,也决定了组合、聚合、依赖在建模和代码层面的不同形态。搞懂多重度,才能把类图从“画方块连线”提升到“表达可验证的领域约束”。
2. 多重度:类图上的数字不是装饰,是对象的引用配额
2.1 从“一对多”到“0..*”:多重度到底约束了什么
先明确一个概念:多重度不是方法调用次数,也不是属性共享数量。UML 里,关联两端各自标注的多重度,表达的是“这一端的一个实例,可以对应另一端多少个实例”。试题里那道客户-订单题考得最典型:业务陈述是“一个客户提交 0 个或多个订单,一个订单由一个且仅由一个客户提交”。于是对应每个“订单”实例,存在 1 个“客户”实例;对应每个“客户”实例,存在 0 个或多个“订单”实例。第一空选 B,第二空选 D。
这里的模糊点恰恰是“对应”这个词。你以为是左右呼应,实际上每个空都要站在一个类的实例出发,向另一个类看去。订单这边往客户看,是 1;客户那边往订单看,是 0..。如果把方向反过来读,订单端写 1..,客户端写 0..*,整个模型表达的边界条件就变了:它允许一个订单属于多个客户,这在零售场景里是灾难。
多重度的设计动机不是让图变得“规范好看”,而是给领域规则一个可检查的量化边界。数据库设计里,它对应外键本身的非空约束与子表数量的上限;对象设计里,它决定了一个字段是单个引用还是集合。试题反复考这一点,说明出题者想确认的并不只是记忆,而是你能不能从自然语言描述中抽取出实例层面的数量关系。
2.2 组合与聚合:生命周期绑定的两种强度
第 2 题里,类 A 由类 B 的一个实例和类 C 的 1 个或多个实例构成,要求画类图。这里的关键词是“构成”。这个题考查的是组合关系:整体 A 拥有部分 B 和 C,部分不能脱离整体单独存在。画法是 A 端用实心菱形,B 端标 1,C 端标 1..*。
组合与聚合是 UML 里最容易被画错的一对:聚合是空心菱形,语义是“整体与部分的松散拥有”,例如公司聚合员工,员工离职后还能存在;组合是实心菱形,语义是“整体强拥有部分”,部分的生命周期被整体托管,例如订单项离开订单就没有独立的业务意义。判断依据就一条:整体消亡时,部分是否跟着消亡、是否没有其他整体同时拥有它。是,组合;不是,聚合。
从实现上看,组合往往表现为整体在构造时创建部分,而不是从外部注入部分。很多人在代码评审里纠结“这个类里 new 出来的对象算不算组合”,答案是把生命周期问题转成所有权问题:谁负责创建,谁负责销毁,谁对外不可见,谁拥有不可替代性。多个对象共享同一个“部分”引用时,组合语义就很难成立。
2.3 从类图到实现:多重度如何映射成字段与约束
// 客户-订单:客户端多重度 0..*,订单端多重度 1 public class Customer { private List<Order> orders = new ArrayList<>(); // 对应 0..* } public class Order { private Customer customer; // 对应 1:不允许为 null public Order(Customer customer) { this.customer = customer; } }这段代码把多重度翻译成了字段类型:多值端用集合承载,单值端用对象引用承载。注意构造函数强制要求传入 customer,这就是在实现层面保证订单端至少有一个客户,相当于把模型上的“1”变成了不变量。如果没有这个强制,UML 图上标着 1,代码里却允许摘除引用,模型与实现就脱节了。
数据库里对应关系更直观,用 SQL 更能说明“1”和“0..*”的落地方式:
CREATE TABLE customer ( id BIGINT PRIMARY KEY ); CREATE TABLE orders ( id BIGINT PRIMARY KEY, customer_id BIGINT NOT NULL, CONSTRAINT fk_orders_customer FOREIGN KEY (customer_id) REFERENCES customer(id) );orders.customer_id 上的 NOT NULL 约束,就是 UML 订单端“1”的数据库形态;customer_id 外键本身是订单对客户的引用。数据库不存储客户到订单的集合,查询时通过外键反查,这就是 0..* 在关系模型里的自然形态。理解这一层之后,你再回看类图上的数字,就能明白它是在约束“合法实例集合”的形状,而不是装饰。
3. 顺序图和协作图:同一段交互的两种投影
3.1 时间顺序 vs 空间组织:交互的两种坐标
第 4 题和第 72 题反复在问顺序图与协作图的区别。核心答案是一句话:它们描述的是同一组对象交互,但投影不同。顺序图把时间放在纵轴,对象生命线水平排开,消息按发生先后从上往下画,像示波器里展开的事件序列;协作图把对象和连接布局在二维平面上,消息顺序退居为编号,像电路图上标的信号时序号。
这个差异不是风格选择,而是表达能力的取舍。顺序图的水平方向被生命线占据,对象一多图就横向膨胀;但它的垂直时间轴让消息顺序一目了然,任何读者都能直接读到“先做什么后做什么”。协作图把对象放在空间位置上,新增对象时只需在空白处加一个节点再接一条线,水平空间压力小;但消息顺序全部压在编号上,消息一多,编号深度变得难以追踪。试题里那句“协作图在增加对象时比较容易,而且分支也比较少,但如果消息比较多时难以表示消息之间的顺序”,说的正是这种取舍。
我一般这么理解:表达事件流、系统操作、调试场景,优先顺序图;表达对象协作的整体语境、辅助设计类分配职责、探讨对象间连接是否合理,优先协作图。两种图可以互相转换,UML 工具里一键切换也是常见功能,但转换无损不代表两种视角等价——一个偏时域,一个偏空域,读图的心智负担不同。
3.2 消息编号:协作图如何把时间压缩进空间
协作图能摆脱时间轴依赖,靠的是消息编号协议。这个协议本质上是一个调用树编码:顶层消息按 1、2、3 编号,每个消息内部的嵌套调用用小数点扩展,比如 1.1、1.2,其中 1.1 是 1 的执行过程中触发的子消息。返回消息一般也编号,但不参与顶层顺序的计数。
以销售场景为例,协作图上可以看见:
| 编号 | 消息 | 层级 |
|---|---|---|
| 1 | makeNewSale() | 顶层 |
| 1.1 | create() | 1 的子消息 |
| 2 | enterItem(itemID, quantity) | 顶层 |
| 2.1 | addLineItem(itemID, quantity) | 2 的子消息 |
| 3 | endSale() | 顶层 |
| 3.1 | getTotal() | 3 的子消息 |
这个表格展示了协作图还原时序的方式:第一列决定阶段顺序,小数点深度决定嵌套层次。读图时先看顶层序号,确认大流程分几步,再看每个顶层序号下挂了多少个子消息,子消息之间又按 1.1、1.2 的顺序排列。写图时最容易犯的错误是把嵌套消息平铺成独立编号,比如把 1.1 写成 2,这会彻底破坏时序含义——本来发生在 makeNewSale 内部的创建动作,被误读成发生在 makeNewSale 之后。
协作图重视空间布局、允许对象被多个交互复用,这套机制适合早期设计阶段快速检查对象之间是否存在多余连接。它牺牲的直观顺序,由编号协议补偿。所以复习时比“谁会画”更重要的是“谁读得懂编号背后的调用树”。
3.3 SSD 与领域模型:黑盒契约和概念类的分界
系统顺序图(SSD)是试题里另一个高频考点,第 49、51 题都在考它。SSD 与一般顺序图的区别非常明确:SSD 把整个系统当成一个黑盒,图上只有参与者、系统边界和系统事件,不出现任何内部对象和内部消息。试题第 51 题的正确答案是 D“不对系统的内部结构与功能进行描述”,这句话很多初学者觉得“怎么可能”,但这正是 SSD 的设计意图。
SSD 回答的问题是“外部参与者以什么顺序触发了系统的哪些操作”,而不是“系统内部怎么响应”。它服务于系统操作契约:先定义系统对每个事件的黑盒行为,再在设计阶段把系统生命线展开成内部协作。以 POS 场景为例,SSD 上只画:
收款员 -> 系统: makeNewSale() 收款员 -> 系统: enterItem(itemID, quantity) 收款员 -> 系统: endSale() 收款员 -> 系统: makePayment(amount)这四条消息就是系统的公共接口。接下来设计阶段的工作是把“系统”这条生命线拆成注册、收藏、销售等多个对象之间的协作。为什么要先黑盒再白盒?因为需求阶段过早暴露内部结构,会让利益相关方把注意力放在实现细节上,而领域模型此时应该只反映真实世界的概念类,比如顾客、收款员、销售、商品,而不是软件部件。先定契约、后谈实现,这条顺序在 UP 里贯穿始终。
4. GRASP 与高内聚:职责分配不是审美,是复杂度控制
4.1 信息专家:为什么“谁持有数据谁干活”是默认答案
GRASP 五模式里,信息专家解决“分配职责给对象的基本原则是什么”,答案是:把职责分配给拥有完成该职责所必需的信息的对象。听上去像一句正确的废话,但把它落到 POS 例子上就会发现,这个原则直接决定了类之间会不会产生多余的绕路。
假设“计算销售总额”这个职责,候选对象有收款员、POS 系统、销售对象。谁拥有计算所需的信息?销售对象拥有商品明细列表,每行明细又有各自的小计。让销售对象自己计算总额,职责执行者和数据源在同一个类里,不需要向其他对象索要数据。写成代码:
public class Sale { private List<LineItem> lineItems; public Money getTotal() { return lineItems.stream() .map(LineItem::getSubtotal) .reduce(Money.ZERO, Money::add); } }getTotal 读取的是自己类内部的 lineItems,这就是信息专家的机制本质:信息局部性。如果反过来让收款员或外部服务来算总额,收款员就必须先向销售对象要 lineItems,再逐条访问商品价格,协作路径变长,任何一端的数据形态变化都会牵动多个类。信息专家之所以是 GRASP 里的默认答案,是因为它尽可能把职责压缩进最小的信息范围内,从根源上降低耦合。
这里要注意“信息”不一定是对象自己存的字段,也可以是它能够通过已有关联间接获得的信息。如果对象能通过已有连接在几步之内拿到数据,它仍然算信息专家。只有当数据源完全无关时,才需要引入纯虚构类或中间者来解耦。
4.2 创建者、控制器与低耦合:把路由和出生分开
创建者模式解决“谁负责创建某个类的新的实例”,原则是:如果某个对象聚集了需要传给新实例的数据,或者与待创建对象有聚合关系,就让那个对象负责创建。控制器模式解决“谁来负责处理一个输入系统事件”,原则是:把系统事件的接收职责交给一个代表系统或代表用况的控制器对象,而不是放任每个外部事件直接操作内部任意对象。
这两个模式很容易被误解为“让一个类包办一切”。初学者常常把所有事件都丢给控制器:makeNewSale 让控制器建 Sale,enterItem 让控制器去改 Sale 的商品明细,endSale 也由控制器算总价。结果控制器涨成一个上帝类,Sale 反而退化成数据结构。正确的分工是:控制器只负责接收外部事件和路由,业务职责按信息专家原则下放,创建职责按创建者原则就近分配。路由、业务、创建是三种不同性质的职责,混在一起就是违背高内聚。
低耦合模式是对这种混用的制衡。它不要求类之间完全没有关联,而是要求关联尽量少、尽量稳定。信息专家跑偏的典型症状就是“为了低耦合,把所有计算搬到外部工具类”,结果是耦合低了一个类,却损失了信息局部性,全局的修改点反而更多。低耦合和高内聚之间需要权衡,不是越低越好。
4.3 高内聚的度量:什么叫“相关职责集中”
试题第 6、83 题对高内聚的定义:一个类中的各个职责之间相关程度和集中程度的度量。这个定义值得拆开看。“相关”是质量维度,不是数量维度。两个职责如果能共享同一批字段、或在同一次事件里被顺序触发,它们就是相关的;一个管打印报表、一个管连接数据库的方法,即使都写在同一个类里,也不代表高内聚。
我判断一个类是否高内聚的实操标准是:你能不能用一个短句说清这个类负责什么。能说清,类名就是那个短句的主语;说不清,甚至要用“以及”连缀两个主题,这个类就大概率需要拆分。高内聚还包含另一层意思:不要把一个类的职责分派到能胜任的其它类之外。试题里那句“不要给一个类分派太多的职责,在履行职责时尽量将部分职责分派给有能力完成的其它类去完成”,可以翻译成职责转移的门槛:不是“别的类会做就分出去”,而是“别的类拥有更相关的信息才分出去”。
策略模式在这里是个典型的正面案例。POS 系统里按计价规则计算单价,规则会频繁变更,解决方式是定义一组算法接口,每个规则封装成一个策略类,让销售对象在运行时选择具体策略:
public interface PricingStrategy { Money calculate(LineItem item); } public class QuantityDiscountStrategy implements PricingStrategy { @Override public Money calculate(LineItem item) { // 数量折扣规则 return item.getSubtotal().applyDiscount(0.9); } }策略类各自内聚一种规则,销售类只依赖 PricingStrategy 接口,规则变更不再波及核心流程。这正是高内聚与低耦合协同作用的地方:策略模式没有消除算法变化,但把变化隔离在了稳定的接口之后。
5. UML 复习常见问题排查:五个翻车现场和它们的原理
5.1 多重度方向读反:一个订单为什么不能属于多个客户
现象:画客户-订单类图,两端都标 0..*,理由是“一个客户有很多订单,一个订单也可以有很多客户”。
原因:没有站在实例方向逐端分析,而是把业务对象的多重度当成了数据库表之间的多对多关系。业务陈述里“一个订单由一个且仅由一个客户提交”已经把订单端锁定为 1,这个约束来自领域规则,不是建模者自由选择的。
解决:标注前强制写两句话:“从客户的一个实例出发,看它对应几个订单实例;从订单的一个实例出发,看它对应几个客户实例”,再分别落两端。顺序图场景检验同样有效:如果允许一个订单属于多个客户,那么订单的归属方不明,账务和配送都无法收敛,这从常识上就能排除 0..。试题的标准答案是客户端 0..、订单端 1,方向一错整套图的意义就变了。
5.2 组合聚合不分:空心菱形实心菱形差的是一个生命周期
现象:“公司由部门构成”和“订单由订单项构成”都画成空心菱形。
原因:只看到“由...构成”就画组合,没判断整体消亡时部分是否随之消亡。部门在公司注销后作为行政概念还会存在,员工可以流转,所以是聚合;订单项被订单整体拥有,订单作废时订单项没有任何独立业务意义,是组合。
解决:用生命周期问题测试:整体销毁后,部分还有没有独立的业务价值?没有,实心组合;有,空心聚合。落到代码上也可以辅助判断:组合里整体负责创建部分,部分不对外暴露可复用的引用;聚合里部分由外部注入,整体只负责组装。试题里“类 A 由类 B 的一个实例和类 C 的 1 个或多个实例构成”,应画实心菱形,B 多重度 1,C 多重度 1..*。
提示:实心菱形标在整体端,即拥有部分的那一端。
5.3 协作图消息编号平铺:时间顺序要靠调用树还原
现象:协作图上消息标成 1、2、3、4、5 五个平铺序号,看起来像把顺序图压扁了。
原因:把协作图理解成“没有时间轴的顺序图”,忽略嵌套调用的层级。平铺编号无法表达一次消息内部又触发哪些子消息,读图的人只能知道消息先后,不知道谁调用了谁。
解决:画之前先画调用树。makeNewSale 是顶层 1,它内部创建 Sale 是 1.1;enterItem 是顶层 2,它内部创建 LineItem 是 2.1;每个顶层消息重新从整数开始,嵌套用小数点。检查技巧是:从 1 跳到 2 之前,1 下面挂的所有子消息必须全部结束。反过来,读到 1.3 时如果没有 1.1、1.2 的铺垫,要么编号缺层,要么出现了跨层跳号,这种图信息是丢失的。
5.4 用例图画错参与者:把系统内部模块当人用
现象:在用例图里画“数据库”“登录验证模块”作为参与者,跟“电话用户”并列。
原因:混淆了外部角色和系统内部部件。参与者定义是“与系统交互的外部角色”,它是用况的发起者或受益者;数据库和验证模块是对系统责任的内部分担者,不对用况负责。
解决:先画系统边界,再列出边界外访问系统的角色。电话用户是参与者,“使用电话卡”和“对方付款”是用例;数据库不是参与者,它只是用例实现阶段的构件。如果确实需要外部系统协作,也把整个外部系统当一个参与者,而不是拆出内部模块参与编排。
5.5 SSD 里画内部对象:黑盒契约画成白盒实现
现象:SSD 上出现“系统 -> Sale: create()”“系统 -> 收款台: display()”这类内部消息。
原因:把 SSD 和一般顺序图混用。SSD 是系统操作契约的黑盒视图,用来记录参与者向系统发起的系统事件,而不是记录系统内部如何分工。
解决:写 SSD 时立一个规矩:系统只有一条生命线,参与者只有外部角色,消息只能是参与者发给系统的系统事件,内部对象一个都不出现。画完做一次白盒检测:如果发现任何两个内部对象发生消息,就把它删掉,挪到协作图或交互图阶段。这个克制不是画图技术问题,而是分析和设计两个阶段的边界问题——需求阶段过早展开内部结构,会迫使利益相关方讨论一种还没设计的实现。
6. 设计取舍:UP 的迭代节奏和用例驱动到底解决了什么
6.1 四个阶段不是瀑布,是风险驱动的检查点
UP 四阶段——初始、细化、构造、提交——常被误解为瀑布的另一个名字。但从各阶段的任务分配看,它实际是风险驱动的检查点:初始阶段只回答“项目值不值得做”,做愿景文档、粗略成本评估;细化阶段集中解决高风险问题,迭代地实现核心架构,定义大多数需求和范围;构造阶段迭代实现剩余功能;提交阶段做 beta 测试和部署。需求定义放在细化阶段而非初始阶段,正是承认需求会随迭代反馈逐步清晰,而不是在源头一次定死。
6.2 用例驱动、增量开发与变更控制
用例驱动把需求组织成“参与者用系统完成业务过程”的事件顺序,先从外部描述系统必须做什么,再指导内部设计。这和 SSD 的定位一脉相承:契约先行,实现后置。增量开发进一步把复杂用况拆成多个迭代,按优先级在不同迭代里实现,反馈周期被压缩,错误在早期暴露。迭代、用例、UML、变更控制这四件事在 UP 里互相咬合,没有哪个是多余的。
6.3 我的习惯
我从一次把多重度方向读反的翻车里养成一个习惯:做任何 UML 题,先不看图也不看选项,而是把题面里的业务陈述改写成“一个 X 对应多少个 Y”的句式,逐条验证方向,再落图。这套流程放在复习《UML系统分析与设计复习试题》这类题集时特别有用,它让每道题都变成一次约束提取练习,而不是答案记忆。从那以后我每次画类图都强制走一遍这个句式,速度和准确率反而都上去了。希望帮到你。
本文还有配套的精品资源,点击获取