Merge pull request #1495 from CyC2018/update-strategy-pattern

Update 设计模式 - 目录.md and 设计模式 - 策略.md
This commit is contained in:
CyC2018
2026-10-28 13:39:23 +08:00
committed by GitHub
2 changed files with 204 additions and 8 deletions
+15 -3
View File
@@ -2,7 +2,19 @@
## 一、前言
设计模式是解决问题的方案,学习现有的设计模式可以做到经验复用。拥有设计模式词汇,在沟通时就能用更少的词汇来讨论,并且不需要了解底层细节。
设计模式是针对反复出现的设计问题给出的解决方案。它关心类和对象之间如何划分职责、如何协作,使需求变化时只需修改少量代码;它描述的是解决思路,而不是可以直接复制的代码。
学习设计模式,一方面可以复用前人总结的设计经验,另一方面在讨论设计时可以用“策略”“装饰”等名词代替大段的描述。
四人组(Gang of Four, GoF)在《设计模式》中把 23 种模式分为三类:
- 创建型关注对象如何被创建,使创建与使用分离,常用的有[单例](设计模式%20%20-%20单例.md)、[工厂方法](设计模式%20-%20工厂方法.md)和[生成器](设计模式%20-%20生成器.md);
- 结构型关注类和对象如何组合成更大的结构,常用的有[适配器](设计模式%20-%20适配器.md)、[装饰](设计模式%20-%20装饰.md)和[代理](设计模式%20-%20代理.md);
- 行为型关注对象之间如何分配职责和通信,常用的有[策略](设计模式%20-%20策略.md)、[观察者](设计模式%20-%20观察者.md)、[责任链](设计模式%20-%20责任链.md)和[模板方法](设计模式%20-%20模板方法.md)。
JDK 中,Runtime.getRuntime() 是单例,Calendar.getInstance() 是静态工厂方法,BufferedInputStream 包装其它输入流以增加缓冲,属于装饰器。Arrays.asList() 把数组适配为 List,属于适配器;java.lang.reflect.Proxy 在运行时生成代理对象。
Spring 中的用法见 [Spring 中的设计模式](Spring.md#七spring-中的设计模式)。
## 二、创建型
@@ -41,8 +53,8 @@
## 参考资料
- 弗里曼. Head First 设计模式 [M]. 中国电力出版社, 2007.
- Gamma E. 设计模式: 可复用面向对象软件的基础 [M]. 机械工业出版社, 2007.
- Bloch J. Effective java[M]. Addison-Wesley Professional, 2017.
- Gamma E. 设计模式:可复用面向对象软件的基础 [M]. 机械工业出版社, 2007.
- Bloch J. Effective Java [M]. Addison-Wesley Professional, 2017.
- [Design Patterns](http://www.oodesign.com/)
- [Design patterns implemented in Java](http://java-design-patterns.com/)
- [The breakdown of design patterns in JDK](http://www.programering.com/a/MTNxAzMwATY.html)
+189 -5
View File
@@ -15,9 +15,11 @@
### 与状态模式的比较
状态模式的类图和策略模式类似,并且都是能够动态改变对象的行为。但是状态模式是通过状态转移来改变 Context 所组合的 State 对象,而策略模式是通过 Context 本身的决策来改变组合的 Strategy 对象。所谓的状态转移,是指 Context 在运行过程中由于一些条件发生改变而使得 State 对象发生改变,注意必须要是在运行过程中。
状态模式和策略模式的类图相似,都是通过组合一个接口对象来动态改变行为,区别在于这个对象是如何被切换的。
状态模式主要是用来解决状态转移的问题,当状态发生转移了,那么 Context 对象就会改变它的行为;而策略模式主要是用来封装一组可以互相替代的算法族,并且可以根据需要动态地去替换 Context 使用的算法。
状态模式的 State 对象随运行过程中的状态转移而切换,转移由状态对象或 Context 的内部逻辑触发。例如订单处于“待支付”时调用 pay() 会完成支付,进入“已支付”后再调用 pay() 会被拒绝。
策略模式的 Strategy 对象由客户端创建并交给 Context,之后客户端通过 Context 使用它,需要时再替换为另一个策略。例如先为订单设置“标准快递”的运费计算策略,用户改选“加急”后替换为加急策略。
### 实现
设计一个鸭子,它可以动态地改变叫声。这里的算法族是鸭子的叫声行为。
@@ -81,12 +83,194 @@ squeak!
quack!
```
#### 1. 用策略替代 if-else
业务代码中,同一件事常按类型分别处理,例如按配送方式计算运费。这里的规则是:标准快递首千克 10 元,之后每千克加 3 元;加急快递首千克 25 元,之后每千克加 6 元;到店自提免运费。调用方先把重量向上取整为千克数 kg,例如 2.5 kg 取 3。
```java
public class IfElseFreightService {
public int freight(String type, int kg) {
if ("STANDARD".equals(type)) {
return 10 + (kg - 1) * 3;
} else if ("EXPRESS".equals(type)) {
return 25 + (kg - 1) * 6;
} else if ("PICKUP".equals(type)) {
return 0;
}
throw new IllegalArgumentException("未知配送方式: " + type);
}
}
```
这种写法的问题是:每新增一种配送方式都要修改 freight(),各分支的计费规则耦合在一个方法中,测试时也只能整体验证。把每个分支拆成一个策略类后,接口为:
```java
public interface FreightStrategy {
int fee(int kg);
}
```
每个实现类只包含一种计费规则。FreightStrategy 是 Strategy,三个实现类是 ConcreteStrategy:
```java
public class StandardFreight implements FreightStrategy {
@Override
public int fee(int kg) {
return 10 + (kg - 1) * 3;
}
}
public class ExpressFreight implements FreightStrategy {
@Override
public int fee(int kg) {
return 25 + (kg - 1) * 6;
}
}
public class PickupFreight implements FreightStrategy {
@Override
public int fee(int kg) {
return 0;
}
}
```
同样的结构也适用于支付渠道、消息推送渠道等场景,只是各实现类的内容换成对应的处理逻辑。
#### 2. 与简单工厂和 Spring 容器结合
策略模式还要解决按类型找到策略对象的问题,这一步与[简单工厂](设计模式%20-%20简单工厂.md)相同:工厂根据类型返回对象,再由对象执行计算。两者的分工是:工厂决定取得哪一个对象,策略决定执行哪一种算法。用 Map 保存类型与策略的对应关系,即可实现这个工厂:
```java
import java.util.Map;
public class FreightStrategyFactory {
private static final Map<String, FreightStrategy> STRATEGIES = Map.of(
"STANDARD", new StandardFreight(),
"EXPRESS", new ExpressFreight(),
"PICKUP", new PickupFreight());
public static FreightStrategy of(String type) {
FreightStrategy strategy = STRATEGIES.get(type);
if (strategy == null) {
throw new IllegalArgumentException("未知配送方式: " + type);
}
return strategy;
}
}
```
调用方式为 `FreightStrategyFactory.of("EXPRESS").fee(3)`,结果是 37,即 25 + (3 - 1) * 6。
在 Spring 中可以不用手写 Map。容器会把实现同一接口的 Bean 收集为 Map<String, 接口>,key 是 Bean 名称(见[附录 A2](#a2-spring-中的-map-注入))。实现类用 @Component 指定名称,名称即为配送方式的编码:
```java
import org.springframework.stereotype.Component;
@Component("STANDARD")
public class StandardFreight implements FreightStrategy {
@Override
public int fee(int kg) {
return 10 + (kg - 1) * 3;
}
}
```
ExpressFreight 和 PickupFreight 分别标注为 `@Component("EXPRESS")` 和 `@Component("PICKUP")`,FreightService 的写法如下:
```java
import java.util.Map;
import org.springframework.stereotype.Service;
@Service
public class FreightService {
private final Map<String, FreightStrategy> strategies;
public FreightService(Map<String, FreightStrategy> strategies) {
this.strategies = strategies;
}
public int freight(String type, int kg) {
FreightStrategy strategy = strategies.get(type);
if (strategy == null) {
throw new IllegalArgumentException("未知配送方式: " + type);
}
return strategy.fee(kg);
}
}
```
新增配送方式时只需新增一个带 @Component 的实现类,FreightService 不需要改动。实测中新增 `@Component("SAME_CITY")` 的类后,注入的 Map 多出了 SAME_CITY 这个 key(见[附录 A2](#a2-spring-中的-map-注入))。
#### 3. 优缺点与注意事项
优点:
- 新增算法时只需新增类,已有的策略和调用方不需要修改,符合开闭原则;
- 每个策略可以单独测试。
缺点:
- 算法较多时类的数量随之增加,即常说的类爆炸问题;
- 调用方要先选出策略,通常由工厂或 Map 负责。
注意事项:
- 策略接口只有一个方法、实现又很简单时,可以用 Lambda 代替实现类以减少类的数量,例如 `Map<String, IntUnaryOperator>` 的值写成 `kg -> 25 + (kg - 1) * 6`([附录 A1](#a1-运费策略与-if-else-对拍)中验证了这种写法);
- 模板方法通过继承固定算法的骨架,由子类覆写其中的步骤;策略通过组合替换整个算法;
- 责任链中的请求依次经过多个处理者,策略则只选择一个算法执行;
- 代理模式与策略模式的类图相似,都是接口加组合,但代理的目的是控制对真实对象的访问,策略的目的是替换算法;
- 用策略替代已有的 if-else 时,可以像[附录 A1](#a1-运费策略与-if-else-对拍)那样用同一组输入对比新旧实现的结果,确认一致后再切换。
### JDK 中的应用
- java.util.Comparator#compare()
- javax.servlet.http.HttpServlet
- javax.servlet.Filter#doFilter()
- java.util.concurrent.RejectedExecutionHandler#rejectedExecution():ThreadPoolExecutor 无法接收任务时调用它,AbortPolicy、CallerRunsPolicy、DiscardPolicy 和 DiscardOldestPolicy 是它的四种实现,通过构造方法或 setRejectedExecutionHandler() 设置。
## 附录:实测
### A1 运费策略与 if-else 对拍
环境:OpenJDK 21.0.13,macOS 15.8.1(arm64)。
用正文中的 IfElseFreightService、FreightStrategy、三个策略实现类和 FreightStrategyFactory 编译,另写一个对拍程序:对 STANDARD、EXPRESS、PICKUP、SAME_CITY 四种类型,各取 kg 为 1~20,共 80 组,比较 if-else 版本、工厂版本和 Lambda 版本的返回值与异常信息。
```text
javac -encoding UTF-8 -d out *.java
java -cp out Main
```
原始输出:
```text
checked=80 mismatch=0
2.5 kg 按 3 kg 计: STANDARD=16 EXPRESS=37 PICKUP=0
lambda EXPRESS(3)=37
unknown: 未知配送方式: SAME_CITY
```
为确认对拍能够发现错误,分别注入两处错误后重跑:工厂中 EXPRESS 对应 StandardFreight,mismatch=20;标准快递的计算改为 10 + kg * 3(续重少减 1 千克),mismatch=20。
### A2 Spring 中的 Map 注入
环境:Spring Boot 3.5.16(Spring Framework 6.2.19),JDK 21.0.13,Maven 3.9.16,macOS 15.8.1(arm64)。
最小项目包含 FreightStrategy 接口、三个带 @Component 的实现类、FreightService(构造器注入 Map<String, FreightStrategy>),以及用 ApplicationRunner 打印结果的启动类。运行输出:
```text
Map 注入的 key(Bean 名): [EXPRESS, PICKUP, STANDARD]
2.5 kg 按 3 kg 计: STANDARD=16 EXPRESS=37 PICKUP=0
unknown: 未知配送方式: SAME_CITY
```
扩展实验:新增 `@Component("SAME_CITY")` 的实现类,FreightService 不做修改,输出的 key 变为 [EXPRESS, PICKUP, SAME_CITY, STANDARD],SAME_CITY 可以正常计算。Spring 官方文档说明,Map 注入时值为该类型的所有 Bean,key 为对应的 Bean 名称。
## 参考资料
- 弗里曼. Head First 设计模式 [M]. 中国电力出版社, 2007.
- Gamma E. 设计模式: 可复用面向对象软件的基础 [M]. 机械工业出版社, 2007.
- Gamma E. 设计模式:可复用面向对象软件的基础 [M]. 机械工业出版社, 2007.
- [Using @Autowired](https://docs.spring.io/spring-framework/reference/core/beans/annotation-config/autowired.html)