简介:这是一份围绕UML系统分析与设计课程的复习试题详解资料,面向软件工程、计算机相关专业学生以及软考备考者,用于解决系统分析与设计核心概念易混、题型练习不足等问题。内容以问答和选择形式覆盖关联多重度、类图组合关系、客户与订单数量映射、顺序图与协作图比较、高内聚度、UML图分类、四种基本关系、用例图、对象可见性、领域模型、GRASP设计模式、FURPS需求分类等高频考点,均配有答案或简要解析,适合考前冲刺、查漏补缺或课堂配套自测。压缩包共1个doc文档,大小132KB,Word格式便于直接编辑和打印,已有405人学习。总体信息密度较高,既能帮助快速梳理UML建模知识框架,也可作为主观题答题要点参考;尤其适合期末或软考前的集中突击,可供教师出题或课堂测验时参考。
1. 多重度:题型背得再熟,也架不住把 1..* 读反
一份UML系统分析复习试题,第一道题通常是关联的多重度。A选项说“一个类有多个方法被另一个类调用”,B选项说“一个类的实例能够与另一个类的多个实例相关联”。背题的人看完答案就翻篇了,但真正值得拆解的恰恰是这里:多重度不是画在类图边上的一对数字,它是一条对数据库外键、对象集合、业务校验都生效的约束规则。把“客户与订单”那两道题的答案读反,画出来的类图从外观上看不出任何问题,生成的代码却会在查询某个订单时写出“订单.客户”这种不可能存在的字段,报表全部错位。这份资源把多重度、组合与聚集、顺序图与协作图、GRASP、统一过程这些考点按题排开,目的不是让你背答案,而是让你在进考场之前就把每个图背后的语义链打通。理解了多重度,再看组合关系、交互图、职责分配这些后续题目,会明显感觉到它们其实是同一个体系里的不同侧面。
2. 静态结构的语义链:多重度、组合与聚集在实现层怎么落地
2.1 多重度:读图先从“站在哪端看哪端”开始
UML的多重度标注的是当前这一端的目标对象可以有几个实例。读法永远是站在关联的某一端,看另一端允许出现的数量:1表示恰好一个,0..1表示零到一个,*表示零到多个,1..*表示一个到多个。试卷第3题给出了最干净的样板:一个订单由一个且仅由一个客户提交,所以站在订单望向客户,这个方向上是1,答案选B;一个客户提交0个或多个订单,站在客户望向订单,这个方向上是0..*,答案选D。同一对类、同一条业务规则,两个方向的多重度是分别定义的。很多人在这一步开始翻车,是因为把它当成了“客户一端的数字是几,订单端就反过来”,实际上这两个数完全可以、也应该从业务陈述里独立读出来。
多重度为什么在实现层是强约束,而不是画图时的装饰?因为对象模型最终要落成持久化结构和代码骨架。看两段常规的Java映射:
public class Customer { private String id; private List<Order> orders; // 对应 0..*:集合本身可为空 } public class Order { private Customer customer; // 对应 1:非空引用 private List<OrderItem> items; // 对应 1..*:至少一项 public Order(Customer owner, List<OrderItem> items) { if (owner == null || items.isEmpty()) { throw new IllegalArgumentException("订单必须归属一个客户且至少包含一件商品"); } this.customer = owner; this.items = items; } }0..*在面向对象代码里通常落成一个集合属性,集合可以是空;1落成非空引用;而1..*这种“至少一个”的约束,光靠List类型本身无法表达,必须借助构造器的校验逻辑来保证。这也是为什么很多建模评审会专门检查构造方法:类图画得再规范,如果代码里允许Order.items为空集合,多重度约束就等于被绕过了。把多重度想象成“对字段形态和校验规则的预写”,读题时就不容易看反。
试卷里“销售实例”那题的答案是a:1, b:1..*, c:1, d:1..*,抛开具体图例,它展示的正是领域模型里最常见的一组结构:一次销售对应一个顾客,销售明细从一条到多条,每条明细对应一个商品。多重度在领域模型阶段帮你把“聚合根”和“明细集合”先定下来,后续画SSD、写操作契约都以这组数量关系为基准。
2.2 组合与聚集:实心菱形和空心菱形,差的是生命周期
第9题要求辨认依赖、关联、泛化、实现四种关系,并指出聚集是关联的一种特殊类型;第2题则给出了一个更具体的判定场景:类A由类B的一个实例和类C的一个或多个实例构成。题干里的“构成”二字,对应的正是UML里的组合关系(Composition),一种比聚集更强的整体-部分关系。组合与聚集在图形上只差一个菱形是否实心,但语义差别很大:组合里部分与整体同生共死,整体被销毁时部分也随之一并销毁,部分不在整体之外独立共享;聚集则允许部分脱离整体继续存在。举例来说,一个订单和它的订单明细是组合,删除订单时明细也没了;一个班级和它的一名学生是聚集,班级解散后学生仍然是系统中的独立对象。
这个差别在代码里同样能看出痕迹。组合关系通常用“整体直接持有并构建部分”的方式实现:
public class Sale { private List<SalesLineItem> lineItems = new ArrayList<>(); public void addLineItem(ProductSpecification spec, int quantity) { lineItems.add(new SalesLineItem(spec, quantity)); // 商品明细由 Sale 内部创建 } }SalesLineItem没有对外暴露构造入口,它的生命周期被限制在Sale内部,这是组合的典型实现。而聚集关系在代码里往往表现为“部分对象在外部已经存在,整体只是持有一个引用”,比如向订单里添加一个已经存在的客户对象。判断一张图该画实心还是空心菱形,不需要死记定义,只看一个问题:删掉整体时,部分是否仍然有理由存在?这个标准在任何评审场景里都适用。
2.3 可见性与四种关系:画了线不代表能导航
第14题问什么是对象间的可见性,答案是“一个对象能够看到或引用另一个对象的能力”。很多人把可见性理解成Java的public/private,其实在分析和设计语境里,它讨论的是对象之间能不能建立引用通道。多重度告诉你“目标端能有多少个实例”,可见性则告诉你“从当前对象出发,能不能到达目标对象”。两者共同决定关联的导航方向。
UML里四种基本关系——依赖、关联、泛化、实现——本质上是四种不同强度的耦合方式。依赖最弱,通常表现为方法参数或局部变量里临时用到另一个类;关联较强,表现为对象之间长期持有的引用;泛化和实现则是类型层面的结构关系。聚集作为关联的一种特殊形式,在可见性上并没有额外要求,整体要导航到部分,同样需要有引用通道。换言之,图上画了一条关联线,代码里却看不到对应字段或方法参数,这条线就只是装饰。建模评审里最常见的追问之一就是“这个关联怎么导航”,它逼着你把静态图里的每一条线翻译成代码里真实的引用关系。
3. 动态行为的两种视图:顺序图与协作图为什么能互相转换
3.1 顺序图和协作图:同一套交互的两种投影
第4题和第72题反复比较顺序图与协作图的异同。顺序图强调的是交互的时间顺序,消息沿垂直时间轴依次排开;协作图强调的是对象之间的组织结构、连接关系以及参与者整体的空间布局。资源里的表述很准确:顺序图可以清晰表示消息之间的顺序和时间关系,但需要较多水平方向空间;协作图增加对象时比较方便、分支也少,但消息一多,顺序就不直观。这个“对偶”关系是理解两类图的关键:它们描述的是同一次交互,只是投影维度不同。顺序图把时间维度拉直,把对象摆在顶部;协作图把对象结构展开,用消息编号补偿丢失的时间信息。
协作图要表现时间顺序,靠的是一套消息编号。比如1: 调用A、1.1: 调用B、2: 调用C,其中1.1表示在消息1的处理过程中嵌套发生的子消息,数字的升序和缩进结构同时编码了先后与嵌套关系。这套编号不是装饰,它让协作图在没有时间轴的情况下仍然能精确还原调用序列。实际工作中,顺序图常用来表达方案、给团队讲清流程,协作图更适合在详细设计阶段描述对象间的静态连接与消息分布。
3.2 SSD图与用例图:从黑盒视角定义系统操作
SSD(系统顺序图)是容易被轻视的一张图。第51题专门问:画SSD时如何对待所涉及的系统?正确答案是“不对系统的内部结构与功能进行描述”。也就是说,SSD里的系统是一个黑盒,图中只出现外部参与者、系统边界以及它们之间的消息。第21题给出了经典场景:收款员启动一次销售makeNewSale()、输入商品标识enterItem(itemID, quantity)、结束销售endSale()、处理支付makePayment(amount)。这四条消息就是四次系统操作,它们不是内部调用,而是外部参与者直接触发的系统入口。
SSD与用例图的关系是递进的。用例图描述系统与外部参与者之间的交互,界定系统的责任范围;SSD则把某个用例里的关键步骤细化成一条条具体的系统操作消息。操作契约进一步描述每条系统操作执行后,领域模型里对象状态如何变化——哪些对象被创建、哪些关联被建立、哪些属性被更新。这三者构成一条完整的分析链条:用例定义了系统要做什么,SSD定义了系统对外暴露哪些操作,操作契约定义了操作执行后的领域状态。第72题里“根据系统操作规约画出顺序图和协作图”正是这条链路的下一跳:从黑盒的系统操作进入白盒的对象协作。
3.3 把交互图翻译成代码:消息变成方法调用
第31题和第72题都强调一个能力:读懂交互图,并配合其他信息将设计转化为代码。顺序图里从外部参与者指向系统的消息,在代码里就是控制器对象暴露给UI层或入口层的公开方法;对象之间的内部消息,则是方法内部对其他对象的调用。以一卡通POS场景为例:
public class Register { private Sale currentSale; public void makeNewSale() { currentSale = new Sale(); // 消息 1:创建一次销售 } public void enterItem(String itemId, int quantity) { currentSale.addLineItem(catalog.find(itemId), quantity); // 消息 1.1、1.2 } public Money endSale() { return currentSale.getTotal(); // 消息 3:系统计算并返回总金额 } }enterItem内部先是catalog.find查商品,再是currentSale.addLineItem加明细,对应协作图里编号1.1、1.2的嵌套消息。顺序图的时间轴在代码里表现为调用发生的先后次序,协作图的编号则表现为调用栈的压栈层级。能看到这一层,交互图就不再是一堆画着箭头的方框,而是代码执行流的“预览图”。这也是为什么很多设计评审要求先画SSD、再画顺序图、再写代码:每一步都在为下一步提供可追溯的输入。
4. 职责分配:GRASP五个模式如何把分析图变成设计决策
4.1 GRASP五模式:每个模式都在回答一个问题
GRASP(General Responsibility Assignment Software Patterns)是职责分配领域的原则性模式。第24题列出的五个模式,每一个都对应一个具体的追问:
| 模式 | 核心问题 | 典型使用 |
|---|---|---|
| 信息专家 | 分配职责给对象的基本原则是什么 | 让持有数据的对象负责计算,比如Sale计算总额 |
| 创建者 | 谁负责创建某个类的新实例 | 聚合、记录或密切使用某类的对象负责创建它 |
| 低耦合 | 如何支持低依赖关系并增加重用 | 通过接口交互,减少对具体类的引用 |
| 高内聚 | 如何把复杂性保持在可控范围 | 一个类的职责高度相关,且工作量不过大 |
| 控制器 | 谁来负责处理一个输入系统事件 | 用Register接收外部消息,UI不直接操作领域对象 |
这五个模式不是并列的“招式”,而是一套决策顺序:先看谁拥有完成职责所需的信息,信息专家优先;如果职责是“创建新对象”,再考虑创建者;同时用低耦合和高内聚约束方案不要引入不必要的依赖和复杂度;最后由控制器统一接收外部系统事件,避免让UI对象直接承担业务职责。GRASP的答案不是唯一的,它提供的是判断标准,而不是固定模板。
4.2 信息专家与控制器:两个最容易混用的模式
信息专家和控制器经常被放在一起比较,因为它们都涉及“把活儿派给谁”。信息专家解决的是“这个职责应该给哪个对象”——答案通常是掌握最多相关信息的对象。一个销售总额的计算应该给Sale,因为Sale拥有所有行项目及价格信息;如果硬塞给窗口类,窗口就不得不暴露Sale的内部数据,耦合立刻上升。控制器解决的是另一个问题:“谁来接收和处理输入系统事件”。在POS场景里,收款员点击按钮触发的事件应该交给Register这个控制器对象,而不是让窗口自身的监听器去计算总额、操作销售数据。
从第21题那条makePayment(amount)消息可以看出控制器的位置:窗口接到UI事件后,只做一件事——把参数传给控制器的公开方法,后续的领域逻辑全部发生在领域对象之间。这样当交互图转换成代码时,窗口很薄、领域很厚,测试时可以绕过UI直接对控制器发消息。这才是“控制器”模式真正的收益:系统事件的入口统一,UI与领域模型解耦。
4.3 从GRASP到GoF:一个分配职责,一个应对变化
第35题考察单例、策略、组合、观察者、适配器等设计模式的理解;第52题则提醒不要把这些模式混入GRASP。两者的层次不同:GRASP是职责分配的元原则,回答“职责该给谁”;GoF模式是经过验证的解决方案,回答“某个变化点怎么封装”。策略模式把一组算法抽成公共接口,使它们可以互相替换;适配器模式用一个中间对象把不兼容的接口转换成目标接口;单例模式通过静态方法保证一个类只有一个实例;观察者模式在对象状态变化时自动通知所有依赖者;组合模式让原子对象和组合对象实现同一接口,从而统一处理。
这些模式并不是凭空出现的,它们是在GRASP原则指导下逐步形成的惯用方案。策略模式背后是“把变化的算法隔离出去,降低耦合”;适配器模式背后是“用间接层消化接口差异”。理解这一层之后再看第35题的答案结构,就会发现每一条都遵循同一个句式:先定义问题,再给出接口与实现分离的机制。GRASP告诉你为什么这么分,GoF告诉你具体怎么分,二者前后衔接,缺一不可。
5. 避坑排查:UML解析题里最典型的五类失分点
考点密集的复习资料,本身就是一份极好的“踩坑清单”。这里挑出五个最高频的问题,每个都按“现象 → 原因 → 解决”拆开讲。
5.1 多重度方向读反
现象:遇到“客户与订单”这类配对题,先写了(46)的D、(47)的B,把订单和客户的数量关系完全写反。图上也看不出错,两条线带两组数字,怎么标都像那么回事。
原因:把多重度当成了倒数的对称关系,以为“一端是多个另一端就必须是一个”。实际上多重度是分别定义的,每个方向都要回到业务陈述里独立确认:站在订单看客户,一个订单对应几个客户;站在客户看订单,一个客户对应几个订单。题干里说得清清楚楚,读的时候把视角标出来就不会混。
解决:拿到关联先画一张这类辅助表:
def check_multiplicity(count, spec): # spec 形如 "1"、"0..1"、"*"、"1..*" if spec == "1": return count == 1 if spec == "0..1": return count in (0, 1) if spec == "*": return count >= 0 if spec == "1..*": return count >= 1 return False把多重度当成一条可在运行时验证的规则来写,每个方向的含义就不会被直觉带偏。做题时把“站在哪端看哪端”这七个字写在草稿纸一角,比反复背选项有效得多。
5.2 协作图说不清时间顺序
现象:问“协作图如何表示消息的时间顺序”,答案里写了“箭头方向”或“布局位置”,拿不到分。协作图没有垂直时间轴,箭头只能表示消息传递方向,画出来也看不出谁先谁后。
原因:把协作图当成了顺序图的简化版,忽略了它自己的顺序编码机制。协作图用消息编号来表达时间,1:、1.1:、2:这种编号既体现先后也体现嵌套。
解决:看到协作图先找编号最小的消息,那就是整个交互的起点;带点号的编号如1.1是在1的处理过程中发生的子调用。复习时可以直接做一次“协作图转顺序图”的练习:按编号从主消息到子消息展开成时间轴,画对了就去掉了整个知识盲区。
5.3 用例图里掺入系统内部行为
现象:画“打电话”用例图(第12题)时,把“验证剩余金额”“连接通话线路”这类系统内部功能也画成用例。案子看起来完整,但评审一眼就看出这不是用例图。
原因:用例图描述的是系统与外部参与者之间的交互,视角在系统边界之外;系统的内部步骤应该留在用例文档或后续设计中,而不是平铺成用例。
解决:画图时只问一个问题:这个行为是否由外部参与者直接触发,并且对参与者产生可观测的结果?“使用电话卡”和“对方付款”都满足,所以它们是用例;“验证剩余金额”不满足,它只是用例内部的一步。第48题对用例、场景、参与者的定义就是这套判据的来源。
5.4 领域模型混入软件部件
现象:按第20题的场景找概念类时,写出了“数据库”“窗口”“按钮”这类软件成分。领域模型看起来像是设计模型的草稿,实际已经偏离方向。
原因:领域模型的目标是“真实世界的概念类”,第15、44题反复强调这一点;它是对业务词汇的抽象,而不是对软件结构的预览。
解决:判断标准很简单——概念的来源是不是业务语言。顾客、商品、销售都是业务里真实存在的词;而“窗口”“数据库”只存在于软件系统内部。如果候选概念必须用技术词才能命名,它就不是概念类。第50题把领域模型称为概念模型,提醒的正是这层定位。
5.5 GRASP答案写成GoF
现象:第52题问哪个模式不属于GRASP,答成了某个GoF模式名;或者反过来,让举GRASP例子时写了单例、策略。
原因:两类模式都在讲“设计”,但层次不同。GRASP是职责分配的原则,回答的是“职责给谁”;GoF是具体变化的解决方案,回答的是“变化如何封装”。单例、策略、适配器都属于GoF体系,而信息专家、创建者等属于GRASP。
解决:背诵时用一个格式分开记:GRASP看“职责+对象”,GoF看“问题+方案”。做题时看到“分配职责给对象的基本原则”“谁负责处理系统事件”这类题眼,直接锁定GRASP;看到“接口不兼容”“算法频繁变更”这类描述,才考虑GoF。
6. 设计取舍:迭代建模与开闭原则,何时该停止细化
统一过程把开发切成初始、细化、构造、提交四个阶段,第41题说得很直接:定义大多数需求和范围的工作在细化阶段完成,而不是初始阶段。这个安排本身就藏着一个设计取舍:初始阶段只编制简要愿景、评估成本、确定范围,不急着把需求钉死;细化阶段通过迭代实现核心架构、解决高风险问题,需求才逐步定型。为什么大多数需求不放到最前面一次性定义?因为早期的信息一定是不完整的,过早把所有需求写死,后面每发现一次偏差都要大规模返工。把“定义大多数需求”放到细化阶段,就是用一次次迭代的反馈来校准方向,代价是前期文档始终处于“足够但不完美”的状态,要求团队能接受这种模糊。
开闭原则讲的是“对扩展开放、对修改关闭”,听起来像一句口号,实际上它是GRASP低耦合与高内聚在演进层面的延伸。一个类的职责足够内聚、依赖足够少时,新增行为就可以通过新类或新策略接入,而不是改动已有实现。第22题把它放进设计过程,用意就在于此:静态结构的多重度、动态行为的交互图、职责分配的GRASP,最终都指向同一个目标——让系统在持续修改中保持稳定。
所以建模的深度该停在哪儿,成了一个实践问题。我的习惯是:一类图画到能支撑下一步决策就停。领域模型画到能导出聚合根和多重度就停,SSD画到能列出系统操作就停,顺序图画到关键消息能直接对应方法调用就停;再往下的细节交给代码评审去修正,而不是在图上继续叠加。否则图会变成第二套代码,反而需要和维护代码一样去维护它。从那以后,我拿到每个需求的顺序图,都会先追一遍“这条消息是不是来自用例步骤、能不能对应一个系统操作”,多重度也强制按“站在哪端看哪端”去读。这几件事反复做下来,分析与设计的衔接会顺畅很多。希望帮到你。
本文还有配套的精品资源,点击获取