设计模式目录
一、前言
设计模式是针对反复出现的设计问题给出的解决方案。它关心类和对象之间如何划分职责、如何协作,使需求变化时只需修改少量代码;它描述的是解决思路,而不是可以直接复制的代码。
学习设计模式,一方面可以复用前人总结的设计经验,另一方面在讨论设计时可以用“策略”“装饰”等名词代替大段的描述。
四人组(Gang of Four, GoF)在《设计模式》中把 23 种模式分为三类:
- 创建型关注对象如何被创建,使创建与使用分离,常用的有单例、工厂方法和生成器;
- 结构型关注类和对象如何组合成更大的结构,常用的有适配器、装饰和代理;
- 行为型关注对象之间如何分配职责和通信,常用的有策略、观察者、责任链和模板方法。
JDK 中,Runtime.getRuntime() 是单例,Calendar.getInstance() 是静态工厂方法,BufferedInputStream 包装其它输入流以增加缓冲,属于装饰。Arrays.asList() 把数组适配为 List,属于适配器;java.lang.reflect.Proxy 在运行时生成代理对象。
集合框架中,List.sort() 传入的 Comparator 是策略,换一个 Comparator 就换一种排序规则;继承 AbstractList 只需实现 get() 和 size(),indexOf()、contains() 等方法由父类基于这两个方法实现,属于模板方法;Collection#iterator() 由各实现类决定返回哪种迭代器,属于工厂方法。
Spring 中的用法见 Spring 中的设计模式。
后面的目录中,标为“选读”的模式在面试中很少单独问到,了解意图即可。
项目中的用法
设计模式针对的是变化:某处代码会随着需求不断增加分支或步骤时,用模式把变化的部分隔离出来,以后只需新增代码而不必修改已有的代码。以电商的下单流程为例,几个常用模式各自用在:
| 环节 | 变化点 | 模式 |
|---|---|---|
| 下单前的校验 | 校验项不断增加:参数、限购、黑名单、库存 | 责任链 |
| 计算运费、选择支付渠道 | 同一件事有多种算法,按类型选择其中一种 | 策略加简单工厂 |
| 接收多家物流公司的回调 | 步骤固定(验签、幂等、处理、应答),只有处理的部分不同 | 模板方法 |
| 支付成功后加积分、发短信 | 接收方不断增加,发布方不想知道有哪些接收方 | 观察者,即 Spring 的事件 |
| 事务、分布式锁、日志 | 与业务无关、很多方法都要加的逻辑 | 代理,即 Spring AOP |
以下单前的校验为例,每个校验项是一个处理者,按顺序执行,任何一个不通过就抛出异常,后面的不再执行:
public interface OrderValidator {
void check(OrderRequest req); // 不通过时抛出异常,链条到此为止
}
@Component @Order(1)
public class ParamValidator implements OrderValidator {
public void check(OrderRequest req) {
if (req.count() <= 0) throw new IllegalArgumentException("数量必须大于 0");
}
}
@Component @Order(2)
public class LimitValidator implements OrderValidator {
public void check(OrderRequest req) {
if (req.count() > 2) throw new IllegalStateException("每人限购 2 件");
}
}
@Service
public class OrderService {
private final List<OrderValidator> validators; // Spring 按 @Order 排好序注入
public OrderService(List<OrderValidator> validators) {
this.validators = validators;
}
public void create(OrderRequest req) {
validators.forEach(v -> v.check(req));
// 校验全部通过,创建订单
}
}
再加上库存校验(@Order(3)),购买 3 件时依次执行参数校验、限购校验,抛出“每人限购 2 件”,库存校验不再执行(Spring Boot 3.3 实测,见附录 A1)。新增黑名单校验只需要再写一个带 @Component 的实现类,OrderService 不用修改。
需要注意的是,链上的处理者最好只做校验这类不修改数据的事。如果某个处理者已经扣了优惠券,后面的处理者再失败,前面的修改不会自动撤销,需要把整条链放在一个事务中,或者由处理者提供补偿操作。链条变长以后,请求在哪一环被拦下也要靠日志才能查清。
几个容易混淆的模式:
- 策略和工厂常一起出现:工厂决定取得哪一个对象,策略决定执行哪一种算法,见与简单工厂和 Spring 容器结合;
- 三种工厂:简单工厂由一个类按参数创建各种产品,新增产品要修改它;工厂方法由子类决定创建哪一种,新增产品只需新增子类;抽象工厂创建一组必须一起使用的相关对象;
- 责任链中的请求依次经过多个处理者,策略只选择其中一个算法执行;
- 策略和状态的类图相似,策略通常由客户端选择并替换,状态随运行中的状态转移而切换(转移逻辑写在状态对象或 Context 中),见与状态模式的比较;
- 代理和装饰的结构相同,都实现被包装对象的接口并持有它的引用,代理用来控制访问,装饰用来叠加功能,见代理。
需要注意的是,模式不是用得越多越好。只有两三个分支、短期内不会再增加的 if-else 不需要改成策略,引入模式的理由应该是已经出现或可以预见的变化。
二、创建型
三、行为型
- 责任链.md
- 命令.md(选读)
- 解释器.md(选读)
- 迭代器.md(选读)
- 中介者.md(选读)
- 备忘录.md(选读)
- 观察者.md
- 状态.md(选读)
- 策略.md
- 模板方法.md
- 访问者.md(选读)
- 空对象.md(选读)
四、结构型
附录:实测
A1 下单校验的责任链
Spring Boot 3.3.5(Spring Framework 6.1.14)、JDK 21,Apple M1 Pro。三个校验器与正文相同,库存校验 StockValidator 标注 @Order(3)、只打印一行;每个校验器先打印自己的名称。用 @Import 注册时故意按库存、参数、限购的顺序列出,注入的 List 仍按 @Order 排序。依次以购买 1 件和 3 件调用 create():
== 购买 1 件
参数校验
限购校验
库存校验
创建订单
== 购买 3 件
参数校验
限购校验
下单失败:每人限购 2 件
参考资料
- 弗里曼. Head First 设计模式 [M]. 中国电力出版社, 2007.
- Gamma E. 设计模式:可复用面向对象软件的基础 [M]. 机械工业出版社, 2007.
- Bloch J. Effective Java [M]. Addison-Wesley Professional, 2017.
- Design Patterns
- Design patterns implemented in Java
- The breakdown of design patterns in JDK
- Spring Framework: Using @Autowired