☰
软件设计师下午题:面向对象、UML与设计模式一体化通关
2026/10/1 3:59:11 网站建设 项目流程

软件设计师这门考试有个挺有意思的现象:上午题拼的是记忆面的宽度,下午题拼的是手上功夫。而所谓手上功夫,八成集中在面向对象、UML、设计模式这三块上。我见过不少人上午题能考到五十多分,下午题却卡在四十分出头,翻来覆去就是类图画不对、多重度标不准、代码填不进去——问题不在知识量,在于没把这三块当成一个整体来练。

这篇内容就是围绕这条主线展开的:面向对象是思维方式,UML 是表达这种思维的图纸语言,设计模式是前人总结出的成熟图纸模板。三者是递进关系,不是三门并列的课。我会从考试构成出发,把 OO 三特性落到真实代码里讲透,把用例图、类图、顺序图、状态图、包图的判定规则和扣分点一条条摆出来,再挑出几类真正高频的设计模式讲识别信号,最后给我自己的备考节奏安排。适合正在啃中级软件设计师、或者准备设计模式期末和大作业的人参考,也适合已经会写代码但没系统画过 UML 的同学补课。

1. 从真题倒推:软设下午题为什么总绕着这三块转

1.1 试卷结构决定了复习的重心在哪里

中级软件设计师的下午卷是五道必做加一道选做,总分七十五分。必做题里,数据流图、数据库设计、UML 图、算法与数据结构(C 语言描述)各占一道,最后一道是 Java 或 C++ 的选做,题干通常给一段带空缺的代码加一张残缺的类图。你会发现,UML 那道题和最后的选做代码题,本质上考的是同一套东西——只不过一个让你画,一个让你写。

这就是为什么我不建议把"面向对象""UML""设计模式"当成三个独立模块去背。你画的那张类图,就是设计模式的骨架;你填的那几行代码,就是类图的落地实现。考试设计者本来就想让它们互相印证。

我统计过自己刷过的近十年下午真题,UML 那道题出现频率最高的图形是类图和用例图,顺序图和状态图次之,包图、活动图偶有出现。选做代码题里,出现过的模式集中在策略、工厂方法、抽象工厂、观察者、模板方法、装饰器、单例、适配器这几类,冷门的解释器、访问者几乎没单独考过。这个分布直接决定了你的复习投入比例,而不是把二十三种模式平均用力。

1.2 大多数人复习顺序是反的

一个很常见的做法是这样:先买一本知识点总结,把"封装继承多态""UML 九种图""二十三种设计模式"挨个背一遍,然后再去做题。背的时候感觉都懂,一做题就发现——题干说"系统需要支持多种支付方式,且未来可能新增",你知道这是要上策略或者工厂,但类图上那个菱形到底该画空心还是实心,就卡住了。

更合理的方向是倒过来:先做两三套下午真题,暴露自己真正卡在哪个环节,是读不懂需求文字,还是不知道该画哪种图,还是关系标注拿不准。带着这些具体问题再回去补知识点,记忆效率完全不一样。我自己第一次做下午题时,最大的障碍其实是"不知道题干里哪句话对应哪个类",跟背不背设计模式关系不大。

1.3 把三块内容串成一条主线的判断标准

我给自己定了一个很朴素的自测标准:拿到一段三百字的需求描述,能不能在十五分钟内产出一张有类名、有属性、有方法、有关系标注(含多重度)的类图,并且能说清楚每个关系为什么这么标。能做到这一步,UML 那道题基本稳了;再能把类图里的抽象类、接口位置指出来,说明你已经具备了选做代码题的骨架能力。

这条标准背后其实是三块知识的合流:读需求靠面向对象的抽象能力,画图靠 UML 的语法,而类与类之间怎么组织才合理,靠的是设计模式积累的套路感。所以后面的章节我会按这条线走,先讲思维,再讲图纸,最后讲套路。

2. 面向对象:把封装、继承、多态放进真实代码里理解

2.1 类与对象:数据和行为绑在一起才有意义

教科书上会说"类是对具有相同属性和行为的对象的抽象",这句话没错但太干。换个说法:类就是一张表格模板,对象就是按这张模板填出来的具体一行。为什么要把数据(属性)和行为(方法)绑在一起?因为一旦分开,数据就可能被任何代码随意修改,出了问题你根本找不到是谁改的。

看个反例就很清楚。假设用最朴素的写法处理一个银行账户:

balance = 1000 def withdraw(amount): global balance balance -= amount

任何一段代码只要写一句balance = -99999,整个账户体系就废了,因为余额是裸露在全局的。改成面向对象的写法:

class Account: def __init__(self, balance): self.__balance = balance def withdraw(self, amount): if amount <= 0: raise ValueError("金额必须为正") if amount > self.__balance: raise ValueError("余额不足") self.__balance -= amount def get_balance(self): return self.__balance

余额被双下划线修饰成了私有属性,外部只能通过withdraw和get_balance访问。所有对余额的修改都必须经过这两个方法,校验逻辑就只有一份。这就是封装的实际价值——不是"把变量藏起来"这种形式动作,而是"把所有修改入口收拢到一个地方,方便加规则、方便排查问题"。

2.2 继承不是复用代码的万能钥匙

继承看起来很诱人:class SavingsAccount(Account)一写,父类的方法全归子类了。但继承建立的是 is-a 关系,是最强的耦合。父类改一个方法签名,所有子类都得跟着改。需求一变化,继承层次就容易长成三层四层的怪物。

所以有个合成复用原则(也常被叫做"组合优于继承"):能用组合(has-a)表达的关系,就别用继承。举个例子,"汽车有发动机"应该是组合,"货车是车"才是继承。

判断方法很直接——造个句子读一读。说"学生是一个人"通顺,那就是继承;说"班级是一个学生"不通顺,那就不是继承。考试里选项往往会给"用继承实现 A 和 B 的关系",如果 A 和 B 是整体部分关系,那正确答案基本就是聚合或组合。

2.3 重写和重载,两个中文只差一个字的概念

这是下午题里最容易混的一处,必须掰清楚。

重写(override)发生在父子类之间,子类重新实现父类已有的方法,方法名、参数列表、返回类型都一样。它靠的是运行时的动态绑定,也就是说,编译时看的是父类引用,运行时才决定调哪个版本。这就是运行时多态。

重载(overload)发生在同一个类里,方法名相同但参数列表不同。编译器在编译阶段就能根据实参类型确定调哪个,属于编译时多态。

class Shape { public double area() { return 0; } } class Circle extends Shape { private double r; public Circle(double r) { this.r = r; } @Override public double area() { return Math.PI * r * r; } } class Calc { public double sum(double a, double b) { return a + b; } public double sum(double a, double b, double c) { return a + b + c; } }

Circle的area()是重写,Calc的两个sum是重载。考试里描述成"同一消息可以根据发送对象的不同采用不同的行为方式",说的就是重写带来的运行时多态。

多态真正的用处在于把变化挡在调用方之外。上层代码只写for (Shape s : shapes) total += s.area();,将来新增三角形、矩形,这行循环一个字都不用改。这才是设计模式大量依赖多态的根本原因。

2.4 抽象类与接口:选择哪一种

抽象类表达的是一种"半成品"——它既可以有已实现的方法,也可以有纯抽象方法,子类只能单继承。接口表达的是一份"能力契约"——只声明不实现(Java 8 之后允许默认方法),一个类可以实现多个。

选哪个的判断标准:如果多个类共享同一套状态和部分实现,用抽象类;如果只是约定一组行为,用接口。设计模式里,工厂方法常配抽象类,策略、观察者、命令这类常配接口,原因就是后者只需要"能执行某件事",不需要共享状态。

2.5 三种语言写法的对照

软考下午选做题是 Java 和 C++ 二选一,但很多学校的课程和大作业会用到 Python、C#。我整理了一张对照表,主要看访问控制、继承和接口的写法差异:

概念JavaC++PythonC#
私有成员privateprivate:__name(名字改编)private
继承extendsclass B : public Aclass B(A)class B : A
接口interface纯虚函数抽象类抽象基类(abc)interface
方法重写标记@Overridevirtual+override直接重定义override
抽象方法abstract= 0@abstractmethodabstract

Python 里没有真正意义上的私有,__balance只是被解释器改成了_Account__balance,属于约定俗成的保护。搞清这一点,看 Python 实现的设计模式代码时就不会被"为什么还能访问"绕住。

3. UML 图:考场上画得对只是及格线,画得快才是优势

3.1 用例图:include 和 extend 的判定套路

用例图考的次数不算最多,但一旦出现,扣分点几乎全在 include 和 extend 的方向上。判断方法其实很机械:

  • include(包含):多个用例里重复出现的、必然执行的一段公共行为,被抽成一个独立用例。箭头从基础用例指向被包含用例,标注<<include>>。
  • extend(扩展):在特定条件下才发生的、可选的行为。箭头从扩展用例指向基础用例,标注<<extend>>。

拿图书馆系统举例。"借书"这个用例,每次都要先验证读者身份,验证是必然发生的公共步骤,所以"借书"<<include>>"验证读者身份"。而"缴纳逾期罚款"只在读者有超期图书时才发生,属于可选扩展,所以"缴纳罚款"<<extend>>"借书"。方向搞反是最高频的错误。

参与者还分主参与者(主动发起)和次参与者(被动响应),比如"借书"里读者是主,图书管理系统对接的支付网关是次。题干里出现"由...触发""需要...配合完成"这类词,就要留意区分。

3.2 类图:六种关系的表示法和语义

类图是重头戏,下面这张表建议直接记住,考场上看一眼就能判断:

关系图形表示语义代码中的体现
依赖虚线 + 开放箭头临时使用方法参数、局部变量
关联实线(可带箭头)长期结构关系成员变量
聚合实线 + 空心菱形整体-部分,部分可独立存在成员变量,构造函数注入
组合实线 + 实心菱形整体-部分,同生共死成员变量,内部 new 出来
泛化实线 + 空心三角is-a,继承extends/:
实现虚线 + 空心三角实现接口implements

聚合和组合的分界,我一般这么判断:整体被销毁时,部分还能不能活。学院和学生是聚合,学院撤销了学生还在;订单和订单项是组合,订单删了订单项也就没意义了。菱形永远画在整体那一端,箭头指向部分。

多重度也是常考填空。常见的写法有1、0..1、*、1..*、0..*。题干里"一个订单可以包含任意多个订单项"就是1对*;"每个订单必须有一个且只有一个客户"就是1对1。这里最容易错的是把"可以"和"必须"搞混,*和1..*的区别就在这。

3.3 顺序图与状态图:消息和迁移的细节分

顺序图的消息区分三种:同步消息是实心箭头,调用方会阻塞等待;异步消息是开放箭头,调用完立刻返回;返回消息是虚线加开放箭头。生命线上的矩形条表示激活期,也就是对象正在执行操作的时间段。顺序图里的编号其实是可选的,但写上能体现调用顺序,不写也不算错。

状态图考的是"状态 + 事件 + 迁移"三元组。矩形圆角框是状态,实心圆是初始状态,牛眼(圆里套实心圆)是终止状态。迁移箭头上的标注格式是事件[守卫条件]/动作,比如提交订单[金额>0]/扣减库存。守卫条件放在方括号里,动作放在斜杠后面,写反或者漏掉方括号都会扣分。

状态图里有个容易忘的点:同一个状态可以有自迁移(箭头从自己出来又指回自己),表示在状态内部响应某个事件但不改变状态,比如"待支付"状态下"修改订单"。

3.4 包图与部署图:分层结构的表达方式

包图用来表达系统的分组结构,形状就是文件夹图标。包之间的关系主要两种:依赖(虚线开放箭头)和泛化(实线空心三角)。经典的题是三层架构——表示层依赖业务逻辑层依赖数据访问层,箭头方向从上层指向下层,不能画反。

部署图描述的是物理节点的部署关系,节点画成三维立方体,节点之间的通信用连线表示。这类图在实际考试里出现频率低,但一旦考到,往往是让你补节点之间的连接关系,判断依据是题干里"部署在...服务器上""通过...协议通信"这类句子。

3.5 用 Visio 画类图的顺手的几个设置

画图工具不限定,Visio 是最常见的。我用下来有几点经验:

  • 新建时直接选"软件和数据库"分类下的 UML 模型图,比用空白绘图再拖形状省事得多,因为它自带了类、接口、包、关系连接线。
  • 拖出类形状后,在形状的属性对话框里填属性和方法,比双击文本框手打更快,而且会自动应用标准的可见性符号(+公开、-私有、#保护)。
  • 画关系线之前先打开"对齐网格"和"连接线",让线自动吸附到形状的连接点上,避免线头悬空——线头悬空在打印出来或者截图后特别显眼,也容易被判成关系不明确。
  • 泛化、聚合这些关系有专门的形状,别用普通箭头硬拼,箭头样式不对直接影响判分。
  • 画完统一调整字号和排列,建议全部左对齐,类与类之间留出固定间距,视觉上会比随手摆的干净很多。

4. 设计模式:二十三种不必全背,先啃透这几类

4.1 创建型模式:解决"对象怎么造出来"

创建型一共五个(简单工厂不算 GoF 的正式成员,但考点上非常高频,必须掌握)。核心矛盾是:如果调用方直接 new 具体类,需求一变化就要改调用方代码。

模式一句话本质典型触发词
简单工厂一个工厂类按参数造不同产品"根据类型创建"
工厂方法父类定义创建接口,子类决定造什么"每种产品由对应工厂创建"
抽象工厂创建一族相关产品"整套界面风格切换"
单例全局只有一个实例"唯一""共享""配置管理器"
建造者分步组装复杂对象"多个可选部件组合"
原型复制现有对象"克隆""深拷贝"

单例是最简单的,也是最容易写错的。饿汉式在类加载时就创建实例,线程安全但可能浪费资源;懒汉式延迟创建,但必须处理并发,双重检查锁定的写法在 Java 里还得给实例字段加volatile,否则可能出现指令重排导致的半初始化对象。软考里常让你补全getInstance()方法,volatile和同步块是常见的两个空。

简单工厂虽然简单,但它的可扩展性差:新增产品就要改工厂类的 switch。工厂方法把这个变化下移到子类,符合开闭原则,所以考试里经常给出简单工厂的实现让你改成工厂方法。

4.2 结构型模式:解决"类与类怎么拼装"

结构型七个,考点最集中在这几个:

  • 适配器:接口不兼容时的转接器。触发词是"已有的类接口不匹配""需要复用遗留代码"。
  • 装饰器:在不改变原类的前提下动态叠加功能。触发词是"动态添加""多种功能自由组合",典型的例子是给输入流套缓冲、套加密。
  • 代理:为对象提供替身,控制访问。和装饰器的区别在于,代理的目的是控制,装饰的目的是增强。
  • 外观:给一个复杂子系统提供统一入口。触发词是"简化调用""提供统一接口"。
  • 组合:把树形结构中的叶子节点和容器节点统一对待。
  • 桥接:把抽象和实现分离成两个维度,各自独立变化。触发词是"两个维度正交变化",比如"形状"和"颜色"。
  • 享元:共享细粒度对象,节省内存。触发词是"大量相似对象""外部状态和内部状态分离"。

装饰器和代理长得像,区分点我总结成一句:装饰器是你主动给它加东西,代理是你不得不多走一层。装饰器的装饰者和被装饰者通常实现同一接口;代理也是,但代理一般自己持有真实对象的引用,由它决定要不要转发。

4.3 行为型模式:解决"职责怎么分配"

行为型十一个,高频的有策略、观察者、模板方法、命令、责任链、状态这几个。

策略把一堆可互换的算法各自封装起来,让它们可以互相替换,调用方只依赖抽象策略。触发词是"多种算法可切换""运行时选择不同算法"。考试里最常见的实现结构是:抽象策略接口 + 若干具体策略类 + 一个持有策略引用的上下文类。

观察者建立一对多的依赖,被观察者状态改变时自动通知所有观察者。触发词是"变化通知""订阅""联动更新"。抽象主题维护一个观察者列表,提供attach、detach、notify三个方法,是所有观察者题目的固定套路。

模板方法在抽象类里定义算法骨架,把变化的步骤留给子类实现。它和策略的区别在于:策略是用组合替换整个算法,模板方法是用继承替换算法中的某几个步骤。题干里出现"流程固定但某些步骤不同",就是模板方法;出现"整个算法都要能换",就是策略。

命令把请求封装成对象,从而支持排队、记录日志、撤销重做。责任链把处理者串成链,请求沿链传递直到被处理。状态把状态迁移封装进状态类,避免大段的 if-else 判断——它和策略的结构几乎一样,区别在于策略之间是平级的、由客户端选,状态之间会自行迁移。

4.4 识别模式的三个信号

做题时不用去猜"出题人想考哪个模式",题干里通常有信号词。我总结了三条:

  1. 找变化点:题干里出现"根据...不同""当...时""未来可能增加...",说明这里需要一个可替换的抽象,多半是策略、工厂或状态。
  2. 找扩展方向:出现"不修改原代码的前提下增加新功能",基本是装饰器;"增加新的产品种类",是工厂方法;"增加新的处理环节",是责任链。
  3. 找耦合位置:出现"需要屏蔽子系统的复杂性",是外观;"需要复用接口不兼容的旧类",是适配器;"需要控制对某对象的访问",是代理。

这三条信号在真题里的命中率相当高。练到后面你会发现,读完题干的前两句话,模式的轮廓基本就出来了。

4.5 选做代码题的答题节奏

最后一道题的结构非常固定:给一段带空缺的 Java 或 C++ 代码,配一张缺了几处的类图,让你填类名、填关系、填方法体。我的做法是分三步走:

第一步,先通读题干需求,在草稿纸上把名词圈出来,这些通常是类;把动词圈出来,这些通常是方法。

第二步,对照已有代码判断哪些类已经确定、哪些需要补。空缺的类名往往在题干里有明确命名提示,比如"客户信息存储类"就写CustomerStorage之类,重点是把抽象类或接口的位置找准。

第三步,最后填方法体。方法体里通常只有一两行,重点是理解上下文。比如策略模式里的上下文类,空缺处一般是strategy.algorithm();模板方法里的抽象类,空缺处一般是子类实现的具体步骤。

注意:如果 Java 和 C++ 都能做,选你更熟的那个。我见过有人因为 C++ 里虚函数和纯虚函数的语法细节丢分,明明思路全对,最后只拿到一半分数。

5. 一道下午题的完整推演:从需求文字到代码填空

5.1 读题阶段:把散文翻译成对象清单

假设题干描述的是一个咖啡店点单系统:顾客选择咖啡类型,不同咖啡价格不同;可以额外加牛奶、加糖、加摩卡,每种配料单独计价;最终要能计算出总价并打印明细。

先圈名词:顾客、咖啡、拿铁、美式、配料(牛奶、糖、摩卡)、订单、总价。再圈动词:选择、加、计算、打印。名词里的"咖啡"是抽象概念,具体的是拿铁、美式,所以大概率需要一个抽象类Coffee和两个子类;"配料"有多个且可叠加,说明它不是继承而是包裹——这时候装饰器的特征已经很明显了。

5.2 画图阶段:搭骨架和标关系

先画抽象类Coffee,属性放description和price,方法放getDescription()和cost()。两个子类用泛化箭头指过来,空心三角。

配料类CondimentDecorator继承自Coffee——注意这一步很关键,装饰器必须和被装饰对象是同一个类型,否则就没法层层包裹。CondimentDecorator里持有一个Coffee类型的成员变量,这条关系是关联(实线),多重度是1。三个具体配料类再泛化到CondimentDecorator。

Order类持有客户信息,和Coffee是关联关系,多重度1对1..*。如果题干说"一个订单包含多个咖啡",就是1对*。

5.3 填代码阶段:装饰器的经典写法

Java 版本大致长这样:

public abstract class Coffee { protected String description = "未知咖啡"; public String getDescription() { return description; } public abstract double cost(); } public abstract class CondimentDecorator extends Coffee { protected Coffee coffee; public CondimentDecorator(Coffee coffee) { this.coffee = coffee; } } public class Mocha extends CondimentDecorator { public Mocha(Coffee coffee) { super(coffee); } @Override public String getDescription() { return coffee.getDescription() + ", 摩卡"; } @Override public double cost() { return coffee.cost() + 5.0; } }

装饰器的精髓就在coffee.getDescription() + ", 摩卡"和coffee.cost() + 5.0这两行——它在调用被装饰对象结果的基础上做增量。考试里如果这个空缺是让你填的,写成直接返回"摩卡"或5.0就完全错了,因为那样就丢失了内层信息。

5.4 复盘:我在这类题上踩过的几个坑

第一个坑是多重度写反。有一次题干说"一个客户可以有零个或多个订单",我下意识写了1..*,其实应该是0..*,"可以"和"必须"是两回事。

第二个坑是把装饰器当继承。看到"加牛奶""加糖",第一反应是设计成CoffeeWithMilk extends Coffee,结果发现配料可以无限叠加,继承层次根本盖不住。只要题干里出现"可以任意组合""数量不限"这类描述,就应该往装饰器或者组合的方向想。

第三个坑是接口和抽象类互换。某次题干里抽象类已经明确带有属性description,我还硬把它写成接口,导致getDescription()没法给出默认实现,整个类图都跟着错。

6. 我自己的六周备考节奏和资料取舍

6.1 六周时间怎么分配

周次重点任务每日投入
第 1 周面向对象基础 + 三种语言语法对照60 分钟
第 2 周UML 六种关系 + 用例图、类图精练90 分钟
第 3 周顺序图、状态图、包图 + Visio 手感训练60 分钟
第 4 周创建型和结构型模式逐个人写一遍90 分钟
第 5 周行为型模式 + 近五年下午真题刷完120 分钟
第 6 周错题回炉 + 限时模拟120 分钟

这个安排的核心逻辑是:前两周打地基,中间两周练图纸,最后两周上手真题。设计模式必须自己手敲一遍,光看是记不住的。我当时把策略、观察者、装饰器、工厂方法、模板方法各写了一版能跑通的代码,写完之后再看真题代码,空缺处几乎不用想。

6.2 资料怎么挑,别贪多

知识点总结类的资料挑一本就够,关键是它得有图。纯文字总结对 UML 部分基本没用。真题必须用带图的完整版,光看题干和答案选项,你练不出画图的手感。

视频课如果看,建议只挑 UML 和设计模式这两块,面向对象的基础部分看书更快。另外提醒一句:真题解析里对同一道题的答案有时会有分歧,尤其是多重度和关系类型,遇到分歧就以题干里的原话为最终依据,别迷信任何一家解析。

6.3 几个我觉得真正有用的小习惯

画类图的时候,我习惯先把所有类的名字写成一行,从中间那个"最核心的类"开始画起,其他的往两边排。这样出来的图层次比较清楚,也不容易漏掉类。

做题时准备一张草稿纸专门记"不确定的点"——比如"这里到底是聚合还是组合""这个多重度是 1 还是 0..1"。做完对照答案时优先解决这些点,比大面积重看知识点效率高得多。

还有一个是我吃过大亏的:画完图一定要回头读一遍题干的每一句话,确认每句话都在图上有对应体现。我曾经因为漏读了最后一句"系统还需要记录每次操作日志",白丢了一个类和一个关联关系,这类失分最可惜。

最后说个我个人的体会。这三块内容真正难的地方不在记忆,在于把它们变成一个整体的直觉——看到"可能新增"就想到开闭原则,看到"多种算法"就想到策略,看到"动态叠加"就想到装饰器。这个直觉没有捷径,就是真题画图、写代码、复盘,来回滚几轮自然就有了。你要是现在还在纠结该先背哪个知识点,不如直接翻开一套下午真题,从读题开始练起。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询