坐在复习软件构造的电脑前,我盯着教材里的一大串模式名称发呆。工厂方法、抽象工厂、适配器、装饰器、策略、观察者……每个词都认识,但真要动手画类图、写代码,或者做选择题的时候,脑子里的概念就开始打架。后来我才意识到,问题不在记忆力,而在我把设计模式当成了孤立的“知识点”去背,没有抓住这门课贯穿始终的主线——可维护性。设计模式不是锦上添花的花架子,它是“软件需要长期演化”这个前提下的必然产物。这篇复习笔记,我想把那些面向可维护性的模式重新串一遍,不只说它们是什么,更想说说它们到底在解决哪个“维护痛点”,以及复习、做题时怎么快速判断该选谁。
如果你是正在复习软件构造课程的学生,或者工作中写代码时总感觉“改一处后面全崩”的开发者,这篇文章应该能帮你把那层窗户纸捅破。
1. 先弄明白:软件构造课里到底什么是“可维护性”
1.1 课本上的定义根本不够用
教科书里对可维护性(Maintainability)的经典定义是“软件被修改的难易程度,包括修正错误、适应环境变化、增加功能的容易程度”。复习的时候如果只看这句话,基本等于没看,因为“难易”没法测量。
我后来把可维护性翻译成了三个更可感知的问题:
- 需求变了,你改动的地方是不是被限制在少数几个类里?
- 新功能加进来,是新增代码还是翻改老代码?
- 改完一个模块,其它模块会不会莫名其妙地“受牵连”?
如果三个问题的答案都指向“封闭”,那代码就是可维护的;如果指向“扩散”,即使类名写得再优雅也是隐患。举个例子,一个电商项目里,订单金额计算原本只支持普通折扣。现在要加一个“满100减20”的优惠。糟糕的做法是把优惠逻辑硬编码进订单类,用一串 if-else 处理;好一点的做法是定义一个折扣策略接口,让订单类只依赖接口,新增优惠就是新增一个实现类。
1.2 可维护性的三个核心诉求:变化、重复、依赖
对着三个问题继续深挖,可以提炼出三个核心诉求。
第一,对变化的隔离。程序里最容易变的东西要跟不容易变的东西分开。比如业务规则经常变,底层通信协议很少变,那就让业务层依赖抽象接口,别直接依赖具体通信实现。一旦协议的细节变了,修改范围被封锁在一个实现类里,而不是顺着调用链牵连十几个文件。
第二,对重复的消灭。如果同一段逻辑在三个地方出现了,那么某个需求改动可能就要同步改三处,漏掉任何一处都会出bug。设计模式里的模板方法模式、策略模式,本质都是在把“重复的行为”收拢到一个可控位置。
第三,对依赖方向的控制。面向对象里有条原则叫“依赖倒置”,通俗讲就是:高层模块不应该直接依赖低层模块,两者都应该依赖抽象。依赖方向控制住了,低层实现随便换,高层逻辑不动摇。维护性好不好,从依赖关系图上基本能一眼看出来——依赖箭头指向稳定抽象的还是指向具体实现的。
1.3 为什么说设计模式是可维护性原则的“代码级落地”
原则(比如SOLID)是世界观,模式是方法论。单一职责原则说“一个类应该只有一个改变的理由”,但怎么写类才能做到?策略模式就是一种落地:把变化的行为抽到接口后面,让原来混杂了多种行为的类只剩下“协调”这一个职责。开闭原则说“对扩展开放、对修改关闭”,怎么落地?装饰器模式就是一种落地:不修改原有类,用包装的方式叠加新能力。
所以复习的时候,我的策略是反过来——先用可维护性的三条诉求当过滤器,再去看每个模式。看到一个模式,先问自己:它是隔离变化的,还是消灭重复的,还是控制依赖方向的?这个问题一旦想清楚了,后面那些名词杂而乱的现象就会消失。因为你会发现,所有面向可维护性的设计模式,本质上都是把“变”和“不变”切分开,让“变”的部分可以被单独替换、单独扩展。
2. 创建对象这件事,为什么值得单独画一章复习
2.1 直接 new 的问题:把具体类型焊死在调用方
很多同学不理解,创建对象不就是 new 一下吗,有什么好研究的?问题在于,new 是Java这类语言里最直接的依赖绑定。写下QiniuUploader uploader = new QiniuUploader();这一行,调用方就和 QiniuUploader 这个具体类绑死了。
不妨想想,如果公司把你原来用的对象存储从A厂商切到B厂商,你需要改多少个地方?如果创建逻辑散落在十几个类里,每次都要找到这十几处然后逐一替换。更麻烦的是,如果有些地方创建时还需要不同的初始化参数(比如不同地域节点),漏改一处就是线上事故。
这就是“创建对象”这个动作背后藏着的维护性危机。复习时我把它概括成一句话:直接 new 就是在调用方身上制造对具体类型的编译期依赖,而这个依赖在需求变更时会变成修改的阻力。
2.2 工厂方法模式:把“选谁”的权利收归一处
工厂方法模式解决的是“调用方不知道该创建哪个具体子类,或者不想关心该创建哪个具体子类”的问题。它把创建动作推迟到子类。
// 反例:调用方既要处理业务,又要决定创建哪个上传器 public class UploadService { public void upload(String content) { // 如果未来新增一种存储实现,这里需要加 if if (Config.STORAGE_TYPE.equals("A")) { new AUploader().upload(content); } else if (Config.STORAGE_TYPE.equals("B")) { new BUploader().upload(content); } } } // 工厂方法模式:创建逻辑封装到专门的工厂类 public interface Uploader { void upload(String content); } public interface UploaderFactory { Uploader create(); } public class AUploaderFactory implements UploaderFactory { @Override public Uploader create() { return new AUploader(); } }调用方只需要依赖UploaderFactory接口,真正选择哪个工厂由外部配置或更上层的逻辑决定。将来接入C厂商,新建CUploader和CUploaderFactory,改动完全不影响UploadService。这就是“变了但不需要修改调用方”的典型效果。
复习提示:工厂方法模式里那个“工厂”本身也是接口,这很重要。因为如果只有一个具体工厂、没有抽象,其实就退化成“简单的静态工厂”,谈不上模式带来的维护性收益。
2.3 抽象工厂模式:让一组对象的创建保持配套
抽象工厂模式比工厂方法多了一层“产品族”的概念。场景往往是:系统里有一组对象需要一起出现,而且它们的“风格/系列”必须一致。比如跨平台UI,Windows下的按钮、文本框、滚动条是一套,Mac下的按钮、文本框、滚动条是另一套。总不能创建了Windows的按钮,配上Mac的文本框。
public interface UIFactory { Button createButton(); TextBox createTextBox(); } public class WindowsUIFactory implements UIFactory { @Override public Button createButton() { return new WindowsButton(); } @Override public TextBox createTextBox() { return new WindowsTextBox(); } }客户端只依赖UIFactory这个接口,不需要关心当前跑在什么平台。平台变了,注入的工厂变了,产品族跟着整体切换。
复习这一模式时,重点看它和工厂方法的区别。工厂方法针对“一个产品等级结构”,抽象工厂针对“多个产品等级结构”的组合。选择题里如果出现“必须在不同操作系统下创建风格一致的一组控件,且客户端不感知具体类”,基本就是抽象工厂。
2.4 Builder模式:当构造参数多到可读性崩坏
还有一种情况会让可维护性下降:构造函数参数太多,调用方根本分不清哪个参数对应哪个含义。比如创建一个MailRequest,可能需要发件人、收件人、标题、正文、附件、优先级、是否回执……如果全用构造器传参,调用处就是一堆需要数括号的魔鬼代码,阅读和出错的成本都很高。
Builder模式用一种渐进式赋值的方式解决这个问题:
MailRequest request = new MailRequest.Builder() .setFrom("from@example.com") .setTo("to@example.com") .setSubject("Weekly Report") .setBody("...") .setPriority(1) .build();每一行都有方法名说明意图,并且可以只设置需要设置的字段,其它字段走默认值。更重要的是,它把“对象创建过程”和“对象最终表示”分离,创建逻辑的变迁不会污染业务代码。
复习这一模式,要记住它解决的是“多参数构造器的可读性”和“参数组合过多时构造函数爆炸”这两个问题。跟工厂的区别在于:工厂重点解决“动态选择类型”,Builder重点解决“复杂对象的逐步构建”。
2.5 复习提示:创建型模式关心的不是性能,而是“将来谁要换实现”
很多人在复习创建型模式时,容易纠结“工厂模式是不是比直接new性能差”。实际上在软件构造的语境下,性能差异几乎可以忽略。真正要关注的是:你这个系统未来是否会替换实现?替换实现时改动的点是聚集的还是分散的?如果项目死定了永远不换存储、不换平台,那用不用工厂都行。可一旦系统要长期演进,把“创建具体对象的决定权”从业务逻辑中剥离出来,就是最划算的一笔投资。
3. 结构性模式:不改动结构,只改变接口关系
3.1 适配器模式:让老接口继续为新人服务
实际项目里最常遇到的尴尬是:代码里依赖了一个接口PaymentProvider,但新接入的第三方SDK只提供PaymentGateway这个五花八门的方法签名,没法直接实现目标接口。如果硬改业务代码去适配第三方,等于把第三方API的细节泄漏得到处都是。
适配器模式就是加一层翻译官:
public class XxxGatewayAdapter implements PaymentProvider { private XxxGateway gateway; public XxxGatewayAdapter(XxxGateway gateway) { this.gateway = gateway; } @Override public void pay(double amount) { gateway.charge(amount, "CNY", "BANK_CARD"); } }业务层只认PaymentProvider.pay(double),底层换成任何第三方,都只需要新增一个Adapter,业务层一行不改。复习时要抓住这个模式的经典信号:“已有接口与目标接口不匹配,但不能或不方便修改已有接口。”
3.2 装饰器模式:用组合代替继承去叠加行为
装饰器模式是我觉得这门课里最“实用主义”的一个,因为Java标准库到处是它的影子:new BufferedInputStream(new FileInputStream(...)),一层包一层,每一种能力都是可选地叠加,而不会污染基础类。
它的核心思想是:不修改原始类,不通过继承爆炸产生子类,而是用“包装”的方式给对象动态添加行为。对维护性来说,最大的好处是符合开闭原则。比如要给一个日志接口增加“加密”功能,再增加“压缩”功能,如果每加一个能力都去改核心类,核心类迟早变成一个巨石;改成装饰器,就是各自写一个包装类。
public class EncryptedLogger implements Logger { private final Logger wrapped; public EncryptedLogger(Logger wrapped) { this.wrapped = wrapped; } @Override public void log(String msg) { wrapped.log(encrypt(msg)); } }使用的时候自由组合:new EncryptedLogger(new TimestampLogger(new FileLogger()))。需要什么能力就包什么能力,不需要的就移除,行为完全正交。
3.3 代理模式:延迟真正的工作,或者给它加一层控制
代理模式和装饰器从类图角度长得很像,都是“持有一个被包装者,实现相同接口”。但意图完全不同:代理模式强调的是控制访问,而不是增强功能。典型场景有远程代理(本地对象代表远程服务)、虚拟代理(延迟加载重量级对象)、保护代理(做权限校验)。
public class VirtualDocumentLoader implements Document { private RealDocument realDoc; private String path; @Override public void display() { if (realDoc == null) { realDoc = new RealDocument(path); // 真正需要时才创建 } realDoc.display(); } }在没有真实对象前,代理可以先占住位置;等真正要操作了,才去创建和调用真实对象。这在维护性上的价值是:调用方完全不知道对象的生命周期和权限控制细节,这些横切关注点被隔离在代理里。
3.4 三者长得像但目标不同:一张表理清适配器、装饰器、代理
复习到第三章时,最常被问到的就是“这三种模式类图都差不多,怎么区分”。我的经验是看它们的动机:
| 对比维度 | 适配器模式 | 装饰器模式 | 代理模式 |
|---|---|---|---|
| 核心动机 | 让不匹配的接口互相兼容 | 动态叠加新行为/职责 | 控制访问、延迟加载、权限保护 |
| 对象关系 | 转换接口 | 保持接口,扩展行为 | 保持接口,限制或管理访问 |
| 是否改变功能 | 改变接口,功能匹配 | 增加行为,功能增强 | 功能本身不变,增加控制 |
| 客户端感知 | 可能感觉接口变了 | 感知不到,透明使用 | 感知不到,透明控制 |
一句话总结:适配器是为了“匹配”,装饰器是为了“叠加”,代理是为了“看守”。
3.5 结构型模式里的“依赖方向”意识
在所有结构性模式背后,其实都藏着一个共同的动作:把正确的依赖方向画出来。适配器让高层依赖自己定义的接口,而不是依赖第三方具体类;装饰器让核心逻辑依赖抽象组件,而不是依赖具体的扩展类;代理让客户端依赖抽象接口,而不是依赖创建和访问细节。写类图练习的时候,可以刻意用箭头标出依赖方向。如果某个具体类被多个高层业务同时依赖,这就是一个“修改时会波及多处”的信号,恰恰是结构性模式准备解决的起点。
4. 行为型模式:把“怎么做”从“做什么”里拆出来
4.1 策略模式:把算法族变成可以替换的零件
如果你写过一段代码,里面有大量 if-else 去判断“不同情况下该用哪种算法”,策略模式就是为这个痛点准备的。它定义了算法族,分别封装起来,让它们可以互相替换,让算法的变化独立于使用算法的客户。
public interface DiscountStrategy { double compute(double price); } class NonVipDiscount implements DiscountStrategy { @Override public double compute(double price) { return price * 0.9; } } class VipDiscount implements DiscountStrategy { @Override public double compute(double price) { return price * 0.8; } } public class OrderService { private DiscountStrategy strategy; public OrderService(DiscountStrategy strategy) { this.strategy = strategy; } public double finalPrice(double price) { return strategy.compute(price); } }之后新增“企业客户折扣”,只需要新增一个EnterpriseDiscount implements DiscountStrategy,然后在构造时注入,OrderService的代码完全不动。维护性的核心价值一目了然:算法本身变成了插件。
4.2 模板方法模式:不变骨架 + 可变步骤
模板方法模式和策略模式经常成对出现,但它们管的不是同一层。策略模式看重“整个算法可以替换”;模板方法模式则是“算法的骨架已经定死,只有某几个步骤允许子类重写”。
public abstract class DataMigrator { public final void migrate() { connect(); extract(); transform(); load(); disconnect(); } protected abstract void extract(); protected abstract void transform(); private void connect() { /* 所有场景一致 */ } private void disconnect() { /* 所有场景一致 */ } }这种设计的维护性体现在:公共流程只需要维护一次,变化点被限制在子类的特定方法里。复习的时候要特别注意,模板方法最典型的场景是“框架类”,比如流程引擎、数据处理管道。它跟策略的类图都有抽象接口,判断依据是:这个变化是发生在“整体步骤能不能换”,还是发生在“固定步骤里某一步怎么做”。
4.3 观察者模式:让消息发布方不关心谁在听
观察者模式是可维护性里“解耦”气质的代表。设想一个订单支付成功事件,接下来要通知的可能是短信、邮件、优惠券、统计系统……如果支付逻辑里直接依次调用这些模块,那么每新增一个接收方,支付代码就得改动一次。更糟的是,通知逻辑和业务逻辑交织在一起。
观察者模式把这种一对多的依赖关系反转过来:一个主题维护多个观察者,主题状态变化时自动通知观察者。
public interface OrderObserver { void onOrderPaid(long orderId); } public class OrderSubject { private final List<OrderObserver> observers = new ArrayList<>(); public void register(OrderObserver observer) { observers.add(observer); } private void notifyObservers(long orderId) { for (OrderObserver observer : observers) { observer.onOrderPaid(orderId); } } }支付逻辑只需要关注OrderSubject.notifyObservers(...),谁在听、有几个听众,支付逻辑一概不管。新增一个微信通知,就是新增一个实现类并注册一下,支付核心代码零修改。复习时值得记住这句话:观察者模式把“发送方”和“接收方”从直接依赖变成通过订阅关系连接。
4.4 状态模式:把状态转换逻辑从 if 海洋里救出来
很多订单类程序写到最后都会变成一团乱麻,因为订单有一堆状态:待支付、已支付、已发货、已完成、已取消。传统写法里,每次操作都要检查“当前状态允不允许这个动作”,于是代码里塞满if (state == PAID && ...)。状态模式换了个思路:把状态本身抽象成类,每个状态对当前状态下允许的行为给出自己的实现。
public interface OrderState { void pay(OrderContext ctx); void ship(OrderContext ctx); } public class UnpaidState implements OrderState { @Override public void pay(OrderContext ctx) { // 完成支付相关逻辑 ctx.setState(new PaidState()); } @Override public void ship(OrderContext ctx) { throw new IllegalStateException("未支付不能发货"); } } public class PaidState implements OrderState { @Override public void pay(OrderContext ctx) { // 已支付状态下再支付,忽略或报错 } @Override public void ship(OrderContext ctx) { ctx.setState(new ShippedState()); } }这样每新增一个状态,只需新增一个状态类;每新增一种动作,改变的是所有状态类的接口。维护性收益在于,那些隐晦的状态流转被显式地画成了一个个独立的类。
4.5 迭代器模式与命令模式:给遍历和请求排队留出扩展位
另外两个容易在考试选择题里出现的模式也值得提一下。
迭代器模式的作用是让客户端以统一的方式遍历容器,而不必知道容器底层是数组、链表还是树。它把“遍历方式”和“集合实现”解耦,将来把数据结构换掉,遍历代码不用改。
命令模式则是把“请求”封装成对象,从而让请求可以被记录、排队、撤销。比如编辑器里的“撤销”功能,如果操作直接写在按钮点击函数里,撤销历史根本无法维护;把所有操作都变成命令对象,撤销历史就是命令对象的一个列表。
这两个模式也都指向同一个道理:把行为的触发方式与行为的具体执行方式分离,变化才能被隔离住。
5. 复习时的重灾区:这些模式总是被搞混
5.1 工厂方法不是“模板方法改名”,两者差异巨大
有种混淆很隐蔽:工厂方法也定义了一个模板方法式的骨架,子类实现create()。但从意图上讲,工厂方法关注“实例化哪一个类”,模板方法关注“固定流程下某一步怎么实现”。一个是创建型模式,一个是行为型模式,只是恰好应用了相同的骨架技巧。做题时看到“延迟到子类决定创建何种对象”,应归为工厂方法;看到“算法骨架固定,可变步骤延迟到子类定制”,则是模板方法。
5.2 判断适配器、装饰器、代理的快速口诀
“缺接口找适配,要叠加找装饰,要做控制找代理。”如果选项里同时出现这三个,先看题目描述里强调的词。
- 题目说“第三方SDK接口与现有系统接口不一致,需要兼容” -> 适配器。
- 题目说“在不修改原类的情况下,动态增强对象功能,并且可以任意组合” -> 装饰器。
- 题目说“权限校验、延迟加载、远程访问” -> 代理。
还有一种出题方式:只描述功能,不写明意图。比如“在访问真实对象前后做额外处理”,这其实是代理的职责,而不是装饰器。因为对代理来说,“额外处理”通常是控制和分发;对装饰器来说,“额外处理”是行为叠加上升,但它的重点不是控制,而是能力扩展。
5.3 策略 vs 状态:一个主动替换,一个被动迁移
策略模式和状态模式看起来都像“把行为放进一个类,再动态切换”。但两者的语义方向相反。
策略模式里的上下文是主动持有策略,按需更换算法,对象本身的状态不影响策略选择。状态模式里的上下文则是被动切换状态,状态转移由内部逻辑决定,外部的操作只是触发条件。一句话:策略是“我选谁”,状态是“我被谁带入下一个状态”。做选择题时,如果题里出现“状态转换条件”、“状态不允许某个操作”这些词,基本可以排除策略。
5.4 用“问题场景→模式→一句话实现”法做考前总结表
这是我复习到最后发现效率最高的方法。不再按模式名称背,而是反过来背“场景”。你可以自己做一张表,我当时总结了大概十几种高频场景。
| 问题场景 | 对应模式 | 一句话实现 |
|---|---|---|
| 调用方不想知道该创建哪个具体产品 | 工厂方法 | 定义一个工厂接口,由子类决定实例化哪个类 |
| 需要创建风格一致的一组产品 | 抽象工厂 | 定义一个产品族工厂接口,每个系列一个实现 |
| 对象构造参数多且容易混淆 | Builder | 提供流式 set 方法,最后统一 build |
| 老接口无法直接被新系统兼容 | 适配器 | 包一层,把目标接口翻译成已有接口 |
| 需要动态叠加功能且避免子类爆炸 | 装饰器 | 用包装类实现同一接口,组合式增强 |
| 访问真实对象之前需要控制或延迟 | 代理 | 用代理类持有目标对象,控制访问时机 |
| 算法族可以互相替换且独立变化 | 策略 | 定义算法接口,运行时注入不同实现 |
| 固定流程中仅个别步骤可变 | 模板方法 | 骨架方法设为 final,可变步骤抽象化 |
| 一个对象状态变化需要通知多方 | 观察者 | 维护观察者列表,状态变时统一通知 |
| 对象行为随内部状态改变而改变 | 状态 | 把每个状态封装成类,状态对象自行决定流转 |
| 需要遍历集合但不想暴露数据结构 | 迭代器 | 通过统一迭代接口访问内部元素 |
| 需要将操作记录、撤销、排队 | 命令 | 把动作封装成命令对象,存入历史列表 |
这张表做完,你就能从一个具体的“维护痛点”迅速跳到对应的模式。考试经常是先描述一个项目困境,问“以下哪个设计模式更适合”,这时候靠这张表匹配会比靠模式特征硬套快得多,也稳得多。
6. 面向可维护性的设计模式,在真实项目里怎么落地
6.1 不要为了模式而模式:先问“这个变化真的会来吗”
说到最后,也是我踩过最深的一个坑:把设计模式当成炫技手段。我刚学完策略模式,看什么 if 都想抽一个策略接口出来;刚学完工厂方法,连一个固定的配置读取都恨不得做两个工厂。
设计模式是有代价的。每多一个接口、多一个抽象层,理解和维护的成本也会上升。如果团队只有三五个模块,功能几乎不变,硬抽出五六层抽象反而降低可维护性。所以真实项目里,我的做法是先问三个问题:
- 这个变化是“近期会被真实触发的”,还是“我预感的”?如果只是预感,别急着上模式。
- 加上抽象后,团队成员能否一眼看懂核心流程?如果画出来的调用链要翻三四个文件才找到具体实现,那这个抽象就是过度设计。
- 是“先写简单版本,等变化出现再重构”还是“现在就上模式”?我现在的倾向是:第一次先写直观的实现,当某种 if-else 第二次出现在附近时,再考虑用模式收拢。这叫“两次才到三次”的原则。
6.2 重构例子:一个支付处理函数的演进
分享一个很朴素但很能说明问题的例子。最初我写了一个支付处理函数,里面直接调用了支付宝SDK、微信SDK、银行卡SDK:
void pay(String channel, double amount) { if (channel.equals("alipay")) { AlipaySDK.getInstance().pay(amount); } else if (channel.equals("wechat")) { WechatSDK.getInstance().pay(amount); } else if (channel.equals("card")) { CardSDK.pay(amount); } }第一次,我新增了“余额支付”,又加了一个 if 分支。此时已经有四个分支了,继续加下去,这个函数会越来越长,任何变动都可能影响其它渠道。于是我抽了一个PaymentStrategy接口,每个渠道一个实现类,原来的支付函数只剩一行strategy.pay(amount)。
这次重构的价值并不在于代码变短,而在于新增渠道不再影响已有渠道,且每个渠道的实现各自封闭,测试也可以精确到单一策略类。这个体验非常直观地解释了为什么设计模式这门课这么强调可维护性。
6.3 考试和面试中,怎么展示你对可维护性的理解
软件构造考试里的设计模式题,通常有三种写法:选择题给场景选模式、画类图补全模式、给一段烂代码让你用模式重构。
做选择题时,别先去想模式名字,先读题里的“变化信号”。比如出现“不同操作系统平台” -> 抽象工厂或工厂方法;出现“算法可以灵活替换” -> 策略;出现“不修改原类 + 动态增加职责” -> 装饰器;出现“一对多状态通知” -> 观察者。
做画图题时,要同时画出接口、具体类、组合关系和依赖箭头。最容易扣分的点是把依赖方向画反。记住高层依赖抽象,低层实现抽象,抽象不依赖细节。
做重构题时,不要只写出最终设计,要说明“原本什么问题会导致维护困难,我的设计为什么解决这个问题”。我在答题时通常写一段“变化分析”,标注出哪些是稳定部分、哪些是可变化部分,然后再给出模式。让老师看到你有可维护性的意识,比只画对类图更拉分。
6.4 关于这门课的复习最终建议
如果时间有限,我会这样排优先级:策略、模板方法、观察者、装饰器、适配器、工厂、状态出现在大题里的概率最高,一定要能徒手画类图并解释动机;抽象工厂、代理、迭代器、命令以选择题和判断题为主,掌握区分特征即可。
复习顺序上,建议先看“可维护性三条诉求”,再看SOLID原则,最后再进入模式。不要一上来就背模式名称,那样很容易迷失。每学一个模式,都亲手做一个小例子,然后问自己:这个模式在隔离哪个变化、减少哪类重复、纠正哪条依赖方向?把这三个问题变成肌肉记忆后,你就不只是在“背设计模式”,而是真正开始“面向可维护性”思考了。
最后再分享一个小技巧:复习时可以把每个模式对应的“坏味道”写出来。比如看到“一个类同时有多个变化维度”就想到策略;看到“构造函数参数过长”就想到Builder;看到“代码里大量if判断对象状态”就想到状态模式。从坏味道出发匹配模式,比从模式出发找场景更接近实战,也更符合软件构造这门课对可维护性的要求。把这些坏味道和模式对照着刷一遍,考前心里会踏实得多。