跳到主要内容

策略模式-Strategy Pattern

序言​

在电商 App 里下单结算时,我们可以在支付宝、微信支付、银行卡之间自由切换,结算逻辑本身完全不变——变的只是"支付方式"这一种算法。如果把支付逻辑写死在结算代码里,每新增一种支付方式都要改结算代码;而把每种支付方式封装成独立策略后,新增支付方式只增加一个类。

策略模式(Strategy Pattern)就是为此而生:定义一族算法,把每个算法封装起来,让它们可以互相替换,算法的变化独立于使用它的客户端。

这种思想在生活中也随处可见:同样是从 A 地去 B 地,可以坐地铁、打车、骑行——路线规划器(客户端)只负责"选择一种出行方式",每种出行方式的实现(策略)各自独立。

定义​

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

定义一系列算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。

三个关键点:

  1. 一族算法:多个算法完成相似的目标,但实现不同;
  2. 封装 + 可替换:每个算法封装成一个类,客户端可以在运行时更换;
  3. 独立变化:算法的实现细节对客户端完全隐藏,新增算法不影响客户端。

结构​

让我们从一个问题开始​

假设购物车结算时支持支付宝、微信支付、银行卡三种支付方式。不用策略模式,最直接的写法是:

public class Order {

public void pay(String paymentType, double amount) {
if ("ALIPAY".equals(paymentType)) {
System.out.println("使用支付宝支付 " + amount + " 元");
// 支付宝签名、回调……一堆逻辑
} else if ("WECHAT".equals(paymentType)) {
System.out.println("使用微信支付 " + amount + " 元");
// 微信统一下单……一堆逻辑
} else if ("BANK_CARD".equals(paymentType)) {
System.out.println("使用银行卡支付 " + amount + " 元");
// 银行卡鉴权……一堆逻辑
} else {
throw new IllegalArgumentException("不支持的支付方式:" + paymentType);
}
}
}

问题显而易见:

  1. pay 方法越来越长,每种支付方式的实现细节(签名、回调、对接 SDK)全堆在一起;
  2. 新增支付方式(如"云闪付")必须修改 Order 类,违反开闭原则;
  3. 支付方式的实现无法独立测试、独立复用。

策略模式的结构​

把每种支付方式封装成一个策略类,订单(Context)持有策略接口:

各个角色的职责如下:

  • Strategy(抽象策略):定义所有算法的公共接口,对应图中的 PayStrategy。
  • ConcreteStrategy(具体策略):实现某个具体的算法,对应图中的 AlipayStrategy、WechatPayStrategy、BankCardStrategy。
  • Context(上下文):持有一个策略引用,把算法调用委托给策略对象,并允许运行时更换策略,对应图中的 Order。
  • 客户端负责选择合适的策略注入 Context。

关键设计:Context 只依赖 PayStrategy 接口,不知道任何具体策略的存在。新增支付方式 = 新增一个策略类,Order 和已有策略一行不改。

代码实现​

我们来实现购物车的支付策略。

定义策略接口​

public interface PayStrategy {
void pay(double amount);
}

定义具体策略​

public class AlipayStrategy implements PayStrategy {

@Override
public void pay(double amount) {
// 模拟支付宝支付流程
System.out.println("正在通过支付宝支付 " + amount + " 元……");
System.out.println("支付宝支付成功");
}
}
public class WechatPayStrategy implements PayStrategy {

@Override
public void pay(double amount) {
System.out.println("正在通过微信支付 " + amount + " 元……");
System.out.println("微信支付成功");
}
}
public class BankCardStrategy implements PayStrategy {

@Override
public void pay(double amount) {
System.out.println("正在通过银行卡支付 " + amount + " 元……");
System.out.println("银行卡支付成功");
}
}

定义上下文(订单)​

public class Order {

private final String orderId;
private PayStrategy payStrategy;

public Order(String orderId) {
this.orderId = orderId;
}

// 运行时更换策略
public void setPayStrategy(PayStrategy payStrategy) {
this.payStrategy = payStrategy;
}

public void checkout(double amount) {
System.out.println("订单 " + orderId + " 结算中……");
// 把支付委托给策略对象
payStrategy.pay(amount);
}
}

客户端使用​

public class Main {

public static void main(String[] args) {
Order order = new Order("202410150001");

// 默认使用支付宝
order.setPayStrategy(new AlipayStrategy());
order.checkout(199.00);

System.out.println("=========================");

// 运行时切换到微信支付
order.setPayStrategy(new WechatPayStrategy());
order.checkout(99.00);

System.out.println("=========================");

// 切换银行卡
order.setPayStrategy(new BankCardStrategy());
order.checkout(50.00);
}
}

输出结果:

订单 202410150001 结算中……
正在通过支付宝支付 199.0 元……
支付宝支付成功
=========================
订单 202410150001 结算中……
正在通过微信支付 99.0 元……
微信支付成功
=========================
订单 202410150001 结算中……
正在通过银行卡支付 50.0 元……
银行卡支付成功

核心收益:

  1. 结算流程(Context)与支付实现(策略)完全解耦;
  2. 新增"云闪付"只需新增一个 CloudQuickPayStrategy 类,无需修改任何现有代码;
  3. 每种支付策略可以独立测试、独立复用。

结合工厂:消除客户端的选择逻辑​

实际项目中,策略往往配合**工厂模式(Factory)**使用:客户端不直接 new 具体策略,而是交给工厂根据类型创建:

public class PayStrategyFactory {

public static PayStrategy create(String type) {
switch (type) {
case "ALIPAY":
return new AlipayStrategy();
case "WECHAT":
return new WechatPayStrategy();
case "BANK_CARD":
return new BankCardStrategy();
default:
throw new IllegalArgumentException("不支持的支付方式:" + type);
}
}
}

// 客户端:只需要传一个类型字符串
Order order = new Order("202410150001");
order.setPayStrategy(PayStrategyFactory.create("WECHAT"));
order.checkout(99.00);

更进一步,Spring 项目中可以用 Map<String, PayStrategy> 注入所有策略 Bean,用策略名做 key,实现零 switch 的策略分发。

在开源框架中的应用​

java.util.Comparator:源码视角的排序策略​

java.util.Comparator 是策略模式在 JDK 中最典型的应用。从源码层面看,JDK 的排序体系就是一个完整的策略模式:

  • Strategy(抽象策略) = Comparator<T> 接口,只声明一个 compare(T o1, T o2) 方法;
  • ConcreteStrategy(具体策略) = 各种 Comparator 实现(comparingInt、reversed、自定义比较器),由调用方在运行时传入;
  • Context(上下文) = Collections.sort / List.sort / Arrays.sort,它们只面向 Comparator 接口调用,完全不关心具体比较逻辑。

策略接口:Comparator​

// java.util.Comparator(源码)
@FunctionalInterface
public interface Comparator<T> {
// 策略的核心方法:比较两个对象
int compare(T o1, T o2);
...
}

Context 源码:List.sort 如何把策略传入排序算法​

List.sort 是接口的 default 方法,它把元素转为数组后交给 Arrays.sort,排序完成后回写:

// java.util.List.sort(源码)
@SuppressWarnings({"unchecked", "rawtypes"})
default void sort(Comparator<? super E> c) {
// 把元素复制为数组
Object[] a = this.toArray();
// 把比较策略传给排序算法
Arrays.sort(a, (Comparator) c);
// 把排好序的元素写回 List
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}

而 Collections.sort 只是转发到 List.sort:

// java.util.Collections.sort(源码)
@SuppressWarnings({"unchecked", "rawtypes"})
public static <T> void sort(List<T> list, Comparator<? super T> c) {
list.sort(c); // 委托给 List.sort
}

排序算法内部:把比较委托给策略对象​

Arrays.sort 对对象数组使用 TimSort,排序算法的骨架(比较、交换、归并)固定不变,唯一的"算法差异点"全部收敛到 c.compare(...) 的调用上:

// java.util.Arrays.sort(源码)
public static <T> void sort(T[] a, Comparator<? super T> c) {
...
TimSort.sort(a, 0, a.length, c, null, 0, 0);
}

// java.util.TimSort.countRunAndMakeAscending(源码骨架:判断升序/降序段)
private static <T> int countRunAndMakeAscending(T[] a, int lo, int hi, Comparator<? super T> c) {
int runHi = lo + 1;
if (runHi == hi) {
return 1;
}
// 唯一的算法差异点:调用策略对象 c 的比较方法
if (c.compare(a[runHi++], a[lo]) < 0) { // 降序段
while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0) {
runHi++;
}
reverseRange(a, lo, runHi);
} else { // 升序段
while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0) {
runHi++;
}
}
return runHi - lo;
}

关键点:TimSort 的排序骨架(归并排序 + 自然段优化)对"按年龄排"还是"按姓名排"完全无感——它只调用 c.compare(a, b)。具体策略(升序/降序/按年龄/按姓名)由调用方传入的 Comparator 决定。这就是策略模式的经典分工:算法骨架固定,比较策略可替换。

Spring:ResourceLoader 的源码策略分发​

Spring 框架中处处是策略模式的实践。以 DefaultResourceLoader 为例,getResource(String location) 是策略选择的"分发器":根据位置字符串的前缀,选择不同的 Resource 策略实现(ClassPathResource / UrlResource / FileUrlResource / 路径资源):

// org.springframework.core.io.DefaultResourceLoader.getResource(源码骨架)
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null");

// 自定义协议解析器(扩展点)
for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
Resource resource = protocolResolver.resolve(location, this);
if (resource != null) {
return resource;
}
}

if (location.startsWith("/")) {
// 以 "/" 开头:按文件系统路径解析
return getResourceByPath(location);
} else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
// "classpath:" 前缀:选择 ClassPathResource 策略
return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
} else {
try {
// 尝试作为 URL 解析:选择 UrlResource / FileUrlResource 策略
URL url = new URL(location);
return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
} catch (MalformedURLException ex) {
// 都不是:退回路径资源策略
return getResourceByPath(location);
}
}
}
  • Strategy = Resource 接口,统一声明 getInputStream()、getDescription() 等方法;
  • ConcreteStrategy = ClassPathResource(类路径读取)、UrlResource(URL 读取)、FileUrlResource(文件 URL)、FileSystemResource(文件系统);
  • Context = ResourceLoader/DefaultResourceLoader,只面向 Resource 接口返回结果,客户端拿到统一的 Resource 对象再调用。

PlatformTransactionManager(DataSourceTransactionManager / JpaTransactionManager)、MessageConverter(JSON/XML 消息转换)等遵循同样的套路:接口定义算法契约,多个实现类封装不同算法,Context 面向接口调用,运行时按需选择。

适用场景​

  • 当同一业务存在多种算法/方式(支付、排序、压缩、路由、定价),且希望在运行时切换时,策略模式可以消除大量条件分支。
  • 当算法实现复杂且相互独立,希望各自封装、独立测试、独立演进时,策略模式把每个算法隔离成类。
  • 当算法经常变化或需要动态扩展(开闭原则)时,新增策略类即可,无需修改 Context。
  • 注意:如果算法数量很少且几乎不变,或者算法之间的差异只是几行代码,直接用条件判断反而更简单;策略模式会引入较多类。

与其他设计模式的关系​

  • 策略模式与状态模式(State)结构几乎相同,但意图相反:策略的多个实现互不相关、可替换,由客户端主动选择;状态的多个实现彼此关联、自动流转,由状态自身驱动。策略是"算法可替换",状态是"状态可流转"。
  • 模板方法模式(Template Method)与策略都封装了算法,但模板方法用继承固定算法骨架(子类覆写步骤),策略用组合替换整个算法(客户端注入)。
  • 策略对象常常是无状态的,可以用单例模式(Singleton)共享;策略的创建可以交给工厂方法模式(Factory Method)。
  • 多个策略串联处理同一请求时,可结合责任链模式(Chain of Responsibility)。

总结​

策略模式用"组合"取代"条件分支",把算法族封装成可替换的策略对象:

  • Context 面向策略接口编程,运行时自由更换算法;
  • 新增算法只需新增策略类,完全符合开闭原则;
  • 每个算法独立封装、独立测试、独立复用。

策略模式的缺点是:策略类数量会随算法数量增长;客户端需要了解各种策略的差异才能做出选择(可通过工厂或默认策略缓解)。作为"用组合代替继承、用多态消灭分支"的代表模式,策略模式是重构 if-else 泛滥代码的第一选择。