策略模式精讲+实战
策略模式(Strategy Pattern)精讲 —— 结合 Spring 实战
1. 官方标准定义(GoF 原话)
定义一组算法,将每个算法都封装起来,并且使它们之间可以互相替换。该模式让算法的变化独立于使用算法的客户端(调用方)。核心原则:针对接口编程,而不是针对实现编程。策略模式(Strategy Pattern)是 GoF 23 种设计模式中的行为型模式。
2. 一句话"人话"解读
它是一个策略路由器。你只需要告诉上下文(Context)你想要什么"策略标识"(比如"ALIPAY"),它就会自动把对应的"技能/算法"(比如支付宝支付逻辑)拿出来执行,而调用方完全不用关心底层是怎么if-else判断类型的。
3. 模式的核心三要素(结构定义)
为满足上面的定义,模式必须包含以下 3 个角色:
| 角色 | 职责 | 对应代码 |
|---|---|---|
| 抽象策略(Strategy) | 定义一个公共接口,所有算法都遵循这个"合同" | PaymentStrategy接口 |
| 具体策略(ConcreteStrategy) | 实现接口的具体类,封装了具体的业务算法 | AlipayStrategy、WechatStrategy |
| 上下文(Context) | 持有策略引用,负责根据条件路由到具体策略 | PaymentContext |
设计原则
- 封装变化:找出应用中可能需要变化的部分,把它们独立出来
- 面向接口编程:策略的具体实现依赖接口,而不是依赖实现类
- 组合优于继承:Context 通过组合持有一个 Strategy 引用,而不是继承具体策略
4. 为什么这个定义很重要?(结合 Spring 场景)
- "互相替换":因为 Spring 把策略类注入了
Map<String, Strategy>,只要type匹配,随时可以替换某个策略的实现类,调用方完全无感知。新增策略时,客户端代码完全不用改,完美符合"开闭原则"。 - "变化独立于客户端":客户端(Controller)只需要传
"WECHAT",不需要知道微信支付类是否被 AOP 代理了。无论注入的是原始类还是代理类,代码都能正常运行——这就是接口编程的威力。
5. 代码实战(解决支付场景)
假设有一个支付系统,支持支付宝、微信等多种渠道。我们将使用 Spring 的依赖注入特性来优雅实现。
5.1 原始反面案例(使用instanceof)
// ❌ 反面教材:每当新增支付方式,都要修改此处代码,违反开闭原则 public String pay(Object channel) { if (channel instanceof Alipay) { // 支付宝逻辑 } else if (channel instanceof WechatPay) { // 微信逻辑 } throw new UnsupportedOperationException(); }5.2 重构为策略模式(优雅实现)
第一步:定义策略接口
public interface PaymentStrategy { // 策略标识(用于前端传参,替代 instanceof 判断) String getType(); // 执行支付 void pay(Double amount); }第二步:实现具体策略(交给 Spring 管理)
@Component public class AlipayStrategy implements PaymentStrategy { @Override public String getType() { return "ALIPAY"; } @Override public void pay(Double amount) { System.out.println("使用支付宝支付:" + amount); // 此处调用支付宝 SDK... } } @Component public class WechatStrategy implements PaymentStrategy { @Override public String getType() { return "WECHAT"; } @Override public void pay(Double amount) { System.out.println("使用微信支付:" + amount); // 此处调用微信 SDK... } }第三步:创建策略上下文(核心工厂)
利用 Spring 的构造函数注入,将所有的策略List自动转换为Map<String, Strategy>。此时已彻底消灭instanceof。
@Component public class PaymentContext { private final Map<String, PaymentStrategy> strategyMap; @Autowired public PaymentContext(List<PaymentStrategy> strategies) { // 将 List 转为 Map,Key 为策略自定义的类型标识 this.strategyMap = strategies.stream() .collect(Collectors.toMap(PaymentStrategy::getType, Function.identity())); } // 对外统一调用接口 public void executePay(String type, Double amount) { PaymentStrategy strategy = strategyMap.get(type); if (strategy == null) { throw new IllegalArgumentException("不支持的支付方式: " + type); } strategy.pay(amount); } }第四步:Controller 层调用
@RestController @RequestMapping("/api/pay") public class PayController { @Autowired private PaymentContext paymentContext; @PostMapping public String pay(@RequestParam String type, @RequestParam Double amount) { // 前端传入 "ALIPAY" 或 "WECHAT",无需任何类型判断 paymentContext.executePay(type, amount); return "支付成功"; } }6. 策略模式 vsinstanceof的优势
| 对比维度 | if-else+instanceof | 策略模式(Map 路由) |
|---|---|---|
| 开闭原则 | 违反(新增渠道需修改原有类) | 符合(新增渠道只需新建类加@Component) |
| 代码可读性 | 随着分支增多,逻辑臃肿难维护 | 路由清晰,业务逻辑内聚在各自类中 |
| 测试难度 | 需要 mock 大量无关分支 | 可单独测试每个策略类 |
| 运行时性能 | 链式判断,O(N) 复杂度 | Map 哈希查找,O(1) 复杂度 |
7. 联动避坑(Spring 代理问题)
如果具体策略类(如AlipayStrategy)本身被@Transactional或@Cacheable注解,Spring 会为其生成代理对象。此时直接通过new创建实例会失去代理能力,必须通过 Spring 容器获取策略实例。这正是策略上下文(Context)通过依赖注入管理策略实例的重要原因。