UML建模复习题精讲:多重度、交互图与设计原则 简介面向UML系统分析与设计课程复习与考试准备这份试题文档围绕统一建模语言在系统分析与设计中的应用展开适合高校学生、备考者以及需要梳理面向对象建模知识的开发者使用可用于课后自测、考前查漏补缺与知识点串联。压缩包内共1个doc文件约132KB内容以试题、参考答案与解析为主体量轻便便于直接查阅和打印复习已有406人学习。文档覆盖关联多重度、类之间的组合关系、客户与订单的数量对应、顺序图与协作图的差别、高内聚、类图与用例图的作用、依赖/关联/泛化/实现等关系以及可见性、领域模型、GRASP模式、SSD图等常见考点并配有答案与简要分析能帮助读者对照题目理解建模概念、定位薄弱环节形成较完整的复习脉络。1. 一句话说清 UML它其实是给软件画的施工图纸做系统分析最难受的时刻往往不是写不出代码而是几个人对同一段业务各说各话产品说「一个客户能下好几单」开发脑子里画成「一单挂好几个客户」等测试发现问题返工成本已经翻了几倍。UML统一建模语言就是为掐掉这类歧义出现的——它是一套图形化的标准符号把软件长什么样、怎么动、谁跟谁有关系用统一画法表达出来。它不绑定编程语言也不规定框架只负责把结构和行为说清楚。搞懂它等于拿到软件设计领域的通用图纸语言看别人的图不用猜画自己的图不怕被误读。2. 多重度那串数字0..1 和 1..* 到底在数什么2.1 为什么一个「一对多」能让人纠结半天业务陈述只有一句话「一个客户提交 0 个或多个订单一个订单由一个且仅由一个客户提交。」听起来毫无难度但落到图上很多人会写反。原因不在概念难而在自然语言天然是「双方向」的同一句话里其实藏着两条独立信息一条从客户看订单一条从订单看客户。多数人只翻译了自己顺手的那个方向另一个方向凭感觉补补错的概率相当高。更麻烦的是多重度写在关联线两端位置本身不带箭头含义数字靠近哪一端描述的却是「另一端的一个实例对应这一端多少个实例」。这个「站在对面数自己」的视角切换是初学者翻车的重灾区。不少人第一次画类图就是在这条线上被导师圈了三个红圈。打个比方多重度不是给类贴标签而是给「一次具体的配对」写规则。就像电影院检票一张票对应一个座位一个座位只属于一张票——你要说清的是「一张票能坐几个座」而不是「票这种东西有几个」。2.2 从业务句子翻译成多重度先单向后双向稳妥的做法是把一句话拆成两个方向分别翻译最后互相校验。以客户和订单为例方向一一个客户能对应几个订单业务说「0 个或多个」于是订单那一端写0..*。方向二一个订单能对应几个客户业务说「一个且仅有一个」于是客户那一端写1。两个方向都写完了再倒着念一遍「每个订单恰好属于一个客户每个客户拥有零个或多个订单。」如果念出来和原句意思一致多重度就对了。这个「正着写、倒着念」的动作看着笨却是最省时间的自查方式比事后被人指出强得多。常见写法与含义对照如下写法读法典型业务1恰好一个订单必须属于一个客户0..1零个或一个员工最多挂一个工位1..*一个或多个一个订单至少含一件商品0..*或*零个或多个客户可以没有订单2..4两到四个一辆车配 2 到 4 个轮子注意1..*和0..*的差别不是书写习惯而是业务约束前者意味着「不允许为空」后者意味着「允许为空」。这两种情况在数据库约束、界面校验、接口返回值上会演化成完全不同的实现别随手写成*蒙混过去。2.3 关联、聚合、组合几种「有关系」的强弱层次UML 里最常被提到的关系有依赖、关联、泛化、实现四种聚合被视为关联的一种特殊形态组合又是聚合的强化版。它们的区别不在画法花哨而在语义强度。依赖是「用一下就走」一个类的方法参数、局部变量、返回值里出现了另一个类编译期需要它运行期不一定长期持有。关联是「长期知道对方存在」通常落成成员变量。聚合是「我有一部分但这部分可以独立活着」比如一个团队有若干成员团队解散了成员还在。组合是「我有一部分我没了它也没了」订单被删除订单项没有独立存在的意义。关系画法语义代码里常见落点依赖虚线箭头临时使用方法参数、局部变量关联实线可带箭头长期持有成员变量聚合空心菱形整体-部分部分可独立成员变量外部传入组合实心菱形整体-部分同生共死成员变量整体内部创建泛化实线空心三角is-a 继承类继承实现虚线空心三角接口实现实现接口题目里那句「类 A 由类 B 的一个实例和类 C 的一个或多个实例构成」关键词是「构成」。构成意味着 A 的存在依赖 B、C 这两块零件画成组合更贴切A 端不写多重度B 端写1C 端写1..*菱形贴在 A 这一侧。下面这段代码只是把「组合」的语感落成可读的形式帮助建立直觉并非 UML 规范的等价物class LineItem: def __init__(self, sku: str, qty: int): self.sku sku self.qty qty class Order: def __init__(self, order_id: str): self.order_id order_id self.items [] # 组合Order 没了items 也没有存在意义 def add_item(self, sku: str, qty: int): # 部分由整体负责创建外部拿不到独立生命周期 self.items.append(LineItem(sku, qty))关键点有两处items是成员变量体现「长期持有」LineItem在Order内部被创建外部无法单独管理它的生死这才叫组合。如果改成构造函数把一组LineItem传进来、外部还留着引用那语义就滑向聚合了。2.4 照着关系画类图的判断顺序拿到一段业务描述按这个顺序落笔比上来就画框稳得多先找名词圈出候选的概念类再看这些名词之间谁包含谁、谁引用谁然后逐个方向确定多重度最后才决定用哪种关系符号。顺序颠倒的典型后果是——先画了一堆继承箭头回头发现根本没那么多 is-a 关系只是「有个字段引用了对方」。一个实用判断法问自己「删除整体时部分还能不能单独存在」。能聚合不能组合。再问「这个关系是永久的还是调用期间才有」调用期间才有就是依赖。三个问题问完符号基本不会错。3. 顺序图与协作图同一段交互的两种排版方式3.1 为什么同一段交互要画两张图交互图描述的是对象之间怎么发消息、按什么次序发。UML 里承担这个任务的主要是顺序图和协作图它们能表达的信息高度重叠甚至可以相互转换。既然能转为什么还要两种因为人看图时的关注点不同。排查「第三步到底在第二步之前还是之后」你要的是时间和次序讨论「这个功能牵扯到哪几个对象、它们的结构关系合不合理」你要的是组织和连接。同一份材料横着排和竖着排读起来的感觉完全不一样。这就像一份会议的记录可以按时间写流水账也可以按参会人分组写发言摘要信息不丢但用处不同。考试里这两张图几乎是必考项尤其是协作图经常以「书上例题改一改」的形式出现。搞不清它们的分工画图时就会把编号和时间轴混着用最后画出一张既不像顺序图也不像协作图的东西。3.2 顺序图竖着看是时间横着看是对象顺序图的结构很规整顶部一排对象或参与者每个下方垂一条生命线生命线上有激活条表示对象正在处理消息用水平箭头在生命线之间穿梭。箭头的上下位置就是时间先后越靠上越早。它的优势是「读一遍就懂流程」谁先发起、谁响应、哪一步是同步等待、哪一步有返回值全部按阅读习惯自上而下铺开。做方案评审、给不熟悉系统的人讲业务流程顺序图几乎是最省口舌的选择。代价是占地方。对象一多生命线就挤成一片消息一多箭头来回穿插读图的人得拿尺子比着看。一段涉及七八个对象、二十几条消息的交互画成顺序图往往会横向撑满一整屏还得配编号辅助。这也是它在复杂场景下容易被嫌弃的原因。3.3 协作图横着看是关系网顺序藏在编号里协作图后来的版本里也叫通信图把对象画成一个个框框与框之间用链连接消息贴着链走前面带序号。时间信息不在位置上而藏在编号里。1: makeNewSale() # 收款员启动一次销售 2: enterItem(itemID, qty) # 输入商品标识与数量 3: endSale() # 结束销售计算总额 4: makePayment(amount) # 处理支付这段编号就是协作图的骨架。读图时先看1:在哪条链上再看2:依此类推。嵌套调用会写成2.1、2.2这样的层级编号表示在第二个消息内部又发生了两次调用。它的长处正好补顺序图的短增删对象方便分支少结构关系一目了然尤其适合展示「哪些对象是核心、它们之间有几条通路」。短处也明显——消息一多编号就得来回找7和8谁先谁后得盯着数字看不像顺序图那样一眼扫下来就清楚。3.4 一张对比表说清取舍与转换维度顺序图协作图强调重点交互的时间顺序对象的组织结构与连接布局方式按时间自上而下按空间关系铺开时间信息位置即顺序靠消息编号体现增加对象横向空间吃紧相对容易消息很多时箭头密集但次序清楚编号众多次序需逐个追典型用途表达方案、讲流程详细设计、看整体结构转换规则不复杂顺序图里每条消息按先后编号把生命线换成对象框、把消息挂到对应的链上就得到协作图反过来按编号顺序把消息还原成自上而下的箭头就回到顺序图。真正的功夫不在转换本身而在判断当前该用哪一张——讲方案用顺序图定结构用协作图这大概是实践里最省事的分工。需要留意一个细节顺序图里对象之间的关系往往被弱化你只能看到消息在生命线之间飞看不到它们为什么能通信。协作图则必须把链画出来所以它天然会逼着你思考「这两个对象到底认不认识」。做详细设计时这种「被迫交代关系」的约束反而是好事。4. 五组最容易混的概念用例、领域模型、SSD 到底差在哪4.1 用例和用例图一个是文档一个是画易混点在于很多人把「用例」当成图上的一个椭圆。实际上用例是一份描述性文档讲的是一个参与者使用系统完成某个业务过程时事件发生的先后顺序用例图只是把这些用例、参与者以及它们之间的关系画出来的一张索引图。之所以混是因为口语里常把「画个用例」和「写个用例」混着说。区分方法很简单图回答「系统有哪些功能、谁能用」文档回答「这个功能具体怎么走、什么情况下算成功」。一份用例文档里通常会写主成功场景、扩展场景、前置条件这些内容在椭圆里一个字都放不下。4.2 领域模型和类图概念类和软件类不是一回事领域模型描述的是真实世界里的概念类它不管数据库怎么建、接口怎么设计只关心业务里有哪些事物、它们之间是什么关系。类图描述的是软件里的类带方法、带可见性、带实现细节。混的原因在于两者画法高度相似都是方框加连线都能标多重度。区分看内容领域模型里的方框通常只写名字和业务属性不会出现saveOrder()这种带动作的方法一旦方框里开始出现、-、#和一堆方法签名它就已经滑向设计类图了。领域模型是「启发设计用的草图」类图是「要拿去写代码的施工图」。4.3 顺序图和系统顺序图一个画对象一个把系统当黑盒顺序图里的参与者是系统内部的对象消息在它们之间流动。系统顺序图SSD只画外部参与者与系统本身系统被整体画成一个黑盒消息是外部输入的系统事件比如enterItem(itemID, quantity)。混的原因是两者外形几乎一样都是一条生命线加水平箭头。区分看对象数量SSD 里只有一个代表系统的框其余全是外部参与者一旦你开始在里面画「订单」「库存」这些内部对象它就不再是 SSD 了。SSD 的用途是先把「系统对外要接收哪些事件」定下来不碰内部结构这个「不描述内部结构与功能」的纪律恰恰是它最大的价值。4.4 参与者和用户角色不是具体的人参与者是角色一个具体的人可以同时扮演多个参与者一个参与者也可以由多个人或多个外部系统来扮演。用户是现实中的具体个体。混的原因很直白——日常说话里两者是同一个词。区分方法参与者永远站在系统边界之外描述的是「谁在用系统做什么」不描述这个人的姓名、职位、部门。给系统做权限设计时才需要落到具体身份而参与者建模阶段只需要角色。4.5 状态图和活动图一个盯对象一个盯流程状态图描述一个特定对象的所有可能状态以及事件发生引起的状态迁移。活动图描述需要做哪些活动、按什么顺序做可能有分支和并发。混的原因是两者都画圈圈和箭头都能表示「从 A 到 B」。区分看主语状态图的主语是某一个对象回答「这个订单现在处于什么状态」活动图的主语是一段工作流回答「这件事分几步做完」。判断口诀是主语能回答「它现在是什么状态」用状态图主语能回答「下一步做什么」用活动图。5. 高内聚低耦合到 GRASP 与 GoF职责归谁和招式怎么套5.1 内聚与耦合两把听起来玄学、其实很好判的尺子高内聚度是对一个类内部各职责之间相关程度和集中程度的度量。一个类承担的职责高度相关、并且总工作量不夸张就是高内聚。它包含两层意思别把太多职责压给同一个类不相关的职责不要塞进同一个类。低耦合说的是类之间的依赖尽量少、尽量弱。两个类牵一根线没问题牵十根线、还互相调用对方内部细节就是耦合过重。这两把尺子其实很好判用两个问题就行这个类里的方法是不是都在服务同一件事改一个类的内部实现会不会顺带把另外三个类改一遍前一个问题的答案偏否定说明内聚不足后一个问题的答案偏肯定说明耦合过高。把这套判断说成玄学的人多半是没试过拿它去量具体的类。5.2 GRASP五个基础模式解决的都是「职责往哪放」GRASP 是一组分配职责的通用原则常被提到的五个各有明确要解决的问题模式解决的问题信息专家分配职责给对象的基本原则是什么创建者谁负责创建某个类的新实例低耦合如何支持低依赖关系并增加重用高内聚如何把复杂性控制在可管理的范围控制器谁来负责处理一个输入的系统事件它们不是代码模板更像五条决策规则。比如「信息专家」说谁掌握的信息最多就把这项职责分给谁。算订单总价订单自己知道所有明细那就让订单算别搞一个OrderCalculator到处伸手问数据。「创建者」说谁包含或聚合了对方、谁记录了对方、谁频繁使用对方就由谁负责创建。「控制器」说把界面层收到的系统事件先交给一个控制器对象由它协调后续动作避免界面直接操作一堆业务对象。这五个模式的价值在于把「凭感觉分」变成「按规则分」。分完之后再用内聚和耦合两把尺子量一遍多数设计问题在纸上就能暴露出来。5.3 GoF解决的是结构怎么搭不是职责怎么分设计模式是经验丰富的面向对象设计者沉淀下来的通用解决方案按问题和解决方案的结构形式整理成型并各自命名。单例解决「如何让一个类严格只有一个实例」策略解决「算法或规则频繁变更怎么适应」组合解决「希望按处理原子对象的方式处理组合对象」观察者解决「一个对象状态变化时如何通知所有依赖它的对象」适配器解决「接口不兼容怎么转换」。GRASP 和 GoF 常被放在一起问但它们不在一层前者是职责分配的判断原则后者是可复用的结构方案。一个偏「想清楚」一个偏「搭出来」。下面两段代码是把其中两个模式的语感落地的样子class AppConfig: _instance None def __new__(cls): # 单例整个进程只保留一个实例配置只读一份 if cls._instance is None: cls._instance super().__new__(cls) return cls._instanceclass PricingStrategy: def price(self, base: float) - float: raise NotImplementedError class NormalPricing(PricingStrategy): def price(self, base: float) - float: return base class DiscountPricing(PricingStrategy): def price(self, base: float) - float: return base * 0.8 # 折扣规则变了只改这个类 class Checkout: def __init__(self, strategy: PricingStrategy): self.strategy strategy # 策略可替换调用方不关心具体算法 def total(self, base: float) - float: return self.strategy.price(base)第一段的关键是__new__里的判空实例只造一次后续调用直接返回已有的那个。第二段的关键是Checkout只依赖抽象接口换计价规则时不用动total方法。这两处的共同点是「把变化挡在外面」只是单例挡的是实例数量策略挡的是算法分支。6. 这套复习题什么时候翻什么时候别指望它先说适合的场景。它最对口的用法是课程复习和考前梳理题目按知识点切得很碎多重度、交互图、UP 四阶段、GRASP、GoF 这些高频考点被反复以选择、填空、简答的形式出现做一遍能很快把术语和自己的理解对上号。第二个场景是面试前的术语对齐很多人能写出代码但被问到「顺序图和协作图的区别」「高内聚指什么」时说不利索翻一遍这类题能快速把口语化理解换成规范表述。第三个场景是画图前的自查拿多重度那几道题当检查清单画完类图逐个方向念一遍能挡掉不少低级错误。再说别指望它的地方这条更重要。它不能当唯一教材——题目是碎片化的覆盖的是考点而不是知识体系遇到「为什么这样设计」的追问光靠答案撑不住。它也不是软件操作手册不会教你在哪个工具里拖哪个图形。最要命的一点是它替代不了真实项目的练习把二十道多重度题做对和面对一段含混的业务描述、跟三个人讨论出唯一正确的多重度是两种能力。场景适合程度说明考前突击知识点高题型覆盖密集查漏效率高面试前梳理术语高简答题的表述可直接借用画图前自查中高适合当检查清单不适合当模板系统学习建模方法低缺前后脉络需要配合教材替代项目实践低题目是简化过的业务歧义被抹平了有个判断标准可以借走如果你能对着答案说出「为什么错的那三项不对」这套题对你就有效如果只是记住「这题选 B」那它顶多帮你多拿几分帮不了你建立认知。我自己有过一次典型的踩坑。早年画类图总是先打开工具拖方框边拖边想关系结果一张图改到第八版还是不对劲导师看了一眼说「你这多重度全是凭感觉写的吧」。从那以后我每次画类图前都强制走一遍固定动作先用一句话写下业务规则再拆成两个方向各自标多重度倒着念一遍确认没有歧义最后才决定用关联还是组合。这个习惯看着慢实际上省掉了大量返工。这套复习题的价值也在这儿——它把容易含糊的地方一道道摆出来逼你把「凭感觉」换成「按规则」。需要完整题目和答案的话资料已经整理好了拿去对着过一遍比零散搜答案省事得多。希望帮到你。本文还有配套的精品资源点击获取