1. 一道"开卷"却大量丢分的题:判分逻辑与4个致命失分点
说起软考软件设计师的下午卷,大部分人的第一反应是数据流图、数据库ER图、UML图、算法题这些"大头"。但真正决定你能不能稳过及格线、或者冲击高分的,往往是最后那道看起来最不起眼的题——第5/6题二选一的设计模式代码填空。Java版这道题的分值一般是15分,放在整张下午卷里占比不算夸张,但它却是全卷性价比最高的一道题:不需要你从零写一个完整程序,不需要你设计类结构,只需要你顺着已有的代码框架把空缺补全。换句话说,类图、场景、代码风格全都提前给到你了,本质上是一场"开卷考试"。
但有意思的是,就这么一道开卷题,每年都有大量考生栽跟头。我见过有人对着类图抄都能把类名大小写抄错,也见过有人明明模式都认识,却在 extends 和 implements 之间犹豫半天,最后白白丢分。这道题真正的难点不是"不懂设计模式",而是没搞清楚阅卷到底按什么标准给分、哪些地方的坑是每年固定埋好的。先把这两个问题彻底弄明白,后面谈识别模式和填代码才是有意义的。
1.1 题型结构:为什么说它是下午卷性价比最高的题
软考软件设计师下午卷的题目结构,近些年基本是稳定的:前四题分别是数据流图、数据库设计、UML建模、算法与数据结构,最后两题是 C++ 实现和 Java 实现,考生选其中一题作答。这两题在绝大多数年份考的就是设计模式,题目会给出一段业务场景说明、一张对应的 UML 类图,以及一段用 Java 或 C++ 写好的核心代码。代码里会留出 4 到 6 个空位,用 (1)(2)(3) 这样的编号标出来,考生需要把每个空对应的内容写在答题栏里。
这种题型有一个很实用的特点:它的答案是"可推导"的。选择题可以靠排除法,简答题可以靠瞎编碰运气,而代码填空是三者里最讲逻辑的——类图上角色清清楚楚,代码风格明明白白,你只需要从"看出它在考什么模式"出发,按照模式的固定骨架去反推每个空。所以我说它是下午卷最划算的一道题:投入产出比极高,只要花时间把高频模式的代码骨架吃透,满分是完全够得着的。
分值上,题目总分 15 分,每空 2 到 3 分。具体怎么给分,阅卷时通常看的是空位的类型:如果空位要求填类名、接口名、方法名,那只要与题面给出的名字不一致,基本就是零分;如果空位要求填一段语句,比如 order.setState(...),那代码逻辑对但大小写或参数顺序错了,依然拿不到分。这也解释了为什么很多人"感觉会做"却对答案时错得莫名其妙——他们以为填个大概意思就行,但世界上没有任何一个学科的阅卷,会认可你写出的"大写U"当小写u用。
1.2 阅卷视角下的4个致命失分点
这些年我自己刷真题、模拟题,加上帮人改卷总结下来,代码填空的失分点其实非常集中,翻来覆去就是那几类。把它们列出来当"避坑清单",比你盲目刷二十道题都管用。
第一个失分点是 extends 与 implements 分不清。Java 里继承类用 extends,实现接口用 implements,具体类继承抽象类也用 extends。很多题会在空位里同时出现接口和抽象类,让人一紧张就写反。比如装饰模式里,抽象装饰类 Decorator 要实现组件接口,那这行必须是 abstract class Decorator implements Component;具体装饰类继承抽象装饰类,那必须是 class ConcreteDecorator extends Decorator。这两个空紧挨着出现时,最容易出错。
第二个失分点是访问控制符写错。最典型的是单例模式,私有构造方法、私有静态实例、公有静态获取方法,三个修饰符一个都不能错。观察者模式里,被观察者持有的观察者集合字段应该是 private,但很多人在填"私有字段"的时候,脑子里想着"这是内部使用",手上却写成 protected 甚至 public,一分没得还觉得自己没错。
第三个失分点是 super 调用缺失或参数写错。装饰模式、桥接模式、模板方法模式里,子类构造方法必须调用父类构造方法,super(component) 这类代码几乎是必考空位。问题在于有些人只写了 super(); 忘了传参,或者参数类型对不上。这里没有中间态,写错就是整空没分。
第四个失分点是类名、方法名大小写与题面不一致。题面类图里写的是 OrderState,你在空里填 orderstate,哪怕后面逻辑全对,这空也判错。软考不是让你重新设计名字的考试,它考的就是你能不能严格按已有代码的命名和风格去补全。这个失分点在所有错误里最冤,也最多见。
提示:拿到题目先在草稿纸上把类图里的类名、接口名、方法名原样抄一遍,做填空时逐个对照,这个小动作能帮你规避一半以上的低级错误。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 看到什么关键词就想到什么模式:模式识别三重定位法
进了考场,面对一道设计模式题,你做的第一个动作不是急着填代码,而是先判断"这题考的是哪个模式"。判断对了,后面的填空就是顺着骨架填肉;判断错了,你会在错误的框架里越走越远,哪怕某个空蒙对了,整体逻辑也会自相矛盾。那么问题来了:怎么快速、准确地识别模式?
我的经验是三条线索交叉验证,缺一不可。第一是题干描述里的关键词,它直接告诉你意图;第二是 UML 类图的形态特征,它反映设计上的结构关系;第三是代码里出现的角色类名。这三条线索就像三个证人,如果它们指向同一个模式,那基本不会错。
2.1 第一重:题干关键字与设计模式对照表
软考的题目说明文字不是随便写的。为了考察某个模式,题干通常会用一两句"意图描述"来点题。这些描述虽然不会直接告诉你"本题考策略模式",但只要你做过几套真题,就会对它们特别熟悉。我把高频对应的关键词整理成了一张表,刷题前建议先记住。
| 题干中的意图描述 | 最容易对应到的模式 |
|---|---|
| 动态地给对象添加/扩展职责,且不修改原类 | 装饰模式 |
| 定义一系列算法,把它们一一封装,并且可以互相替换 | 策略模式 |
| 对象间形成一对多的依赖关系,一个对象状态改变时,所有依赖者都能收到通知 | 观察者模式 |
| 使原本接口不兼容的类能够一起工作 | 适配器模式 |
| 将抽象部分与实现部分分离,让它们可以独立变化 | 桥接模式 |
| 用树形结构表示"部分-整体"的层次结构 | 组合模式 |
| 将请求封装为对象,从而支持参数化、排队、日志 | 命令模式 |
| 对象的行为取决于其状态,并且运行时可改变状态 | 状态模式 |
| 定义算法的骨架,将某些步骤延迟到子类实现 | 模板方法模式 |
| 保证一个类仅有一个实例,并提供全局访问点 | 单例模式 |
| 提供一个统一的接口来访问子系统中的一群接口 | 外观模式 |
| 避免请求发送者与接收者之间的直接耦合 | 中介者模式/命令模式 |
这张表的作用是建立"第一直觉"。当你看到题干里有"动态地添加职责",先想到装饰模式;看到"算法封装、互相替换",先想到策略模式。不过要注意,关键词只是线索,不是铁证,它帮你框定一个大方向。真正的确认,还需要结合类图形态和角色命名。
2.2 第二重:类图形态特征快速判断
软考题目里给的 UML 类图是高度模式化的,跟你在项目设计文档里画的那种不太一样,它刻意把模式的核心结构画得很清晰。这时候你就需要会"读图",而且只读关键特征。
装饰模式的类图有一个非常标志性的形态:Component 接口在最左侧,ConcreteComponent 在下方实现它,Decorator 这个抽象类在右侧,它既实现了 Component,又通过一条带箭头的线关联回 Component——看起来就像装饰类"抱住"了组件。凡是看到这种"一个抽象类同时实现接口且持有同接口引用"的结构,八成是装饰模式。
策略模式和状态模式在类图上容易混,因为它们都是"上下文类持有一个策略/状态接口"。区别在于策略模式的类图里,各个具体策略类之间是"平级兄弟"关系,没有互相转换的线;而状态模式的类图里通常能看到具体状态类依赖上下文类(或者状态类调用上下文类的某个方法),因为状态切换需要借助上下文对象。
观察者模式的类图最好认:被观察者类里有一个集合属性,集合的泛型是观察者接口或抽象类,图中通常表现为一条"一对多"的关联关系线。
适配器模式的类图同样经典:Target 接口、Adapter 类、Adaptee 类三个角色排在一起,Adapter 指向 Target 表示实现关系,同时指向 Adaptee 表示持有/继承关系。
类图形态是比关键词更可靠的信号,因为关键词可能会有歧义,而结构关系不会说谎。你看到一个类既实现接口又持有接口引用,它不可能是单一职责的工厂模式,对吧?
2.3 第三重:类名/接口名里的角色信号
很多题目为了降低难度,类名本身就带有模式角色的提示。Subject 和 Observer 这种名字一出现,你根本不需要再猜它是观察者还是策略;Component、Leaf、Composite 三个名字同时出现,那绝对是组合模式。这不是什么高深的技巧,纯粹是"你认识这些英文单词"。
我习惯在做题时做一件很机械但很有效的事:把题面代码里出现的所有类名、接口名先抄到草稿纸的左侧,然后根据名字判断角色,比如 Context 是上下文、Strategy 是策略接口、ConcreteStrategyA 是具体策略。角色一标完,整个代码结构在你眼里就从"一堆看不懂的英文"变成了"一个模式的标准骨架",空位在哪里、该填什么,自然就清楚了。
需要记住的高频角色名其实不多:Observer/Subject、State/Context、Strategy/Context、Component/Decorator/ConcreteComponent、Target/Adaptee/Adapter、Command/Receiver/Invoker、Handler/ConcreteHandler、Product/Creator/ConcreteCreator。这些名字就像模式的身份证,一旦在题目里看到,识别时间能从两分钟压缩到十秒。
三重定位跑完后,你心里应该能打出两个字:"策略""状态""装饰"……当你能用一个词说清题目考什么的时候,就可以进入下一步——按模式的固定代码骨架去逐空推答案了。
3. 挑大梁的9个高频模式:结构骨架与挖空位置精讲
模式识别是前提,真正决定你能不能拿满分的是对高频模式代码结构的熟悉程度。软考历年真题刷下来,你会发现在几十种设计模式里,命题人翻来覆去用的就那九到十种。它们占据了 90% 以上的考察频率,剩下的模式偶尔露面,频率低到可以放在"应急识别卡"里对付。这一节我按真题出现频率,把这九个模式的关键代码骨架、每个骨架里最容易挖空的位置、以及填法要点一次性讲透。
3.1 策略模式:算法封装的上下文调用链
策略模式的核心思想是把一系列可以互相替换的算法封装成独立的类,然后让上下文持有算法接口的引用,运行时切换具体算法。金融计费系统、电商折扣、出行价格计算,都是它的经典应用场景。
java复制// 策略接口
interface PriceStrategy {
double calc(double price);
}
// 具体策略:原价
class NormalPrice implements PriceStrategy {
public double calc(double price) {
return price;
}
}
// 具体策略:折扣价
class DiscountPrice implements PriceStrategy {
private double rate;
public DiscountPrice(double rate) {
this.rate = rate;
}
public double calc(double price) {
return price * rate;
}
}
// 上下文
class PriceContext {
private PriceStrategy strategy;
public void setStrategy(PriceStrategy strategy) {
this.strategy = strategy;
}
public double quote(double price) {
return strategy.calc(price);
}
}
策略模式在软考里最容易挖空的位置有三个:接口的定义处、setStrategy 方法的方法体、以及 quote 方法里调用策略的地方。第一个空要你会写接口方法声明,注意接口方法默认是 public abstract,但写不写 abstract 都可以,软考答案一般会省略 abstract;第二个空基本就是 this.strategy = strategy;,考察字段名和参数名冲突时 this 的用法;第三个空是 strategy.calc(price),考察你有没有把调用对象搞错。很多人在第三个空会写成 this.quote(price) 或者直接 calc(price),这会直接导致编译失败。
还有一个细节值得注意:PriceContext 里的字段类型是 PriceStrategy 接口,而不是某个具体策略类。这是策略模式的灵魂,也是填空时判断"该填接口还是该填实现类"的重要依据——上下文只面向接口编程,具体选哪个策略,由客户端在使用时通过 setStrategy 传入。
3.2 观察者模式:一对多通知中的三个固定空
观察者模式解决的是"多对多"的通知问题:一个对象状态变了,所有关心它的对象都要被通知到。股票行情、订阅号推送、订单状态通知都属于这个模式。代码骨架通常是被观察者维护一个观察者列表,状态变化时遍历列表调用每个观察者的更新方法。
java复制import java.util.*;
interface Observer {
void update(double price);
}
class Stock {
private List<Observer> observers = new ArrayList<>();
private double price;
public void attach(Observer observer) {
observers.add(observer);
}
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(price);
}
}
public void setPrice(double price) {
this.price = price;
notifyObservers();
}
}
class DisplayBoard implements Observer {
private String name;
public DisplayBoard(String name) {
this.name = name;
}
public void update(double price) {
System.out.println(name + " 收到最新价格:" + price);
}
}
观察者模式的填空位置非常有规律,我总结为"三个固定空":第一个是被观察者里的观察者集合字段,填 List<Observer> observers,很多人会漏掉泛型里的 Observer;第二个是 notifyObservers 方法里的遍历代码,可能是增强 for 循环,也可能用迭代器;第三个是在循环体内对每个观察者调用更新方法,也就是 observer.update(price)。
这里有个易错点:更新方法传递的参数到底是什么。观察者模式有两种实现风格,一种是"推模型",被观察者把关心的数据直接传给 update 方法;另一种是"拉模型",update 不传参,观察者自己从被观察者那里取。软考大多考"推模型",参数是价格或其他业务数据,你需要从题面上下文判断参数名和类型。如果题干里写的价格变量名是 price,那 update 的参数类型是 double,如果你写 int 或写别的方法名,整空作废。
3.3 装饰模式:super链上每一环都不能漏
装饰模式是软考下午题里出现频率最高的模式之一。它的作用是动态地给一个对象添加功能,且不修改原来的类。理解它最重要的一点是:装饰类和被装饰类实现的是同一个接口,装饰类内部又持有一个该接口的引用。这种"自己实现接口又包着同类对象"的结构,决定了它必然涉及两个关键代码动作:构造注入和转发调用。
java复制interface Component {
void operation();
}
class ConcreteComponent implements Component {
public void operation() {
System.out.println("基础功能");
}
}
abstract class Decorator implements Component {
protected Component component;
public Decorator(Component component) {
this.component = component;
}
public void operation() {
component.operation();
}
}
class LogDecorator extends Decorator {
public LogDecorator(Component component) {
super(component);
}
public void operation() {
super.operation();
System.out.println("增加日志功能");
}
}
软考在这道题上特别喜欢挖 6 个空甚至 7 个空,基本就是把这个骨架的六个关键点全部挖掉。
第一个空在 ConcreteComponent implements Component,考察接口实现;第二个空在 Decorator 类的声明处,答案是 abstract class Decorator implements Component,这一行同时考了 abstract、implements 和 Component 三个知识点;第三个空是 protected Component component 字段,注意修饰符是 protected 而不是 private,因为子类装饰器在 super.operation() 这类调用中虽然不直接访问该字段,但更规范的写法是让子类可以通过继承访问,软考答案通常用 protected;第四个空是构造方法的参数和赋值,this.component = component;第五个空在具体装饰类的构造方法里,super(component);第六个空是 super.operation(),也就是先执行被装饰对象的功能,再添加新功能。
这个模式里最容易连环丢分的点是 super 调用:如果第五个空不会填,后面具体装饰类的 operation() 里 super.operation() 也没有意义,因为整个链条断掉了。所以做装饰模式的题,我的建议是从"链"的角度去思考:每个装饰类都是一层壳,壳与壳之间靠 super 串起来,最底层是那个原始组件。你填出的代码必须形成一条从最外层到最里层的完整调用链,任何一环断裂,运行就会报错。
3.4 工厂方法模式:创建对象的延迟到子类
工厂方法模式解决的是"创建对象和使用对象解耦"的问题。父类定义了一个创建产品的抽象方法,具体创建哪个产品,由子类决定。软考中出现频率不如装饰模式高,但也隔三差五考一回。
java复制abstract class Product {
abstract void use();
}
class ProductA extends Product {
public void use() {
System.out.println("使用产品A");
}
}
abstract class Factory {
abstract Product createProduct();
public void operate() {
Product p = createProduct();
p.use();
}
}
class FactoryA extends Factory {
protected Product createProduct() {
return new ProductA();
}
}
工厂方法模式的填空点通常集中在三个位置:产品类的抽象方法定义、工厂类里的抽象工厂方法定义、以及具体工厂对工厂方法的实现。前两个空本质上考的是"抽象方法怎么写",答案里必须有 abstract 关键字和正确的返回类型,比如 abstract Product createProduct();。第三个空考的是具体实现,return new ProductA();。
这里有个隐蔽的考点:子类覆盖父类方法时,访问权限不能比父类更严格。如果父类的工厂方法是 abstract Product createProduct();,而父类方法默认是包级私有(default),子类写成 protected 或 public 都没问题,但如果子类写成 private,编译直接报错。软考真题中,我见过有空位专门放在这个修饰符上的,所以做这类题时留意一下子类方法的修饰符是否合法。
3.5 适配器模式:接口转换的两种写法
适配器模式解决接口不兼容的问题。它有两种实现风格:类适配器(通过继承)和对象适配器(通过组合)。软考中对偶适配器考察得更多一些,因为它的写法更符合"组合优先于继承"的现代设计理念。
java复制interface Target {
void request();
}
class Adaptee {
public void specificRequest() {
System.out.println("被适配者");
}
}
class Adapter implements Target {
private Adaptee adaptee;
public Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
public void request() {
adaptee.specificRequest();
}
}
对象适配器的填空位置相当固定:Adapter implements Target、private Adaptee adaptee 字段、构造方法参数与赋值、request() 方法里对 adaptee.specificRequest() 的调用。这四个空你只要记住一个判断逻辑——适配器要做的事情,是把 Target 接口的 request() 翻译成 Adaptee 的 specificRequest(),所以 Adapter 必须实现 Target,同时持有 Adaptee 对象的引用。
如果是类适配器,代码会变成 class Adapter extends Adaptee implements Target,此时不需要持有 Adaptee 字段,直接从继承来获得 specificRequest()。软考的代码填空里出现过要求判断选哪种适配器的情况,判断依据很简单:看类图里 Adapter 和 Adaptee 之间是空心三角箭头(继承)还是普通箭头(组合)。如果你看到类图是继承关系,那空里多半要填 extends Adaptee implements Target。
3.6 状态模式:状态对象自己决定下一步
状态模式把对象在不同状态下的行为封装到独立的状态类中,让对象的状态在运行时可以切换。电商订单状态流转、电梯运行状态、审批流程状态,都是典型的应用场景。它和策略模式结构上很像,但区别在于:策略模式由上下文决定用哪个算法,而状态模式通常由状态对象自己决定下一个状态是什么。
我在项目里经常用一句话跟同事解释这个差别:策略是你"选"出来的,状态是你"变"过去的。这个差别在填空中体现得很明显——状态模式的具体状态类里,几乎一定会出现 context.setState(new 某个状态()) 这样的代码,而策略模式不会出现这种"切换"动作。
软考的典型状态模式代码骨架是这样的:
java复制interface OrderState {
void pay(Order order);
void ship(Order order);
}
class PendingState implements OrderState {
public void pay(Order order) {
order.setState(new PaidState());
}
public void ship(Order order) {
System.out.println("未支付不能发货");
}
}
class Order {
private OrderState state;
public Order() {
state = new PendingState();
}
public void setState(OrderState state) {
this.state = state;
}
public void pay() {
state.pay(this);
}
}
状态模式最容易挖空的点有四个:OrderState 接口的方法定义、具体状态类里的 order.setState(...) 切换语句、Order 类里的状态字段、以及 setState 方法体的 this.state = state。其中 setState 的赋值语句是每年都会出现的"送分空",但越简单的空越有人错,因为他们忽略了字段名和参数名相同时必须用 this 来区分的问题。
3.7 命令模式:请求发送者与执行者解耦
命令模式把请求封装成对象。请求的发送者(Invoker)不直接认识请求的执行者(Receiver),它只认识一个命令接口,调用命令的 execute() 方法;真正的执行动作发生在具体命令类里,由命令类持有接收者并调用它的方法。
java复制interface Command {
void execute();
}
class Light {
public void on() {
System.out.println("开灯");
}
}
class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.on();
}
}
class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
}
命令模式的填空点分布特别规整,几乎就是按角色的代码各挖一个:LightOnCommand implements Command、private Light light 字段、构造方法赋值、execute() 方法里的 light.on()、RemoteControl 里的 setCommand 方法体、以及 pressButton 里的 command.execute()。只要你能在草稿纸上标出题目里的 Invoker、Command、Receiver 三个角色,这些空大部分都是"套公式"。
这个模式的难点不在填空,而在于你要从一段没有任何提示的代码里找出"谁是指令发送者、谁是接收者"。识别方法很简单:找类里有没有 setCommand 或 setXXX 方法,那个类多半是 Invoker;找被调用具体业务方法(比如 light.on())的类,那是 Receiver;而包含 execute() 方法且持有 Receiver 引用的类,就是具体的 Command。
3.8 模板方法模式:骨架固定,步骤延迟
模板方法模式定义了一个算法的骨架,把某些步骤延迟到子类实现。父类里的模板方法通常用 final 修饰,防止子类修改整体流程;子类只需要实现那些抽象的原语操作。
java复制abstract class DataParser {
public final void parse() {
openFile();
readData();
closeFile();
}
abstract void openFile();
abstract void readData();
void closeFile() {
System.out.println("关闭文件");
}
}
class CsvParser extends DataParser {
protected void openFile() {
System.out.println("打开CSV文件");
}
protected void readData() {
System.out.println("读取CSV数据");
}
}
模板方法模式的填空点集中在三个地方:final 修饰的模板方法、抽象原语方法的声明、以及具体子类对原语方法的实现。第一个点考 final 的原因是,如果你不写 final,代码也能编译运行,但它不符合模式的设计意图——模板方法就是要锁定算法的骨架。软考答案里通常会写 public final void parse(),这算是一个"超纲但很重要"的考点。
原语方法的声明处,要填 abstract void openFile();,注意 abstract 关键字不能漏,方法末尾的分号不能忘。子类实现时,访问权限至少要和父类一致,父类是 abstract 修饰的包级方法,子类写 protected 或 public 均可,但如果你写 private 就是错的。
3.9 单例模式:三个私有一个公共
单例模式是所有模式里代码量最小的,但同时也是考察频率不低、失分率奇高的一个。它要求一个类只能有一个实例,并提供一个全局访问点。Java 实现单例有饿汉式、懒汉式、双重检查锁等好几种写法,软考代码填空最常考的是懒汉式加同步锁的写法。
java复制class Singleton {
private static Singleton instance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
单例模式的灵魂是"三个私有一个公共":私有静态字段 instance、私有构造方法、公有静态方法 getInstance。填空点基本就围着这三个修饰符转。字段处填 private static Singleton instance;,构造方法处填 private Singleton() {},方法处填 public static synchronized Singleton getInstance()。如果考双重检查锁,你的答案里还得多一个 synchronized (Singleton.class)、一个 volatile 修饰的字段。软考基本不会考到双重检查锁那么细,但你至少要知道 getInstance 返回类型必须是 Singleton,且方法内部必须通过 instance == null 判断来创建实例。
根据我的经验,单例模式一旦出现在试卷里,往往是作为某个大题的"配角"出现,比如系统里某个全局配置管理器需要做成单例。所以它单独作为一道大题的可能性不大,但在一道大题里嵌入一个单例模式的空,概率相当高。类似的"配角"还有工厂模式,经常和策略模式、模板方法模式一起出现。
4. 剩下14个模式的"应急识别卡":不熟也能拿分
高频九个模式讲完了,但你要知道,软考考纲里列出的设计模式远不止这九个,23 种甚至 24 种都在范围内。万一考到前面没提到的模式怎么办?我的策略是:不需要把每个模式的完整代码背下来,但至少要有一张"应急识别卡"——看到名字就知道它是干什么的、长什么样、最可能出在哪两个空。这样即使运气差碰到冷门模式,也能靠类图和代码上下文硬推出一半的分数。
4.1 结构型模式:桥接、组合、外观、代理的记忆锚点
先记一句话:结构型模式解决的是"怎么组合类和对象"的问题。这四个名字容易混,但记忆锚点很清晰。
桥接模式,核心是"抽象部分持有实现部分的接口"。典型代码是抽象类 Shape 里有一个 Drawing 接口字段,构造方法注入,draw() 方法里调用 drawing.drawCircle() 这类方法。如果题目里你看到"独立变化"这个词,十有八九是桥接。它最可能出空的地方是抽象类的构造方法和里面那个接口字段的声明。
组合模式,核心是"树形结构、部分-整体"。典型代码是 Component 接口下有 Leaf 和 Composite 两个实现类,Composite 里维护一个 List<Component> 集合,提供 add、remove、getChild 方法。题目里一旦出现"树形结构"和"递归调用",就锁定组合模式。它最容易挖的空是 Composite 类里的集合泛型类型和 add 方法的方法体 children.add(component)。
外观模式,核心是"提供一个统一的门面"。典型代码是 Facade 类里聚合多个子系统对象,operation() 方法里依次调用各个子系统的方法。它几乎是所有模式里最简单的,出空的位置也很固定:Facade 类里的子系统对象字段、以及门面方法里对子系统方法的调用。
代理模式,核心是"控制访问"。典型代码是 Proxy 类实现业务接口,并持有一个 RealSubject 对象的引用,在 request() 方法里完成 if (realSubject == null) realSubject = new RealSubject(); realSubject.request(); 这样的延迟加载逻辑。题目如果提到远程代理、安全代理、延迟加载这些场景词,就往代理模式想。最容易出空的地方就是那个 if 判断和真实对象的调用。
4.2 行为型模式:职责链、中介者、备忘录、迭代器、访问者的应对姿势
行为型模式占了大头,但它们之间差异很大,用对比记忆会快得多。
职责链模式,核心是"请求沿链传递"。典型代码是抽象 Handler 类有一个 successor 字段和 setSuccessor 方法,每个具体处理者处理完自己不能处理的部分后,调 successor.handleRequest(...) 转给下一个。最可能出空的位置就是那个 successor.handleRequest(...) 调用。
中介者模式,核心是"对象间交互通过中介者解耦"。典型代码是 Colleague 类持有 Mediator 引用,交互调用都走 mediator 的方法。出空位置一般在 Colleague 里调用中介者的那一行。
备忘录模式,核心是"撤销、恢复"。典型代码是 Originator 类里有 createMemento() 和 restoreMemento(Memento m) 两个方法,Memento 类存状态快照,Caretaker 负责保管。如果题目里出现"撤销"二字,基本就是它。
迭代器模式,核心是"顺序访问集合但不暴露内部结构"。它不用自己写代码,因为 Java 集合框架已经内置了迭代器。但软考如果出题,无非就是让你填 hasNext() 和 next() 两个方法。这个模式单独出大题的几率极低,填一两个空倒是常见。
访问者模式,是所有模式里代码最绕的一种。它需要在元素类里定义 accept(Visitor v),方法体是 v.visit(this),然后在具体访问者里定义对每种元素的 visit 方法。这个模式在软考里很少出大题,但一旦出了,你会发现题面代码量很大。应对策略很简单:记住 accept 方法里那一行 visitor.visit(this); 就是几乎所有的填空点,剩下全靠上下文推。
4.3 创建型里其余的:抽象工厂、原型、建造者
创建型模式除了前面讲过的工厂方法、单例,还有抽象工厂、原型、建造者三个。它们的共同特点是围绕"对象创建"做文章,但侧重点不同。
抽象工厂模式解决的是"产品族"的创建问题。它和工厂方法的区别在于:工厂方法只创建一种产品,抽象工厂创建一系列相关产品。典型代码里有 AbstractFactory、ConcreteFactoryA、ProductA1、ProductA2 这些角色,出空位置通常是抽象工厂接口方法的返回类型和具体工厂里 return new ... 的语句。
原型模式解决的是"克隆复制"的问题。Java 里的实现依赖 Cloneable 接口和 clone() 方法。如果题目代码里有 implements Cloneable 和 super.clone(),那就是它。填空点一般就是 super.clone() 的返回值和异常处理,代码量很小。
建造者模式解决的是"复杂对象的构建过程与表示分离"的问题。典型角色有 Builder、ConcreteBuilder、Director、Product。代码里通常有 builder.buildPartA() 这样的链式调用,出空位置以 Director 类里的构建流程代码为主。
这些模式我在备考时并没有完整背过代码,只记了上面这些"锚点"。事实证明,软考代码填空不会要求你对一个平时不怎么考的模式写出从零到一的完整实现,它更愿意把模式的核心骨架搭好,只挖一两个最关键的空让你填。所以哪怕你对某个模式不熟,只要能抓住它的角色组成和核心调用关系,拿一半分以上并不难。
5. 实战走一遍:订单状态流转的6个空逐空拆解
说了这么多方法论,不如完整走一遍真实做题流程。我按软考真题的格式,自己编了一道接近考试难度的模拟题,场景用了电商订单状态流转。大家可以跟着我的思路,看看拿到一道陌生题后,怎么从"读题"到"填空"一步步推进。这道题设计的空位和软考真题的挖空风格相似,考点覆盖也很有代表性。
5.1 题干与类图:先判断模式再进代码
题目场景是电商平台的订单状态管理,说"一个订单在生命周期内经历待支付、已支付、已发货、已完成四种状态。不同状态对 pay() 和 ship() 方法的响应不同,且允许状态间按规则流转,现采用状态(State)设计模式实现"。这句话里的关键词非常直接:"状态""流转""响应不同",几乎是把"状态模式"这四个字写在脸上了。
类图的结构是:Order 类持有一个 OrderState 接口的关联关系;OrderState 下面有 PendingState、PaidState、ShippedState、CompletedState 四个实现类。这个类图一出来,模式基本锁定:上下文类是 Order,状态接口是 OrderState,四个具体状态类是四个实现类。
5.2 待补全的Java代码
下面是题目给出的 Java 代码,用 (1) 到 (6) 标出了空位:
java复制interface OrderState {
void pay(Order order);
void ship(Order order);
}
class PendingState implements OrderState {
public void pay(Order order) {
System.out.println("订单支付成功");
order.setState(new PaidState()); // (1)
}
public void ship(Order order) {
System.out.println("订单未支付,不能发货");
}
}
class PaidState implements OrderState {
public void pay(Order order) {
System.out.println("订单已支付,请勿重复支付");
}
public void ship(Order order) {
System.out.println("订单已发货");
order.setState(new ShippedState()); // (2)
}
}
class ShippedState implements OrderState {
public void pay(Order order) { }
public void ship(Order order) {
System.out.println("订单已完成");
order.setState(new CompletedState()); // (3)
}
}
class CompletedState implements OrderState {
public void pay(Order order) { }
public void ship(Order order) { }
}
class Order {
private OrderState state; // (4)
public Order() {
state = new PendingState(); // (5)
}
public void setState(OrderState state) {
this.state = state; // (6)
}
public void pay() {
state.pay(this);
}
public void ship() {
state.ship(this);
}
}
5.3 逐空解析:每个空的判断依据
空 (1) 在 PendingState 类的 pay 方法里。业务逻辑是"支付成功后订单进入已支付状态",所以要调用订单的状态切换方法。状态类里持有的是 Order 对象引用(方法参数),而 Order 对外暴露了 setState 方法,所以答案很明确:order.setState(new PaidState());。这里有两个关键检查点:第一,setState 的入参类型必须是 OrderState,而 PaidState 是 OrderState 的实现类,所以填 new PaidState() 没问题;第二,千万别把 order 写成 this,PendingState 类自己并没有 setState 方法,只有 Order 类有。
空 (2) 和空 (3) 与空 (1) 完全同构,只是切换目标不同。空 (2) 发货成功后切到 ShippedState,空 (3) 确认收货后切到 CompletedState。这三个空连在一起,看似重复,实际上是在考察你能否在整个状态链上保持逻辑一致。如果你在空 (1) 写对了 PaidState,空 (2) 写了 CompletedState,那系统就永远不会进入发货状态,这条状态机就断裂了。所以填这种"同构空"时,一定要按业务顺序把它们串起来读一遍:待支付 → 已支付 → 已发货 → 已完成,保证每一步都指向正确的下一个状态。
空 (4) 在 Order 类里,是上下文类的状态字段声明,答案是 private OrderState state;。它的考点有两个:字段类型必须是 OrderState 接口而不是具体状态类,否则在空 (1)(2)(3) 里 setState(new PaidState()) 会编译失败;访问修饰符用 private,符合封装原则。
空 (5) 在 Order 的构造方法里,作用是初始化状态。订单刚创建时是待支付状态,所以答案是 state = new PendingState();。这个空告诉我们一件事:状态模式的初始状态由业务逻辑决定,没有统一公式。做题时必须从题干场景去找"对象创建时处于什么状态"的描述,而不是凭感觉填。
空 (6) 是 setState 方法体的实现,答案是 this.state = state;。这是全场最简单的空,但也是最容易被轻视的空。很多人在这个题里出错,不是因为不会写赋值语句,而是因为方法参数名和字段名都叫 state,忘了加 this,导致赋值变成了"自己给自己赋值",字段值根本没有更新。如果题目把参数名写成 newState,那答案可以写 state = newState;,但加不加 this 都不会影响正确性——关键在于你写出的代码是否真的更新了字段。
这 6 个空做完,整个状态模式的核心闭环就完整了:状态对象通过 Order 的 setState 修改上下文的状态,上下文通过 state.pay(this) 和 state.ship(this) 把请求转发给当前状态对象。做题时如果你发现填出的代码无法形成这样的闭环,那说明某个空肯定填错了,这时候回头检查比死磕某一空更有效。
6. 备考冲刺:最后两周怎么把这15分焊死
方法、模式、真题演练都讲完了,最后聊聊备考策略。说实话,设计模式代码填空是软件设计师下午卷里投入产出比最高的题型,你不需要像准备算法题那样刷几百道,只需要做对几件事,十五分基本就能稳稳拿下。
6.1 时间分配与刷题方法
下午卷一共 150 分钟,我的建议是给最后这道设计模式题留出 25 到 30 分钟。听起来很多,其实节奏应该是这样的:前 5 分钟通读题干和类图,确认模式类型,在草稿纸上标出角色名;中间 15 分钟填完所有空;最后 5 到 10 分钟把填完的代码从头到尾读两遍,检查编译会报什么错。很多人觉得前面大题难,想省时间多研究前面,结果最后设计模式题仓促作答,反而丢掉最容易拿的 15 分。这个取舍从应试角度讲非常不划算。
刷题资料来源很简单:近五年到八年的软考软件设计师真题,把其中 Java 版的代码填空题全部挑出来做。注意,同一道题建议做两遍。第一遍按正常考试节奏做完、对答案;第二遍隔几天再做,但这次不是单纯填空,而是要求自己在每题代码旁边标注出所有角色名称,比如某个类是 Context、某个类是 ConcreteStrategy。这个"标注角色"的动作,能帮你把每个模式的结构真正内化,比单纯背十遍代码都有效。
6.2 我的个人体会:一个让正确率显著提升的习惯
最后分享一个我自己的做题习惯,它帮我从"会做但总丢分"提升到"稳定拿满分":填每个空之前,先在草稿纸上回答三个问题——这个空所在的代码位置属于哪个角色?这个角色在这个模式里应该做什么?它能访问哪些对象?三个问题答案都明确后,再动笔写答案。
比如你在状态模式里看到某个具体状态类的方法里有个空,先想这是状态类自己处理业务,还是要切换上下文的状态。如果需要切换状态,那它必须有 Order 对象的引用,而这个引用就是方法参数里的 order,所以答案里一定包含 order.setState(...)。这一步分析做完,你根本不会出现"不知道该填什么"的情况。
还有一个容易被忽视的点是:填完所有空之后,把整段代码从头到尾读一遍,在脑子里模拟执行一次。比如订单状态流转的题,就模拟从创建订单、发起支付、确认发货到完成订单,整个过程打印了什么、状态在哪里切换。模拟执行能发现大部分逻辑断裂问题,这比你反复检查某一空的拼写有价值得多。代码填空最怕的不是某个空不会填,而是填完之后整段代码根本跑不通,那才是真正的灾难。
根据我个人的备考体验,设计模式这 15 分是下午卷里最"良心"的分数。它不考临场发挥,不考灵感,只考你对模式结构的熟悉程度。把这个结构吃透,考场上的每一分钟都是你在"按图索骥",而不是"临场发挥"。希望这篇讲练结合的内容,能帮你把这 15 分焊死在兜里。
