策略模式 (Strategy) —— 条条大路通罗马
前言
在深入探讨策略模式之前,想向大家推荐一个非常棒的开源项目:design-patterns-23。这个项目用最现代的技术栈重写了 23 种设计模式,非常适合实战学习,还配套了在线交互演示站,可以边读文章边动手玩。本文的实战案例灵感也来源于此。
1. 策略模式是什么?
策略模式(Strategy Pattern)定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。
简单来说,策略模式就是准备一组算法,在运行时选择其中一个来使用。
1.1 核心概念
策略模式主要由三个角色组成:
- Context(环境类):它持有一个 Strategy 类的引用,负责将客户端的请求委托给具体的策略对象。它是客户端和具体策略之间的桥梁。
- Strategy(抽象策略):定义了所有支持的算法的公共接口。Context 只依赖这个接口,而不依赖具体的实现。
- ConcreteStrategy(具体策略):实现了 Strategy 接口,提供具体的算法实现。比如“满减算法”、“打折算法”等。
1.2 为什么我们需要它?
场景: 在电商系统中,支付和促销是两个典型的策略模式应用场景。 比如用户结账时,可以选择支付宝、微信支付或者银联云闪付; 又或者在双十一大促期间,商品可能有多种优惠方式:满 100 减 20、全场 8 折、或者VIP 会员专享价。
痛点: 如果不使用策略模式,你的代码里往往会充斥着大量的 if-else 或 switch 语句。
public double calculatePrice(String type, double price) {
if (type.equals("MAN_JIAN")) {
return price - 20;
} else if (type.equals("DISCOUNT")) {
return price * 0.8;
} else if (type.equals("VIP")) {
// VIP 逻辑...
}
return price;
}这种写法有两个致命缺点: 第一,违反开闭原则。每次运营想搞个新活动(比如“第二件半价”),你都得去修改 calculatePrice 的源码,重新编译部署,风险极大。 第二,代码臃肿。如果促销逻辑很复杂,这个方法会变得巨长无比,难以维护。
解决方案: 把每种计算逻辑(算法)封装成一个独立的类,客户端想用哪个就传哪个进去。这样新增一种策略只需要新增一个类,不需要修改原有代码。
2. Java 实战:电商促销引擎
我们来实现一个灵活的打折引擎。
2.1 标准实现
Step 1: 定义策略接口
public interface DiscountStrategy {
// 计算打折后的价格
double calculate(double originalPrice);
}Step 2: 实现具体策略
满减策略
public class ManJianStrategy implements DiscountStrategy {
@Override
public double calculate(double originalPrice) {
System.out.println("执行满减策略:满100减20");
return originalPrice >= 100 ? originalPrice - 20 : originalPrice;
}
}折扣策略
public class DiscountStrategyImpl implements DiscountStrategy {
@Override
public double calculate(double originalPrice) {
System.out.println("执行折扣策略:全场8折");
return originalPrice * 0.8;
}
}无优惠策略
public class NoDiscountStrategy implements DiscountStrategy {
@Override
public double calculate(double originalPrice) {
System.out.println("无优惠");
return originalPrice;
}
}Step 3: 上下文 (Context)
public class PriceCalculator {
private DiscountStrategy strategy;
// 构造注入,或者 setter 注入
public PriceCalculator(DiscountStrategy strategy) {
this.strategy = strategy;
}
public void setStrategy(DiscountStrategy strategy) {
this.strategy = strategy;
}
public double getFinalPrice(double originalPrice) {
return strategy.calculate(originalPrice);
}
}Step 4: 客户端
public class Client {
public static void main(String[] args) {
// 1. 周末活动,全场8折
PriceCalculator calculator = new PriceCalculator(new DiscountStrategyImpl());
System.out.println("最终价格:" + calculator.getFinalPrice(100));
// 2. 平日活动,满减
calculator.setStrategy(new ManJianStrategy());
System.out.println("最终价格:" + calculator.getFinalPrice(100));
}
}3. 进阶技巧:如何消除客户端的 if-else?
虽然我们把算法封装了,但在 Client 端(或者 Controller 层),通常还是需要根据前端传来的 type 参数来决定 new 哪个策略类。
// Controller 层可能还是这样:
if (type.equals("VIP")) {
calculator.setStrategy(new VipStrategy());
} else if ...这就又回到了起点!怎么彻底消灭 if-else?
解决方案:策略工厂 + Map 注册表
import java.util.HashMap;
import java.util.Map;
public class StrategyFactory {
private static final Map<String, DiscountStrategy> strategyMap = new HashMap<>();
static {
// 实际项目中,这里可以使用 Spring 的 @Autowired 自动注入所有实现类,放入 Map
strategyMap.put("MAN_JIAN", new ManJianStrategy());
strategyMap.put("DISCOUNT", new DiscountStrategyImpl());
strategyMap.put("NO_DISCOUNT", new NoDiscountStrategy());
}
public static DiscountStrategy getStrategy(String type) {
return strategyMap.getOrDefault(type, new NoDiscountStrategy());
}
}现在的客户端代码:
public void pay(String type, double price) {
// 这一行代码彻底消灭了 if-else
DiscountStrategy strategy = StrategyFactory.getStrategy(type);
PriceCalculator calculator = new PriceCalculator(strategy);
double finalPrice = calculator.getFinalPrice(price);
System.out.println("最终价格:" + finalPrice);
}结合 Spring 框架,我们可以利用 ApplicationContext 在启动时自动扫描所有实现了 DiscountStrategy 的 Bean,并根据 Bean Name 注册到 Map 中,实现真正的零修改扩展。
4. Java 8 函数式编程实现
如果策略逻辑很简单,专门写一个类有点重。Java 8 的 Lambda 表达式让策略模式变得极其轻量。
public class Java8Strategy {
public static void main(String[] args) {
// 直接传入 Lambda 作为策略
PriceCalculator calculator = new PriceCalculator(price -> price * 0.5);
System.out.println("半价:" + calculator.getFinalPrice(100));
// 另一种策略
calculator.setStrategy(price -> price - 10);
System.out.println("减10块:" + calculator.getFinalPrice(100));
}
}这里利用了 DiscountStrategy 是一个函数式接口(只有一个抽象方法)的特性。
5. 源码中的策略模式
5.1 JDK java.util.Comparator
这是最著名的策略模式。 Collections.sort(list, comparator)。
list是数据。comparator是策略。 你想怎么排,传不同的Comparator进去就行,sort方法本身不用变。
5.2 JDK 线程池 ThreadPoolExecutor
线程池的拒绝策略 RejectedExecutionHandler。 当任务满了,是直接抛异常?还是丢弃?还是由调用线程执行?
AbortPolicyCallerRunsPolicyDiscardOldestPolicy...这些都是策略。
5.3 Spring Resource
Spring 加载资源时,根据路径的不同前缀(classpath:, file:, http:),使用不同的 Resource 实现类来读取文件。这也是策略模式的一种应用。
6. 策略模式 vs 模板方法模式 vs 状态模式
| 模式 | 核心区别 | 举例 |
|---|---|---|
| 策略模式 | 组合 (Composition)。Context 持有算法接口,运行时可以换。侧重于算法的选择。 | 排序算法(冒泡、快排) |
| 模板方法 | 继承 (Inheritance)。子类重写父类的特定步骤。结构固定,细节改变。 | 也就是去银行办业务(取号、排队、办具体业务、评价) |
| 状态模式 | 状态流转。侧重于对象状态改变导致行为改变,通常状态会自动流转。 | 订单状态(待支付 -> 已支付) |
如何选择?
- 如果你想重用一套算法骨架,只是改某个步骤 -> 模板方法。
- 如果你有一堆平等的算法,想在运行时随便切 -> 策略模式。
- 如果你的对象有状态,且不同状态下行为狂变 -> 状态模式。
7. 总结
策略模式是开闭原则(OCP)的忠实实践者。
- 它把变化的代码(算法)从不变的代码(客户端调用逻辑)中剥离出来。
- 配合工厂模式或 Spring 容器,可以做到新增算法完全不改动旧代码。
8. 实验实操
在 design-patterns-web 项目中,我们通过一个支付方式选择的 Demo 来演示策略模式。
- 策略切换:当你选择 "Credit Card"、"PayPal" 或 "Bank Transfer" 时,你实际上是在切换不同的支付策略 (
PaymentStrategy)。 - 表单渲染:注意观察,随着策略的切换,下方的表单内容会发生完全不同的变化(信用卡需要输入卡号,PayPal 提示跳转,银行转账显示账户信息)。这是因为不同的策略对象实现了不同的
render逻辑。 - 统一执行:点击支付按钮时,Context(页面组件)只需要调用当前策略的
pay()方法,而不需要关心具体是哪种支付方式。 截图建议:选择 "Credit Card" 支付方式,截取包含上方选择器和下方信用卡输入表单的界面,展示策略切换带来的 UI 变化。