简介本资源是一份面向软件工程专业学生及UML初学者的系统化复习资料聚焦UML建模核心考点与典型试题解析助力备考《系统分析与设计》《面向对象分析与设计》等课程。内容覆盖关联多重度、组合/聚合关系判别、客户-订单类图建模、顺序图与协作图对比分析、高内聚设计原则、UML九种图类图、用例图、序列图、协作图等的适用场景与识别要点、可见性与领域模型定义、GRASP设计模式基础、统一过程阶段划分等12大高频知识点每题均附标准答案与原理详解。资源为单个Word文档.doc全文约132KB结构清晰、排版规范便于打印复习或碎片化学习。已有407人下载学习适合作为课堂补充、考前冲刺与概念自查的实用型试题精讲材料。1. UML 不是画图工具而是系统认知的“结构化翻译器”为什么你画得再标准也改不了需求错、设计崩、代码散的宿命很多人把 UML 当成考试前突击背题的“图形口诀本”——记牢“实心菱形是组合”“虚线箭头是依赖”就能在试卷上勾对答案也有人把它当成开发前走流程的“PPT装饰项”类图画完就锁进文档库后续编码全靠拍脑袋。但真实项目里最痛的翻车现场往往不是语法画错了而是需求评审时业务方说“客户能下多个订单”开发点头说“好1..*”结果上线后发现“一个订单必须绑定唯一收货地址”而地址信息却挂在客户类里改一次要动三张表五个服务画完顺序图消息箭头连得滴水不漏可真正写代码时发现收款员对象根本没权限调用makePayment()因为支付网关鉴权逻辑被硬塞进了销售控制器违反了 GRASP 的“信息专家”原则协作图里三个对象围成三角传递消息看着很美但部署时才发现它们分属不同微服务跨进程调用延迟飙升而图里压根没标出任何边界或协议约束。这些不是“UML 用得不熟”而是把 UML 当成了静态快照仪而非动态契约生成器。它真正的底层机制是一套将模糊业务语义→精确结构约束→可验证行为契约的三级翻译引擎。多重度不是数字游戏是数据一致性边界的数学表达交互图不是消息流水账是对象职责边界的显式声明领域模型不是名词堆砌是真实世界因果链在内存中的拓扑映射。这份《UML 系统分析与设计复习试题》之所以值得深挖正因为它用 80 道真题把这套翻译引擎的输入规则、转换逻辑和输出校验点全拆解成了可复现、可反推、可踩坑的实战切片——它不教你怎么画图它逼你回答“这个箭头背后谁在承担失败这个数字范围之内哪条路径会崩”2. 关联多重度不是“几个对象连几条线”而是数据一致性边界的数学签名UML 类图中那串看似随意的数字如1、0..*、1..5常被初学者当作装饰性标注。但当你在数据库建模时发现外键约束报错或在微服务间调试时卡在空指针异常回溯根源90% 都指向对多重度的误读——它根本不是描述“视觉上连几条线”而是用离散数学语言为两个类实例间的存在性依赖和数量守恒律签发的强制契约。2.1 多重度的本质从集合论到事务边界的三层映射多重度符号如A 1..* —— B 1实际是三个维度的压缩表达存在性Existence0..*中的0表示“可不存在”1表示“必须存在”。这直接对应数据库的NOT NULL约束或代码中的非空校验。基数Cardinality*表示“无上限”但生产环境里它永远受物理资源限制如内存、连接数、索引长度。1..5则强制要求业务层做范围校验否则INSERT会因违反 CHECK 约束失败。生命周期耦合Lifecycle Coupling1对0..*的关联隐含着“B 的创建必须先于 A”的时序约束而1对1的组合则意味着“B 的销毁必须晚于 A”——这在 ORM 映射中会触发级联删除在分布式系统中则需 Saga 模式保障最终一致性。提示多重度不是孤立存在的。题干中“类 A 由类 B 的一个实类和类 C 的 1 个或多个实类构成”这里的“由……构成”是组合Composition的语义标记它比普通关联更强C 的实例生命周期完全依附于 A。若忽略这点仅按A 1 —— C 1..*实现就会导致 C 实例在 A 删除后仍残留形成数据孤儿。2.2 从试题到代码客户-订单关系的三重实现陷阱试题第 3 题给出经典场景“一个客户提交 0 个或多个订单一个订单由一个且仅由一个客户提交”。表面看是Customer 0..* —— Order 1但落地时有三重陷阱陷阱一ORM 映射反模式# 错误示范用外键直连忽略聚合根边界 class Order(models.Model): customer models.ForeignKey(Customer, on_deletemodels.CASCADE) # ❌ 违反 DDD 聚合根原则问题Order直接持有Customer外键导致订单服务能绕过客户域逻辑直接修改客户状态。正确做法应是Order仅存储customer_id值对象所有客户变更必须通过CustomerService。陷阱二API 设计失焦# 错误请求POST /orders { customerId: 123, ... } # 问题暴露了内部 ID且未校验 customerId 是否真实存在正确契约应为POST /customers/123/orders # 路径体现聚合归属 # 服务端必须先查 Customer(123) 是否 active再创建 Order陷阱三并发冲突盲区当高并发下单时Order创建与Customer余额扣减若不在同一事务会出现超卖。多重度1要求的不仅是“存在”更是“强一致性存在”。解决方案不是加锁而是用事件溯源OrderCreated事件触发CustomerBalanceDeducted由 Saga 协调器保证最终一致。2.3 多重度校验的自动化实践用 Python 做契约卫士与其靠人眼检查类图不如让代码自动验证多重度约束。以下是一个轻量级校验器基于 SQLAlchemy 模型定义from sqlalchemy import inspect from typing import Dict, List, Optional def validate_multiplicity(model_class, field_name: str, expected_min: int, expected_max: Optional[int]) - List[str]: 校验模型字段的多重度是否符合预期 :param model_class: SQLAlchemy 模型类 :param field_name: 关联字段名如 customer :param expected_min: 最小基数1 表示必须存在 :param expected_max: 最大基数None 表示 * :return: 错误列表 errors [] mapper inspect(model_class) # 检查外键约束存在性 if not hasattr(mapper.attrs, field_name): errors.append(f{model_class.__name__} 缺少字段 {field_name}) return errors rel_attr mapper.attrs[field_name] if not rel_attr.back_populates and not rel_attr.backref: errors.append(f{model_class.__name__}.{field_name} 未配置反向关系无法校验多重度) return errors # 检查 NOT NULL对应最小基数 1 if expected_min 1: fk_col getattr(model_class, field_name).property.local_columns[0] if not fk_col.nullable: errors.append(f{model_class.__name__}.{field_name} 外键列应为 NOT NULL但当前 nullable{fk_col.nullable}) # 检查最大基数需结合业务逻辑此处仅提示 if expected_max is not None: errors.append(f警告{model_class.__name__}.{field_name} 最大基数 {expected_max} 需在业务层校验数据库无法强制) return errors # 使用示例 errors validate_multiplicity(Order, customer, expected_min1, expected_maxNone) if errors: raise AssertionError(多重度校验失败 ; .join(errors))这段代码的价值不在“运行时校验”而在编译期契约声明它把1和0..*从图纸符号转化成了可执行、可测试、可集成进 CI 的代码断言。当你在 PR 中看到validate_multiplicity报错就知道不是“图没画对”而是“契约没对齐”。2.4 常见问题排查多重度引发的血泪现场现象前端提交订单时后端返回IntegrityError: null value in column customer_id原因Order.customer_id字段设为NULLABLEFalse但 API 入参未传customer_id或前端传了空字符串。多重度1要求该字段必须有有效值但校验逻辑缺失。解决在 FastAPI 的 Pydantic Model 中强制customer_id: int非 Optional并在路由层捕获RequestValidationError返回 422。现象数据库查询SELECT * FROM orders WHERE customer_id 123返回 0 条但业务逻辑认定“客户 123 必须有订单”原因多重度0..*允许“零订单”但业务方口头承诺“新注册客户首单必达”这属于隐含约束未建模。解决在领域模型中新增CustomerStatus枚举ACTIVE_WITH_ORDER状态才允许进入下单流程用状态机校验。现象微服务 A 调用服务 B 的/orders接口返回{error: customer not found}但 A 日志显示刚创建了客户原因Customer 1..* —— Order 1要求客户存在但服务 B 的客户缓存未及时刷新导致读己之所写Read-Your-Writes失效。解决在客户创建成功后主动向服务 B 的缓存集群发送INVALIDATE customer:123事件而非依赖 TTL 过期。现象导出报表时Customer.name字段大量为NULL但订单数据完整原因多重度只约束customer_id存在性未约束Customer实体本身的完整性。customer_id可能指向已软删除的客户记录。解决在关联查询时添加AND customers.deleted_at IS NULL或在Order模型中增加customer_name冗余字段需事件驱动更新。现象压力测试中Order创建吞吐量骤降DB CPU 达 100%原因为满足1的存在性每次创建Order都执行SELECT * FROM customers WHERE id ?校验未加索引导致全表扫描。解决在customers.id上建主键索引默认已有并确认 ORM 未因懒加载触发 N1 查询。3. 交互图顺序图与协作图不是“画法不同”而是系统控制流的两种观测坐标系UML 交互图常被简化为“顺序图是竖着画协作图是横着画”。但当你面对一个支付失败率突增的线上事故翻遍顺序图找不到瓶颈却在协作图里一眼锁定问题节点——这时你就明白它们根本不是同一件事的两种画法而是同一组对象交互行为在时间维度与空间维度上的正交投影。顺序图告诉你“消息何时发生”协作图告诉你“消息在何处交汇”。忽略任一维度都等于在黑匣子里蒙眼调试。3.1 顺序图时间轴上的控制流原子钟顺序图的核心价值在于将模糊的“系统应该怎么做”转化为精确的“第 N 毫秒对象 A 向对象 B 发送消息 XB 在 M 毫秒内返回 Y”。它强制你回答三个致命问题时序不可逆性makeNewSale()必须在enterItem()之前调用否则Sale对象未初始化enterItem()会抛NullPointerException。这种依赖不是逻辑约定而是内存状态的物理约束。生命线活性POS对象的生命线垂直虚线从makeNewSale()开始到endSale()结束。若makePayment()调用时POS已销毁就是典型的“use-after-free”错误。激活框Activation Bar的资源语义POS的激活框覆盖makeNewSale()到endSale()全程意味着它在此期间独占 CPU 时间片或数据库连接。若激活框过长就是性能瓶颈的可视化信号。提示顺序图中的alt条件分支和loop循环不是流程图装饰。alt [paymentMethod cash]意味着makePayment()方法内部必须有if分支且分支逻辑必须原子化——现金支付不能部分成功如只扣款不打印小票。3.2 协作图空间拓扑中的责任网络图协作图剥离时间轴聚焦对象间的连接关系与消息流向。它的力量在于暴露那些被顺序图掩盖的架构真相连接即耦合POS与PaymentGateway之间画了一条线意味着POS类必须持有PaymentGateway的引用或通过工厂获取。这条线越粗如用实线箭头耦合度越高。消息编号即控制粒度1.1、1.2表示它们属于同一逻辑单元如一次makePayment()调用而2.1属于另一次调用。若1.1和1.2跨越了服务边界如POS在 JavaPaymentGateway在 Go编号就揭示了分布式事务的必要性。对象分组即边界划分将POS、Sale、Item放在同一泳道PaymentGateway、BankAPI放在另一泳道直观呈现“核心域”与“外部依赖”的物理隔离。3.3 从 SSD 图到可执行代码收款流程的契约落地试题第 21 题要求画出 SSDSystem Sequence Diagram图描述makeNewSale()→enterItem()→endSale()→makePayment()流程。SSD 是交互图的特例其关键约束是系统边界内只有一个“系统”对象所有外部参与者如收款员只能与该对象交互。这意味着makeNewSale()的调用者是CashierActor接收者是System黑盒System内部如何分解为POS、Sale等对象SSD 不关心——那是后续设计图的事System返回给Cashier的必须是明确的响应如newSaleId而非内部对象引用。将此 SSD 转为代码必须遵守三大契约契约一接口隔离// ✅ 正确SSD 定义的公共接口隐藏内部对象 interface SalesSystem { makeNewSale(): Promise{ saleId: string }; enterItem(saleId: string, itemID: string, quantity: number): Promisevoid; endSale(saleId: string): Promise{ totalAmount: number }; makePayment(saleId: string, amount: number): Promise{ receiptId: string }; } // ❌ 错误暴露内部对象违反 SSD 黑盒原则 class POS { createSale(): Sale { ... } // Sale 是内部类不应出现在公共接口 }契约二错误传播路径SSD 中enterItem()若失败如商品不存在必须返回明确错误而非让Cashier捕获NullPointerException。因此接口需定义错误类型type EnterItemError | { code: ITEM_NOT_FOUND; message: string } | { code: SALE_CLOSED; message: string }; enterItem(saleId: string, itemID: string, quantity: number): Promisevoid | EnterItemError;契约三状态一致性endSale()必须确保Sale状态从OPEN变为CLOSED且totalAmount计算不可被并发修改。这要求Sale类实现状态机endSale()是状态转换方法数据库中sales.status字段用ENUM(OPEN, CLOSED)并加CHECK (status CLOSED OR total_amount IS NULL)约束。3.4 常见问题排查交互图落地的五处断点现象顺序图中1: makeNewSale()后POS生命线立即出现2: enterItem()但实际代码中enterItem()抛SaleNotInitializedException原因顺序图未标注makeNewSale()的完成条件。它可能异步初始化而enterItem()在回调前就调用。解决在makeNewSale()消息上加async标签并添加1.1: [onSuccess] initializeSale()激活框明确同步/异步边界。现象协作图里POS与InventoryService有连接但enterItem()调用时InventoryService返回503 Service Unavailable整个流程中断原因连接线未标注容错策略。POS应实现熔断Circuit Breaker或降级Fallback而非直接传播异常。解决在连接线上加注释circuit-breaker: 3 failures in 30s并在POS类中注入InventoryClient的熔断代理。现象endSale()在顺序图中是单条消息但日志显示它触发了 12 个数据库事务原因顺序图未体现内部子流程。endSale()实际包含计算折扣、更新库存、生成发票等子操作每个都是独立事务。解决用ref框引用子顺序图或在消息旁标注transaction boundary。现象makePayment()在协作图中连接PaymentGateway但支付成功后订单状态仍是PENDING原因缺少事件通知链。PaymentGateway成功后应发布PaymentSucceeded事件由OrderService订阅并更新状态。协作图未画出事件总线。解决添加EventBus对象PaymentGateway向其发送1.1: PaymentSucceededOrderService订阅1.2: updateOrderStatus()。现象SSD 图中Cashier调用makeNewSale()但前端按钮点击后无响应Network Tab 显示OPTIONS预检失败原因SSD 未考虑协议约束。Cashier是人但实际调用是浏览器 JS需处理 CORS。解决在 SSD 顶部注明Protocol: HTTPS/REST并在System对象旁标注CORS enabled for cashier-ui.example.com。4. 领域模型与 GRASP为什么“找名词”只是起点而“分派职责”才是设计的生死线很多开发者卡在“识别概念类”这一步从“顾客带着商品来到 POS 收款台”中圈出Customer、POS、Sale、Item……然后停住。但试题第 20、23、24 题反复追问的从来不是“有哪些类”而是“谁该负责什么”。领域模型不是名词词典它是职责分配的决策日志GRASP 不是设计模式手册它是避免职责错配的防错指南。当你把calculateTotal()方法放在Sale类里还是放在PricingEngine类里决定的不是代码位置而是未来三年系统的可维护性。4.1 领域模型从现实世界到内存世界的因果链拓扑领域模型的核心任务是捕捉业务中的不变因果律。例如“顾客提交订单” → “订单状态变为SUBMITTED” 是必然因果“库存不足” → “订单不能创建” 是约束因果“支付成功” → “订单状态变为PAID” 是事件因果。这些因果关系必须映射为模型中的关联方向与方法归属。试题第 22 题的makeNewSale()表面是POS的方法实则是Sale生命周期的起点——POS只是触发者Sale才是状态载体。若把makeNewSale()实现在POS里就割裂了“销售行为”与“销售实体”的因果绑定。提示领域模型中的关联线如Customer —— Sale不是数据库外键的映射而是责任委托链。Customer不负责管理Sale但Sale必须能通过customer_id回溯到Customer的信用等级、历史订单等上下文以支撑calculateDiscount()。4.2 GRASP 五大原则职责分配的五把手术刀GRASP 不是抽象理论它是针对具体设计病灶的手术方案原则解决的典型病灶试题中的印证代码级实现信号信息专家方法该放哪个类凭感觉乱放第 24 题“谁负责创建Sale实例” →POS因它持有创建所需参数类中方法访问的属性80% 应属于本类或直接关联类创建者新对象由谁 new避免到处 new第 24 题“谁负责创建Sale” →POS因它知道Sale初始化所需的上下文new Sale(...)出现在POS.createSale()而非Cashier.onClick()低耦合修改一个类为何要改十个第 24 题“如何支持低依赖” →POS不直接调用BankAPI而是通过PaymentProcessor接口POS依赖IPaymentProcessor而非AlipayImpl或WechatPayImpl高内聚一个类为何又管订单又管日志第 6、83 题“高内聚度是对职责相关程度的度量” →Sale只管销售状态不处理支付细节Sale类中无sendPaymentRequest()方法只有getTotalAmount()控制器用户点击按钮谁来协调第 24 题“谁负责处理输入事件” →POSController非POS实体类POSController.handleMakeNewSale()调用saleFactory.create()而非自己 new4.3 从“找名词”到“定契约”销售场景的职责重构实战试题第 20 题列出概念类顾客、POS 收款台、收款员、销售、商品标识、商品、商品说明。但若直接按此建模会陷入泥潭错误建模高耦合、低内聚class POS: def enterItem(self, itemID, quantity): # ❌ 违反信息专家POS 不该知道商品价格计算规则 item self.inventory.getItem(itemID) # 直接访问库存 price item.basePrice * self.getDiscountRate() # 自己算折扣 self.currentSale.addItem(item, quantity) # 直接操作 Sale self.printReceipt() # 直接调用硬件正确建模GRASP 指导# 信息专家PriceCalculator 知道所有定价规则 class PriceCalculator: def calculate(self, item: Item, quantity: int, customer: Customer) - Decimal: # 组合会员折扣、满减、时段优惠等策略 pass # 创建者SaleFactory 封装 Sale 创建逻辑 class SaleFactory: def create(self, cashier: Cashier, customer: Optional[Customer]) - Sale: return Sale(cashier_idcashier.id, customer_idcustomer.id if customer else None) # 控制器POSController 协调流程不持有业务逻辑 class POSController: def __init__(self, sale_factory: SaleFactory, price_calculator: PriceCalculator): self.sale_factory sale_factory self.price_calculator price_calculator def handleEnterItem(self, sale_id: str, item_id: str, quantity: int): sale self.getSale(sale_id) item self.inventory.getItem(item_id) # 依赖注入 InventoryService total self.price_calculator.calculate(item, quantity, sale.customer) sale.addItem(item, quantity, total) # Sale 自己管理状态 self.receiptPrinter.printLine(f{item.name}: {total}) # 依赖注入打印机这个重构的关键在于将“谁该做什么”的决策从代码行变成了类职责声明。POSController的构造函数签名__init__(self, sale_factory, price_calculator)就是 GRASP 原则的代码化契约——它明示了“创建职责归SaleFactory定价职责归PriceCalculator”。4.4 常见问题排查职责错配的五种典型症状现象Sale类中有sendEmailNotification()方法原因违反高内聚——销售实体不该关心通知渠道。这是典型的“上帝类”征兆。解决提取NotificationServiceSale发布SaleCompleted事件由监听器调用NotificationService.sendEmail()。现象Cashier类中有createSale()方法但Cashier实体只存姓名和工号原因违反信息专家——Cashier不持有创建Sale所需的上下文如当前营业厅、税率配置。解决SaleFactory.create()接收Cashier对象作为参数但核心逻辑在工厂中Cashier仅提供身份标识。现象修改Item的basePrice字段需同时改Sale、Invoice、ReportGenerator三个类原因违反低耦合——Item的价格变更不该波及所有使用它的类。解决Item提供getCurrentPrice()方法内部封装价格策略如根据日期返回不同价格其他类只调用此方法。现象POSController的单元测试需要 mockInventoryService、PaymentGateway、ReceiptPrinter三个外部依赖原因违反控制器原则——控制器应只协调不持有具体实现。过度依赖注入暴露了职责过载。解决将InventoryService、PaymentGateway的调用封装进SaleContext对象POSController只依赖SaleContext。现象Sale类的addItem()方法中直接调用InventoryService.decreaseStock()原因违反低耦合与信息专家双重原则——Sale不该知道库存服务的存在更不该承担库存扣减的失败重试逻辑。解决Sale添加Item后发布ItemAddedToSale事件InventoryService订阅该事件执行扣减并处理失败如发告警、回滚 Sale。5. UML 图谱的协同机制为什么单独画好一张图毫无意义而九张图的交叉验证才是设计可信度的唯一来源UML 提供 9 种图用例图、类图、对象图、包图、状态图、活动图、序列图、协作图、部署图但很多团队陷入“每种图画一张交差”的误区。试题第 7、16、38、39、79 题反复强调不同图从不同视角描述同一系统它们的交集才是设计的黄金三角。用例图定义“系统必须做什么”类图定义“由什么组成”序列图定义“如何协作完成”三者若不一致设计就必然崩塌。这份复习题的真正价值在于它用题目间的隐含关联逼你建立这种交叉验证思维。5.1 图谱协同的底层逻辑从需求到部署的四层契约链UML 图不是平行关系而是逐层细化、双向验证的契约链层级核心图承诺内容验证方式试题印证L1业务契约用例图系统对外提供的价值功能每个用例必须有业务方签字确认的 Brief 描述第 12、38、47 题用例图用于描述系统与用户交互L2结构契约类图 包图功能如何被分解为可组合的模块类图中的类必须在用例图中至少被一个用例使用第 7、16、39 题类图给出静态设计视图用例图组织行为L3行为契约序列图 状态图 活动图模块如何协作实现用例序列图中的消息必须是类图中类的方法状态图的状态必须是类图中类的属性值第 4、72、86 题序列图描述时间顺序交互状态图描述对象状态转移L4部署契约部署图 构件图行为如何在物理环境中落地部署图中的节点必须对应构件图中的构件构件图中的接口必须被序列图中的消息调用第 39、79 题部署图说明物理拓扑构件图展现构件组织提示交叉验证不是形式主义。当用例图中“使用电话卡”用例与序列图中PhoneCardPayment类的消息不匹配就说明要么用例描述遗漏了支付方式要么序列图画错了实现路径——必须回溯修正而非强行“对齐”。5.2 用例图与类图的双向锚定从“功能”到“结构”的精准映射用例图中的每个椭圆用例必须在类图中找到对应的职责承载者。试题第 12 题的“使用电话卡”用例绝不是简单画个PhoneCardPayment类就完事而要回答谁触发PhoneUserActor调用POS的makePayment()方法谁执行POS将支付请求委派给PhoneCardPaymentService非PhoneCardPayment实体类因电话卡是外部服务谁存储PaymentRecord类持久化支付结果关联Sale谁验证PhoneCardValidator类校验卡号有效性其接口必须在POS的依赖中声明。这种映射最终体现在代码的包结构中src/ ├── usecase/ # 用例实现MakePaymentUseCase ├── domain/ # 领域模型Sale, PaymentRecord ├── service/ # 外部适配PhoneCardPaymentService, PhoneCardValidator └── adapter/ # 技术适配PhoneCardAPIAdapter (调用第三方 HTTP API)若usecase/包中出现了PhoneCardPayment类就证明用例图与类图脱节——用例是“行为”类是“结构”混淆二者会导致贫血模型。5.3 序列图与状态图的时序对齐让“消息流”与“状态流”严丝合缝序列图中的每条消息都应驱动状态图中的一次状态转移。试题第 21 题的endSale()消息必须对应Sale状态图中的OPEN → CLOSED转移stateDiagram-v2 [*] -- OPEN OPEN -- CLOSED: endSale() OPEN -- CANCELLED: cancelSale() CLOSED -- PAID: makePayment() CANCELLED -- [*]若序列图中endSale()后Sale对象还能接收addItem()消息就违反了状态图约束——这在代码中表现为Sale.addItem()在status CLOSED时未抛异常。因此状态图不是文档装饰而是运行时守门员的规格说明书。5.4 部署图与构件图的物理落地从“逻辑模块”到“物理进程”的硬约束试题第 39 题提到“部署图说明体系结构的静态实施视图”这直指微服务拆分的核心矛盾构件图中PaymentService是一个逻辑构件部署图中它必须被部署到payment-node-01物理节点且该节点需开放8080端口若序列图中POS调用本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?