简介:本资源是面向软件工程专业学生及UML初学者的系统分析与设计复习备考资料,聚焦UML核心概念的理解与应用能力提升。内容涵盖关联多重度、组合关系、客户-订单业务建模、顺序图与协作图对比、高内聚设计原则、UML九种图的定位与选用(如类图表静态结构、用例图述系统边界、序列图显时序交互)、可见性、领域模型、GRASP设计模式等12个高频考点,每题均附详细解析与图示说明,助力夯实基础、突破易错点。资源为单文件Word文档(.doc),共1个文件,大小132KB,排版清晰、图文结合,便于打印复习或碎片化学习。已有404人下载学习,适合作为期末冲刺、软考中级系统集成项目管理工程师备考及UML课程课后巩固的精炼型习题集。
1. UML 不是画图软件,而是系统设计的“通用手语”:为什么你翻来覆去背了几十道题,还是画不出一张能被开发同事一眼看懂的类图?
你有没有过这种经历:花半小时画完一张“完美”的UML类图,兴冲冲发给后端同学,对方盯着看了两分钟,回一句:“这个‘订单’和‘客户’之间……到底是谁删了谁?删的时候另一方还留不留记录?”——你愣住,翻出教材第37页,指着“1..*”说:“这不就是一对多吗?”他摇摇头:“可代码里客户删了,订单表外键设的是ON DELETE CASCADE,还是SET NULL?图上没写啊。”
那一刻你突然意识到:UML不是贴在墙上的装饰画,它是一套有语法、有语义、有上下文约束的工程语言。它不负责写代码,但必须让所有角色——分析师、设计师、开发者、测试员——对同一个箭头、同一个数字、同一个虚线,达成毫无歧义的共识。所谓“复习试题”,表面考的是“多重度选哪个”,实际考的是:你能不能把业务里那句“一个客户能下多个订单,但一个订单只能属于一个客户”,精准翻译成图形符号+文字契约+隐含约束的完整表达?这份《UML系统分析与设计复习试题详解》,不是让你死记硬背ABCD,而是帮你把散落的图元、关系、规则,拧成一条能真正指导开发的逻辑链。它解决的不是“会不会画”,而是“画出来别人敢不敢按它写代码”。
2. 关联的多重度:不是数学题,而是业务规则的“数量契约”
多重度(Multiplicity)常被初学者当成“数数游戏”:左边写个1,右边写个*,就叫“一对多”。但真实项目里,它根本不是填空题,而是一份带法律效力的业务承诺书。它回答的从来不是“最多能有几个”,而是“系统必须保证什么”“违反时该怎么处理”。
2.1 多重度的本质:从“能有多少”到“必须怎样”
看这道题:
“一个客户提交0个或多个订单;一个订单由一个且仅由一个客户提交。”
对应每个“订单”实例,存在___(46)“客户”实例;对应每个“客户”实例,存在(47)___个“订单”实例。
标准答案是:(46) B.1个,(47) D.0个或多个。
但如果你只停在这里,就错过了最关键的工程含义。
- (46) 填“1个”,意味着系统在创建任何“订单”对象时,必须强制绑定一个且仅一个“客户”ID。数据库里订单表的
customer_id字段必须设为NOT NULL,且外键约束指向客户表;代码里new Order().setCustomer(null)必须抛异常;前端提交订单时,客户选择框绝不能是可清空的。这不是“建议”,是架构级硬性要求。 - (47) 填“0个或多个”,则暗示客户可以“零订单存在”。系统必须允许客户注册后长期不购物;查询客户详情时,“订单列表”字段返回空数组而非报错;删除客户时,若其名下有订单,需明确策略(级联删除?设为空?抛业务异常?)。这个“0..*”背后,是数据一致性、事务边界、异常处理路径的完整设计。
提示:多重度数字旁的“0..”“1..”“1”等符号,本质是运行时校验规则的图形化声明。它直接驱动数据库DDL、API入参校验、领域模型构造函数的参数检查。
2.2 多重度的常见陷阱:为什么“1对1”比“1对多”更难实现?
很多同学觉得“1对1”最简单——不就是两个类各加个对方ID字段吗?但现实中的“1对1”往往藏着魔鬼细节。例如题干中“类A由类B的一个实类和类C的1个或多个实类构成”,这里A与B是“1对1”,但这个“1”是强所有权关系(组合),而非普通关联。
- 普通1对1关联(如“用户”和“用户档案”):两张表各自独立,通过外键连接。删用户,档案表记录可保留(软删除)或级联删。
- 组合式1对1(如“订单”和“订单支付信息”):支付信息生命周期完全依附于订单。订单删,支付信息必须同步删;订单创建时,支付信息必须同时初始化(哪怕暂存空值)。UML中要用实心菱形+1表示,代码里常体现为
Order类内嵌PaymentInfo对象,而非仅持ID引用。
# 普通关联:用ID解耦,生命周期独立 class User: def __init__(self, id, name): self.id = id self.name = name class UserProfile: def __init__(self, id, user_id, bio): # 仅存user_id self.id = id self.user_id = user_id # 可为空,可独立存在 self.bio = bio # 组合式1对1:对象内嵌,生命周期绑定 class Order: def __init__(self, id, customer_id): self.id = id self.customer_id = customer_id self.payment_info = PaymentInfo() # 创建即初始化,非None class PaymentInfo: def __init__(self): self.amount = 0.0 self.status = "PENDING"这段代码差异,正是UML中空心菱形(关联)与实心菱形(组合)的落地体现。复习题里那个“类A由类B的一个实类构成”,考的不是符号记忆,而是你能否判断:此处的“1”是否意味着B的实例必须随A一起诞生、一同消亡?
2.3 多重度与数据库设计的映射:别让ER图和UML图互相打架
UML类图的多重度,必须能无损翻译成数据库表结构。这是检验你是否真懂的关键试金石。以“客户-订单”为例:
| UML多重度 | 数据库实现方式 | 违反后果举例 |
|---|---|---|
| 客户:1 → 订单:0..* | 订单表含customer_id外键,NOT NULL | 插入订单时customer_id=NULL,数据库报错IntegrityError |
| 客户:0..1 → 订单:* | 客户表id为主键,订单表customer_id为可空外键 | 删除客户后,订单customer_id变NULL,但业务要求“订单必须归属有效客户”→ 数据不一致 |
| 客户:1 → 订单:1..* | 同上,但订单表增加CHECK (customer_id IS NOT NULL) | 应用层未校验,直接INSERT空值,DB层拦截 |
注意:UML中“0..1”(零或一个)在数据库中常对应可空外键,但业务上可能要求“一旦有关联,就不能为NULL”。此时需在应用层加校验,或用数据库
CHECK约束。复习题里所有多重度选项,本质上都在考你:这个数字,最终会变成哪一行SQL约束?
3. 顺序图 vs 协作图:不是两种画法,而是两种“思考视角”的切换开关
UML交互图常被简化为“顺序图是竖着画,协作图是横着画”。但这样理解,就像说“开车是踩油门,骑车是蹬踏板”——忽略了背后完全不同的控制逻辑。顺序图和协作图,本质是同一组对象交互行为的两种正交切面:一个锁定时间轴,一个锁定空间结构。选错图,等于用错误坐标系描述运动。
3.1 顺序图:时间轴上的“事件流剧本”
顺序图(Sequence Diagram)的核心是生命线(Lifeline)和激活条(Activation Bar)。它强制你回答:
- 这个消息在第几毫秒发出?
- 接收方处理耗时多久?是否阻塞后续?
- 并发请求如何交错?
看题干第21题的SSD图(系统序列图):
- 收款员启动销售
makeNewSale() - 输入商品标识
enterItem(itemID,quantity) - 结束销售
endSale() - 顾客付款
makePayment(amount)
画成顺序图,必须明确:
makeNewSale()返回后,enterItem()才能调用(同步调用);enterItem()可能被多次调用(循环),每次返回商品信息后,界面刷新;endSale()必须等所有enterItem()完成才执行;makePayment()在endSale()成功后触发,且支付成功前,订单状态不能变更为“已完成”。
sequenceDiagram participant 收款员 participant 系统 收款员->>系统: makeNewSale() 系统-->>收款员: SaleCreated(id) loop 输入商品 收款员->>系统: enterItem(itemID, quantity) 系统-->>收款员: ItemAdded(name, price, total) end 收款员->>系统: endSale() 系统-->>收款员: SaleSummary(totalAmount) 收款员->>系统: makePayment(amount) 系统-->>收款员: PaymentConfirmed(orderID)这段伪代码式顺序图,暴露了关键设计决策:enterItem()是同步阻塞调用(否则无法实时显示总价),而makePayment()需要等待endSale()的完整结果(订单号、总金额)。如果考试让你画图,漏掉激活条的起止位置,就等于没定义方法的执行边界——开发同学会困惑:“这个方法是立刻返回,还是等数据库写完才返?”
3.2 协作图:空间结构里的“责任网络拓扑”
协作图(Collaboration Diagram,现称通信图)则彻底放弃时间轴,转而聚焦:
- 这些对象在内存里如何连接?
- 消息沿着哪条引用链传递?
- 哪个对象是“中间人”,负责协调其他对象?
仍以POS系统为例。协作图中,你会看到:
Cashier(收款员)持有Sale(销售)的引用;Sale持有ItemList(商品列表)和Payment(支付)的引用;ItemList持有多个LineItem(明细项);Payment持有CreditCard或Cash等具体支付方式。
消息编号1.1,1.2,2.1等,不是时间戳,而是调用栈深度标记:
1.x表示Cashier发起的顶层调用;2.x表示Sale内部调用ItemList的方法;3.x表示ItemList调用LineItem的方法。
这种编号,直指GRASP模式中的控制器(Controller)和信息专家(Information Expert)原则:谁该响应enterItem()?是Cashier(用户入口)还是Sale(业务实体)?UML协作图用连接线和编号告诉你:Cashier作为控制器接收输入,再委托给Sale处理,Sale才是商品管理的信息专家。这避免了“所有逻辑堆在UI层”的经典反模式。
3.3 什么时候必须用顺序图?什么时候协作图不可替代?
| 场景 | 必须用顺序图 | 必须用协作图 |
|---|---|---|
| 需要精确控制时序 | 支付超时自动取消订单(startTimer()→checkTimeout()→cancelOrder()) | ❌ 时间关系模糊 |
| 验证并发安全 | 多个收款员同时操作同一订单,addDiscount()和removeItem()的锁竞争顺序 | ❌ 无法表达竞态条件 |
| 定位性能瓶颈 | endSale()耗时800ms,需拆解calculateTax()、validateInventory()、generateReceipt()各占多少 | ❌ 无时间维度 |
| 重构对象职责 | 发现Sale类承担太多,需将库存校验抽离为InventoryService,协作图清晰显示新引用关系 | ❌ 无法体现对象间依赖变更 |
| 设计微服务边界 | PaymentService调用NotificationService发短信,协作图用跨服务虚线连接,标注HTTP POST /notify | ❌ 顺序图会混淆服务内/服务间调用 |
提示:考试中若题目要求“分析两者区别”,答“顺序图强调时间,协作图强调结构”只是及格线。高分答案必须指出:顺序图用于验证流程正确性(Correctness),协作图用于验证职责合理性(Responsibility)。前者问“事情是否按对的顺序发生”,后者问“事情是否由对的对象来做”。
4. 易混概念辨析:那些让你在ABCD间反复横跳的“幽灵陷阱”
UML复习题里,最易失分的不是冷门知识点,而是几个名字相似、符号相近、但工程含义天差地别的概念。它们像幽灵一样飘在选项里,稍不注意就掉坑。下面这5组,是某高校近3年UML考试中错误率超65%的“高频刺客”。
4.1 聚集(Aggregation)vs 组合(Composition):都是“整体-部分”,但一个能离婚,一个得殉葬
- 易混点:都用菱形+连线表示,都描述“整体包含部分”,题干常写“类A由类B和类C构成”,学生直接选“聚集”。
- 为什么混:教材常简化为“空心菱形=聚集,实心菱形=组合”,却没说清生命周期绑定强度这个本质区别。
- 怎么区分:
- 聚集:部分可独立存在,脱离整体仍有意义。如“汽车”和“司机”——司机不隶属于某辆汽车,可开不同车;删汽车,司机不消失。UML中空心菱形,代码里用
List<Driver>或DriverId引用。 - 组合:部分生命周期完全依附整体,整体销毁,部分必须销毁。如“订单”和“订单明细”——明细行不能脱离订单存在;删订单,明细行必须级联删除。UML中实心菱形,代码里
Order类内嵌List<OrderItem>对象,而非ID。
实战口诀:“能单飞的是聚集,必陪葬的是组合”。题干中“类A由类B的一个实类构成”,关键词“由...构成”暗示强拥有关系,应选组合(实心菱形)。
- 聚集:部分可独立存在,脱离整体仍有意义。如“汽车”和“司机”——司机不隶属于某辆汽车,可开不同车;删汽车,司机不消失。UML中空心菱形,代码里用
4.2 依赖(Dependency)vs 关联(Association):一个临时搭车,一个长期同居
- 易混点:都用虚线箭头(依赖)或实线(关联),题干图示常只画箭头,学生凭感觉选。
- 为什么混:混淆了“使用”和“持有”的层次。依赖是瞬时的、方法级的;关联是持久的、属性级的。
- 怎么区分:
- 依赖:A类的方法里,临时用到B类的实例,但A类不保存B的引用。如
ReportGenerator.generatePDF(reportData)中,reportData是参数,方法结束即丢弃。UML中虚线+开放箭头,标<<use>>。 - 关联:A类定义了B类型的属性,长期持有引用。如
Student类有List<Course>属性。UML中实线,可标多重度。
血泪经验:看到图中箭头旁写
<<use>>或<<create>>,闭眼选依赖;看到线上标1或0..*,闭眼选关联。 - 依赖:A类的方法里,临时用到B类的实例,但A类不保存B的引用。如
4.3 泛化(Generalization)vs 实现(Realization):一个抄作业,一个签合同
- 易混点:都用空心三角箭头,都表示“is-a”关系,学生看到“类A是类B的一种”,就选泛化。
- 为什么混:没抓住抽象层级差异。泛化是类与类之间,实现是类与接口之间。
- 怎么区分:
- 泛化:子类继承父类的属性和方法,可重写。如
Car泛化自Vehicle,Car有engineType属性,start()方法。UML中实线+空心三角。 - 实现:类承诺提供接口定义的所有方法,但不继承属性。如
CreditCardPayment实现Payment接口,必须实现process()方法,但无Payment的属性。UML中虚线+空心三角,标<<interface>>。
玄学判断法:把箭头换成中文——“Car is a Vehicle”(泛化)✓,“CreditCardPayment is a Payment”(错!应是“implements Payment”)✗。
- 泛化:子类继承父类的属性和方法,可重写。如
4.4 用例图(Use Case Diagram)vs 领域模型(Domain Model):一个画“谁要什么”,一个画“世界有什么”
- 易混点:都出现“客户”“订单”等名词,题干问“描述系统与外部交互”,学生看到“客户”就画类图。
- 为什么混:混淆了需求层和设计层。用例图捕获用户目标,领域模型刻画问题域本质。
- 怎么区分:
- 用例图:只有Actor(小人图标)和椭圆(用例),连线表示“谁触发什么功能”。如“客户”Actor连向“下订单”用例。绝不出现类、属性、方法。
- 领域模型:只有类(矩形)和关联线,标注多重度。如
Customer类连Order类,标1和0..*。绝不出现Actor、用例、系统边界框。
翻车现场:题干“为电话用户画用例图”,有学生画出
PhoneUser类、Call类、PhoneNumber属性——这是领域模型,0分。
4.5 状态图(State Diagram)vs 活动图(Activity Diagram):一个盯对象,一个盯任务
- 易混点:都用圆角矩形(状态/活动)、菱形(判断)、箭头(流转),学生看到“订单状态变化”就画状态图。
- 为什么混:没分清主体。状态图描述单个对象的状态机,活动图描述业务流程的控制流。
- 怎么区分:
- 状态图:聚焦一个对象(如
Order),节点是状态(Created、Paid、Shipped),边是事件触发(paymentReceived、packageShipped)。 - 活动图:聚焦一个流程(如“处理退货”),节点是动作(
VerifyReturn、RefundMoney、UpdateInventory),边是完成条件([success]、[failed])。
关键提示:状态图里有
entry/exit动作(进入/离开状态时执行),活动图里有fork/join(并行分支)。题干若说“描述订单生命周期”,选状态图;若说“描述退货审批流程”,选活动图。 - 状态图:聚焦一个对象(如
5. GRASP原则与设计模式:不是玄学口诀,而是你每天写的每一行代码的“宪法”
复习题里频繁出现GRASP(通用职责分配原则)和GoF设计模式,很多人当教条背:“信息专家=找最熟的,单例=全局唯一”。但真实开发中,它们是你面对一个新需求时,第一反应调用的思维框架。比如接到需求:“支付方式要支持微信、支付宝、银行卡,未来可能加数字货币,且不同支付渠道手续费不同”。这时,你脑子里闪过的不该是“用策略模式”,而是GRASP的多态(Polymorphism)和低耦合(Low Coupling)原则。
5.1 GRASP:五条铁律,决定你的代码是“乐高”还是“水泥块”
GRASP不是设计模式,而是分配职责的底层逻辑。它回答:当一个请求进来,该让谁来处理?为什么是它,而不是别人?
| GRASP原则 | 解决的问题 | 代码中的信号 | 反模式(踩坑现场) |
|---|---|---|---|
| 信息专家 | 谁该做这件事? | 方法访问的数据就在本类属性里(如Order.calculateTotal()用this.items) | PaymentService.calculateTotal(items)——把计算逻辑从Order挪到服务层,导致Order变贫血模型 |
| 创建者 | 谁负责造新对象? | 类A的方法里,new了类B的实例,且A持有B的引用(如Order.createItem()返回new LineItem()) | OrderService.createOrder()里new Order()又new Customer()——服务层越权创建领域对象,破坏封装 |
| 低耦合 | 如何减少依赖? | 类A的方法参数是接口(PaymentProcessor.process(payment)),而非具体类(WechatPay.process()) | OrderService直接new WechatPay(),导致加支付宝时要改OrderService源码 |
| 高内聚 | 类的职责是否干净? | 一个类只有3-5个public方法,且都围绕同一主题(如Order只管订单状态、商品、金额,不管发邮件、记日志) | Order类里有sendEmail()、logToELK()、generatePDF()——职责爆炸,改一个功能牵一发动全身 |
| 控制器 | 谁响应用户请求? | MVC中OrderController接收HTTP请求,调用OrderService,不处理业务逻辑 | OrderController里写if (order.total > 1000) applyVIPDiscount()——控制器掺和业务规则 |
注意:GRASP原则常组合使用。例如“支付渠道扩展”需求,先用信息专家原则:手续费计算逻辑应放在
PaymentMethod子类中(WechatPay.feeRate());再用多态原则:OrderService.processPayment(PaymentMethod method)接受接口,运行时注入具体实现;最后用低耦合:OrderService不依赖任何支付SDK,只依赖PaymentMethod接口。这三步,就是策略模式的自然推导。
5.2 设计模式:不是“套模板”,而是“给常见冲突命名”
设计模式是前人踩坑后总结的冲突解决方案命名。比如“单例模式”,本质是解决“全局唯一实例”与“延迟初始化”“线程安全”的冲突。复习题里考单例,绝不是让你默写双重检查锁代码,而是考你:什么场景下必须用单例?什么场景下用单例反而是毒药?
必须用单例的场景:
- 资源管理器:数据库连接池、线程池、日志文件句柄。这些资源昂贵且全局共享,
new多次会耗尽系统资源。 - 配置中心:
ConfigManager.getInstance().get("db.url"),确保全应用读同一份配置。
✅ 正确用法:
ConfigManager是饿汉式单例(类加载时初始化),避免并发问题;DatabaseConnectionPool是懒汉式+双重检查,节省启动时间。- 资源管理器:数据库连接池、线程池、日志文件句柄。这些资源昂贵且全局共享,
禁用单例的场景:
- 有状态的业务对象:如
ShoppingCart。用户A的购物车和用户B的购物车内容完全不同,用单例会导致数据串扰。 - 需要Mock测试的类:单例的静态
getInstance()方法难以被测试框架替换,导致单元测试必须连真实数据库。
❌ 翻车现场:某系统用单例
CartManager存用户购物车,结果用户A加商品,用户B刷新页面也看到——典型的“状态污染”。- 有状态的业务对象:如
// ✅ 正确:无状态工具类用单例(安全) public class StringUtils { private static final StringUtils INSTANCE = new StringUtils(); private StringUtils() {} // 私有构造 public static StringUtils getInstance() { return INSTANCE; } public String capitalize(String s) { return s.toUpperCase(); } // 无状态 } // ❌ 错误:有状态业务对象用单例(危险) public class ShoppingCart { private static ShoppingCart INSTANCE = new ShoppingCart(); // 全局唯一! private Map<String, Integer> items = new HashMap<>(); // 状态! public void addItem(String productId, int qty) { items.merge(productId, qty, Integer::sum); // 所有用户共用这个Map! } }这段代码的灾难性后果,就是复习题里“高内聚”“低耦合”原则的活体反例:ShoppingCart单例强行把所有用户状态耦合在一起,彻底丧失内聚(一个类管所有人的购物车)。
5.3 从复习题到真实项目:我的血泪教训
去年参与一个电商后台重构,需求是“订单导出支持Excel和PDF,未来可能加CSV”。团队最初方案是:
def export_order(order_id, format_type): if format_type == "excel": return ExcelExporter().export(order_id) elif format_type == "pdf": return PdfExporter().export(order_id) # 加CSV?再加一个elif...——典型的条件分支反模式,违反GRASP的多态和低耦合。
我坚持用策略模式重构:
class ExportStrategy(ABC): @abstractmethod def export(self, order_id): pass class ExcelExporter(ExportStrategy): def export(self, order_id): ... class PdfExporter(ExportStrategy): def export(self, order_id): ... # Controller中 def export_order(order_id, strategy: ExportStrategy): return strategy.export(order_id) # 依赖倒置,零修改加新格式上线后第三周,产品提需求加CSV导出。同事只新增CsvExporter类,改一行DI配置,5分钟搞定。而隔壁组还在export_order()里加elif,改完要测所有分支。
从那以后,我每次接到“支持多种XX”的需求,第一反应不是写if-else,而是打开GRASP原则清单,划掉“信息专家”(导出逻辑归谁?)、“低耦合”(如何不改现有代码?)、“多态”(如何让新类型无缝接入?)。这份复习资料里的每一道题,都是真实战场的弹痕。希望帮到你。
本文还有配套的精品资源,点击获取