资讯动态

UML不是画图,是系统责任的契约表达

发布时间:2026/10/30 2:52:56 来源:尧图企业网站定制
简介本资源是面向软件工程专业学生及UML初学者的系统分析与设计复习备考资料聚焦UML核心概念与典型考题解析助力快速掌握类图、交互图、关系建模、高内聚低耦合等关键知识点。压缩包为单个132KB的Word文档.doc内容结构清晰涵盖12道高频考点题——包括关联多重度判定、组合关系建模、客户-订单基数表达、顺序图与协作图对比、UML图分类类图/用例图/序列图/协作图、依赖/关联/泛化/实现四类关系辨析、领域模型定义、可见性含义等并附标准答案与详细解析。所有题目均源自真实教学场景解析紧扣UML规范兼顾理论深度与应试逻辑便于对照学习、查漏补缺与考前强化。目前已有405人学习下载适合课程复习、期末冲刺及软考初级系统分析员备考使用。1. UML 不是画图工具而是「系统认知的压缩协议」为什么一份复习题集比十套建模软件教程更能暴露设计盲区你有没有过这种经历用 StarUML 拉出一张完美类图箭头方向、多重度、虚线实线全对可一到写代码时——“这个方法该放哪个类”“订单状态变更谁来负责通知库存”“客户删除了它的历史订单要不要级联删”——瞬间卡壳。不是不会画是画完不知道图和代码之间那层“翻译规则”到底在哪。这份《UML系统分析与设计复习试题》表面是选择题、填空题、简答题的集合实则是一套经过千锤百炼的「认知压力测试」它不考你会不会拖拽图形而专挑那些在真实建模中会让人犹豫半秒、改三次、最后默默注释掉的灰色地带——比如第3题里“一个订单对应几个客户”看似是语法题实则是检验你是否真正理解「关联方向性」与「责任归属」的绑定关系第4题对比顺序图与协作图表面问布局差异深层在逼你回答“当团队争论‘这个消息该由谁发’时哪张图能一眼锁定决策依据”。它把 UML 从视觉规范还原为系统责任的分配契约。你答对一道“多重度”题不是记住了 B 选项而是确认了自己脑中那个“类 A 持有类 B 列表”的模型是否真的承载了“创建、维护、销毁”这一整条生命周期责任。这才是复习题的价值它不教你怎么画它逼你验证——你画出来的每一条线是不是都已在代码里埋好了对应的指针、回调或事件监听器。2. 关联多重度不是数字游戏而是对象生命周期的控制权声明UML 类图里最常被当成“填空题”处理的就是关联线旁的1、0..*、1..*这些数字。但如果你只把它看作约束条件就彻底误读了 UML 的设计哲学。多重度的本质是两个类之间控制权与依赖强度的量化表达。它直接决定代码中谁持有引用、谁负责初始化、谁触发销毁——这些细节恰恰是后期重构时最痛的根源。2.1 多重度背后的真实代码契约以“客户-订单”为例题目第3题给出业务规则“一个客户提交 0 个或多个订单一个订单由一个且仅由一个客户提交”。标准答案是46选 B1 个客户、47选 D0 个或多个订单。但这个答案的代码落点是什么我们来看两种典型实现// 方案AOrder 持有 Customer 引用强控制 public class Order { private Customer customer; // 必须非空 → 对应多重度 1 // ... } public class Customer { private ListOrder orders; // 可为空 → 对应多重度 0..* // ... }// 方案BOrder 仅存 customerID弱耦合 public class Order { private Long customerId; // ID 可为空但业务要求“必须有客户”故仍需非空校验 private Customer customer; // 延迟加载但构造时需保证 customer ! null }关键区别在于方案A 中Order 的存在完全依赖 Customer 实例的存活。一旦 Customer 被 GC 或显式置 nullOrder 就成了“孤儿对象”后续调用order.getCustomer().getName()必然 NPE。而方案B 通过 ID 解耦Order 可独立存在customer 加载失败时可降级处理如显示“客户信息不可用”。这正是多重度1在代码中的真实重量——它不是数据库外键约束而是运行时对象图的拓扑强制。你画下Order ——1—— Customer就等于承诺Order构造函数里必须接收一个有效的Customer实例且其生命周期不得早于Order结束。2.2 组合Composition与聚合Aggregation多重度如何升级为生死契约题目第2题提到“类A由类B的一个实例和类C的1个或多个实例构成”答案指向组合关系。这里多重度1和1..*已不再是简单计数而是所有权移交的法律文书A ——1—— BB 的生命周期完全由 A 控制。A 销毁时B 必须同步销毁如A的close()方法中调用b.close()。A ——1..*—— C每个 C 实例的创建、使用、销毁均由 A 全权负责。不能出现new C()后交给外部管理也不能让外部直接c.destroy()。这在代码中体现为严格的构造与销毁协议public class A { private final B b; // final 确保不可替换 private final ListC cs; // 所有 C 均由 A 创建 public A() { this.b new B(); // A 创建 B this.cs Arrays.asList(new C(), new C()); // A 创建所有 C } public void close() { this.b.close(); // A 销毁 B this.cs.forEach(C::close); // A 销毁所有 C } }注意这里cs是ListC而非List? extends C因为 A 必须精确知道每个 C 的具体类型才能调用其close()。如果 C 是接口A 还需持有具体的工厂或策略来创建/销毁它们——这正是 GRASP 中“创建者Creator”模式的落地场景。多重度在此刻已从图形标注升维为构造函数签名、成员变量修饰符、资源清理方法的强制约定。2.3 为什么“0..1”比“1”更危险——可选关联的隐式分支爆炸多重度0..1零个或一个常被误认为“可选很简单”。但它是系统中最易滋生 bug 的温床。看这个反例public class Payment { private Invoice invoice; // 0..1 关联 public void process() { if (invoice ! null) { invoice.markAsPaid(); } // ... 其他逻辑 } }表面安全但问题藏在process()的调用链里如果Payment被多个线程并发调用而invoice是延迟加载的就会出现竞态条件——两个线程同时发现invoice null各自创建新Invoice导致重复开票。0..1的真正含义是该关联的存在与否必须由某个明确的、原子的决策点控制。正确做法是将决策上移public class PaymentService { public Payment createPayment(Long orderId) { Invoice invoice invoiceRepository.findByOrderId(orderId); return new Payment(invoice); // 决策在此invoice 存在则传入不存在则传 null } }此时Payment的构造函数成为唯一入口invoice的null状态是确定性的而非运行时随机态。0..1的本质是要求你在代码中显式声明一个“决策上下文”而不是把if (x ! null)散布在各处。这是复习题第9题中“依赖”与“关联”区分的关键——依赖是临时借用关联是长期持有0..1关联必须配一个确定性的创建/获取上下文。3. 交互图顺序图与协作图不是两种画法而是两种调试视角的切换开关UML 交互图常被简化为“顺序图按时间排协作图按空间排”。这种说法没错但完全没触及核心——它们是开发者在不同调试阶段使用的两种心智模型。当你在 IDE 里单步调试一个复杂流程时顺序图是你脑中的时间轴当你排查分布式系统中服务间调用链断裂时协作图是你眼中的网络拓扑图。复习题第4、72、86题反复对比二者正是要你建立这种“场景化切换”能力。3.1 顺序图时间轴上的责任追踪器顺序图的核心价值在于它强制你回答“这条消息发出后下一个执行步骤是谁它失败了谁来兜底” 题目第21题的 SSDSystem Sequence Diagram要求画出makeNewSale()→enterItem()→endSale()→makePayment()的流程。这不是画四条垂直生命线那么简单而是要定义每一步的契约边界:Cashier :POS :System | | | | makeNewSale() | |----------------| | | | | enterItem(id,q)| |----------------| | | | | endSale() | |----------------| | | | | makePayment(amt)| |----------------|关键在:System这条生命线——它代表整个黑盒系统。复习题第51题强调“画 SSD 图时不对系统的内部结构与功能进行描述”。这意味着:System的每一个输入消息都必须对应一个明确定义的操作契约Operation Contract。例如endSale()的契约可能是前提已调用makeNewSale()且至少一次enterItem()保证返回总金额且销售状态设为COMPLETED副作用创建Sale实体更新库存这个契约就是你写endSale()方法签名和 Javadoc 的依据。顺序图的时间轴本质是操作契约的执行序列。你画错了一条消息的顺序比如把makePayment()画在endSale()之前代码里就必然出现“未完成销售就付款”的逻辑漏洞。顺序图不是描述“发生了什么”而是定义“必须按什么顺序发生”。3.2 协作图对象关系网中的故障定位仪协作图的价值在于它让你一眼看清“当某个消息失败时影响范围有多大”。复习题第72题指出“协作图清楚地表示了对象间的关系但时间顺序必须从顺序号获得”。这句话的潜台词是协作图帮你快速识别‘脆弱连接点’。看一个典型场景[Cashier] ——1—— [Sale] ——1..*—— [LineItem] | | | | ——1—— [Inventory]假设Sale.endSale()调用失败协作图立刻告诉你直接依赖LineItem因1..*关联可能有多个间接依赖Inventory因Sale也关联Inventory无依赖Cashier只是发起者不参与后续计算而顺序图只会显示Cashier → Sale → LineItem → Inventory的线性链条无法直观呈现Sale同时向两个方向发消息的并行性。协作图的“空间组织”本质是依赖图的可视化。当你在生产环境看到Sale.endSale()超时协作图会引导你优先检查LineItem和Inventory的响应时间而不是去翻Cashier的日志——因为Cashier与失败无直接关联。这就是为什么复习题强调“协作图用于过程的详细设计”它帮你设计出可隔离、可监控、可熔断的对象网络。3.3 为什么二者必须共存——时间与空间的不可分割性顺序图和协作图的转换不是图形学操作而是同一系统在不同抽象层级的投影。复习题第4题说“顺序图可以清晰表示消息顺序但需要较多水平空间协作图增加对象容易但消息多时难表顺序”。这揭示了一个深刻事实任何真实系统既无法脱离时间谈空间也无法脱离空间谈时间。举个例子enterItem()操作需同时更新LineItem和Inventory。顺序图会画两条平行消息:Cashier :Sale :LineItem :Inventory | | | | | enterItem() | | |---------------- | | | | | |---------|但协作图会显示Sale同时连接LineItem和Inventory。如果Inventory更新慢顺序图会让你想“加超时”协作图则会提示你“Sale是否应该解耦Inventory调用比如发异步消息”。前者优化时间流后者重构空间拓扑。二者缺一不可。复习题反复考对比就是要你建立这种双重视角——就像医生既要看心电图时间序列也要看CT扫描空间结构才能诊断心律失常。4. 高内聚与低耦合不是设计原则而是代码可维护性的物理定律“高内聚、低耦合”被说烂了但多数人只记住口号不知其物理本质。复习题第6、24、83题反复出现是因为它直指一个残酷现实90% 的代码腐化始于一个类悄悄承担了本不属于它的职责。高内聚不是“把相关方法放一起”而是“让一个类只做一件足够小、足够完整的事”低耦合不是“少写 import”而是“让修改一个类时其他类几乎不用动”。它们是软件熵减的守恒定律。4.1 高内聚的代码证据单一职责的“最小闭环”复习题第6题定义“高内聚度是对一个类中的各个职责之间相关程度和集中程度的度量”。关键在“相关程度”和“集中程度”。我们看一个反例// 低内聚一个类干三件事 public class OrderProcessor { // 职责1订单校验 public boolean validate(Order order) { ... } // 职责2库存扣减 public void deductInventory(Order order) { ... } // 职责3支付网关调用 public PaymentResult pay(Order order) { ... } }问题在哪validate()只读Order字段deductInventory()要调InventoryServicepay()要调PaymentGateway。三者依赖不同变化原因不同校验规则变、库存策略变、支付渠道变却绑在一个类里。这就是内聚度低——职责之间不相关校验不依赖库存库存不依赖支付且不集中没有共同的数据或状态。高内聚的改造是拆出三个最小闭环// 职责1闭环纯数据校验 public class OrderValidator { public ValidationResult validate(Order order) { ... } // 只依赖 Order } // 职责2闭环库存状态管理 public class InventoryManager { public void deduct(String sku, int quantity) { ... } // 只依赖库存领域模型 } // 职责3闭环支付流程编排 public class PaymentOrchestrator { public PaymentResult process(Order order) { ... } // 协调 Validator InventoryManager Gateway }每个类现在都有明确的输入、输出、不变量OrderValidator的不变量是“输入 Order输出 ValidationResult”InventoryManager的不变量是“扣减后库存 0”。高内聚的物理表现就是你能给每个类写一句精准的、不带“and”或“or”的 Javadoc。复习题第83题强调“不要给一个类分派太多职责”就是在教你识别这种“and/or 气味”——代码里出现“and”“or”“以及”往往是职责撕裂的警报。4.2 低耦合的工程实践依赖倒置的具体化低耦合常被误解为“减少类之间的引用”。但复习题第24题 GRASP 的“低耦合”模式明确指出“如何支持低依赖关系和增加重用”。重点在“重用”——耦合度高的代码根本无法重用。看一个经典陷阱// 高耦合直接依赖具体实现 public class ReportGenerator { private ExcelExporter exporter new ExcelExporter(); // 依赖具体类 public void generateReport(ListData data) { byte[] excel exporter.export(data); // 无法替换成 PDF } }ReportGenerator与ExcelExporter耦合导致无法生成 PDF 报告。低耦合的解法不是删掉exporter而是引入抽象让依赖发生在抽象层// 定义抽象报告导出能力 public interface ReportExporter { byte[] export(ListData data); } // 具体实现1 public class ExcelExporter implements ReportExporter { ... } // 具体实现2 public class PdfExporter implements ReportExporter { ... } // ReportGenerator 依赖抽象 public class ReportGenerator { private final ReportExporter exporter; // 依赖接口 public ReportGenerator(ReportExporter exporter) { this.exporter exporter; // 通过构造注入 } public void generateReport(ListData data) { byte[] report exporter.export(data); // 调用抽象方法 } }这里的关键变化ReportGenerator的构造函数参数从ExcelExporter变为ReportExporterexporter成员变量类型变为接口generateReport()内部调用的是接口方法而非具体类方法这三点就是低耦合的可验证代码证据。复习题第24题 GRASP 的“低耦合”模式其落地就是这三行代码改造。它不追求“零依赖”而是追求“依赖可插拔”。当你看到一个类的构造函数里全是具体类名就知道它耦合度超标了。4.3 高内聚与低耦合的共生关系一个类的“自洽性”证明高内聚与低耦合不是并列原则而是因果关系。一个类内聚度越高它对外的耦合度就越低。因为内聚度高的类职责单一所需外部依赖自然最少。反之一个类耦合度高往往是因为它内聚度低——它被迫去协调多个不相关的子系统。验证方法很简单统计一个类的import语句和构造函数参数。如果import包含com.pay.*、com.inventory.*、com.report.*且构造函数接受PaymentService、InventoryService、ReportService那它一定是低内聚职责分散→ 高耦合依赖过多。如果import只有java.util.*和本包类构造函数参数只有Order、Money等领域对象那它大概率高内聚专注领域逻辑→ 低耦合无需外部服务。复习题第24题 GRASP 的“信息专家”模式就是这一思想的源头“把职责分配给拥有最多相关信息的类”。Order类拥有价格、商品列表、客户信息那么“计算总价”就该是Order的方法而不是让OrderCalculator去读取Order的所有字段——后者是典型的低内聚OrderCalculator不拥有数据→ 高耦合必须知道Order的所有细节。5. 常见问题排查那些在复习题里埋着、却在真实项目中让你通宵的坑复习题不是用来背答案的而是用来预演真实踩坑现场的。下面这些“现象→原因→解决”全部来自某高校课程实验和某公司遗留系统重构的真实血泪经验。它们在题干里轻描淡写但在代码里足以让你 debug 三天。5.1 现象顺序图里消息编号是 1.1、1.2、2但代码里endSale()总是空指针原因SSD 图中:System生命线被当作黑盒但实际编码时你忘了:System必须是一个真实存在的、可注入的 Spring Bean。endSale()方法被定义在SystemFacade类里而该类未被Service注解导致 Spring 无法注入cashier.system.endSale()调用的是 null。解决SSD 图中的:System必须映射为一个具体的、Spring 管理的组件。检查SystemFacade类是否有Service并在Cashier的构造函数中通过Autowired注入而非new SystemFacade()。5.2 现象协作图显示Sale与Inventory有1..*关联但数据库里sale_id字段允许 NULL原因UML 关联的多重度1..*表示“每个Sale必须关联至少一个Inventory操作”但数据库外键约束只保证sale_id不为空不保证Inventory记录真实存在。更致命的是Sale类的inventoryOperations字段被声明为ListInventoryOperation但初始化为null而非new ArrayList()导致首次添加时 NPE。解决在Sale构造函数中强制初始化inventoryOperations new ArrayList()数据库层面若业务要求强关联应添加NOT NULL约束并在Sale创建时立即插入一条默认InventoryOperation。5.3 现象用例图里Actor是“电话用户”但代码中PhoneUser类包含creditCardNumber字段引发 PCI-DSS 合规审计失败原因复习题第9题要求画Actor与UseCase的关系但Actor在 UML 中是角色role不是用户实体user entity。PhoneUser作为 Actor只代表“能拨打电话的人”不应包含任何敏感数据。creditCardNumber属于Customer实体应在MakePaymentUseCase 中通过Customer关联获取而非放在PhoneUser里。解决删除PhoneUser类中的creditCardNumber在MakePayment用例实现中通过customerId查询Customer实体再获取其支付信息。Actor 与 Entity 必须严格分离。5.4 现象领域模型图里Customer与Order是1对0..*但Order类的customer字段是private Customer customer;导致 Hibernate 无法代理懒加载查询所有订单时 N1 查询爆炸原因UML 关联的多重度1表示“每个Order必须有Customer”但 Hibernate 的ManyToOne(fetch FetchType.LAZY)要求customer字段必须是protected或public以便 CGLIB 生成代理子类。private字段使代理失效Hibernate 改为急加载触发 N1。解决将Order.customer字段改为protected Customer customer;或使用Access(AccessType.PROPERTY)将 getter/setter 设为public字段保持private。5.5 现象GRASP “控制器”模式说“谁负责处理输入系统事件”但Cashier类里堆满了makeNewSale()、enterItem()、endSale()方法违反高内聚原因复习题第24题“控制器”模式的本意是让Cashier作为 UI 层的薄胶水层只负责接收事件、调用领域服务、返回结果而非实现业务逻辑。makeNewSale()等方法本该属于SaleServiceCashier只需saleService.makeNewSale()。解决将所有业务逻辑方法从Cashier移出创建SaleService类Cashier仅保留PostMapping方法调用saleService并处理 HTTP 层异常。Cashier应像路由器只转发不计算。6. 从复习题到源码为什么我每次建模前都强制手写三份契约文档这份复习题集最珍贵的不是答案本身而是它强迫你面对一个真相UML 图不是设计的终点而是契约的起点。我曾参与一个电商系统重构团队花两周画出完美的类图、顺序图、状态图上线后第一周就因“订单取消后库存未恢复”被投诉。复盘发现所有图都正确但没人写出cancelOrder()操作的精确契约——它没定义“库存恢复是同步还是异步”没说明“恢复失败时订单状态应设为CANCELLATION_FAILED还是CANCELLED_WITH_ERROR”更没约定“库存服务不可用时的降级策略”。图是骨架契约才是血肉。现在我的硬性流程是先写操作契约Operation Contract对每个用例主事件如endSale()用三句话定义——前提Precondition调用前系统必须满足的状态如“所有商品库存充足”保证Guarantee调用后系统必须达到的状态如“创建Sale实体状态为COMPLETED”副作用Side Effect必须发生的外部动作如“调用InventoryService.deduct()”再画 SSD 图确保每个消息都对应一个契约里的保证项。endSale()消息发出后:System生命线上必须有一个SaleCreated返回消息否则契约未履行。最后画类图根据契约中的“副作用”推导出哪些类必须存在、哪些关联必须建立。InventoryService.deduct()的存在强制Sale类必须有关联InventoryService的依赖关系。这个流程把复习题里的选择题变成了可执行的工程纪律。比如第3题“客户-订单”关系我会先写契约前提客户存在且状态为ACTIVE保证创建Order实体order.customerId指向该客户副作用发送OrderCreatedEvent然后 SSD 图里必然有Cashier → System → OrderCreatedEvent类图里Order必有customerId字段和ManyToOne关联。图不再飘在空中它长在契约的根系上。从那以后我每次启动新模块都强制走一遍这三步。不是为了交差而是为了在代码写第一行前就让所有人对“这个功能到底要做什么”达成不可辩驳的共识。UML 的力量不在画得有多美而在它能否成为团队间无需解释的通用语言。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑