欢迎你来读这篇博客,这篇博客主要是关于策略模式。
其中包括策略模式的核心思想、适用场景、优缺点、与状态模式、模板方法模式、工厂模式、责任链模式的区别,以及 Java 后端开发中订单优惠折扣策略的完整案例。
序言
在 Java 后端开发中,我们经常会遇到这样一种代码:
1 2 3 4 5 6 7 8 9
| if ("NORMAL".equals(userType)) { } else if ("VIP".equals(userType)) { } else if ("SVIP".equals(userType)) { } else if ("NEW_USER".equals(userType)) { }
|
刚开始只有两三种情况,这样写很正常。
但业务发展后,条件越来越多:
- 普通用户折扣;
- VIP 用户折扣;
- SVIP 用户折扣;
- 新用户首单折扣;
- 满减活动折扣;
- 会员日折扣;
- 优惠券折扣;
- 积分抵扣;
- 渠道专属折扣;
- 黑五活动折扣。
最后一个方法里堆满了 if else。
更麻烦的是,每新增一种折扣规则,就要修改原来的代码。
这会带来几个问题:
- 方法越来越长;
- 分支越来越多;
- 修改旧代码容易引入 bug;
- 单元测试越来越难;
- 违反开闭原则;
- 业务规则耦合在一起;
- 新增算法不够优雅。
策略模式就是为了解决这类问题:
定义一系列算法,把每个算法封装起来,并且让它们可以相互替换。
简单说:
策略模式就是把一堆 if else 中的不同算法拆成一个个策略类。
调用方不关心具体怎么算。
调用方只需要选择一个策略,然后执行。
这就像出门有多种交通方式:
它们都能完成“到达目的地”这个目标,但实现方式不同。
策略模式就是把这些“不同实现方式”封装起来。
正文
chapter 1:什么是策略模式
策略模式,英文是 Strategy Pattern,属于行为型设计模式。
它的定义是:
定义一系列算法,将每一个算法封装起来,并使它们可以相互替换。策略模式让算法的变化独立于使用算法的客户端。
这个定义里有三个关键点:
- 一系列算法;
- 每个算法独立封装;
- 客户端可以替换算法。
在 Java 中,策略模式通常包含三个角色:
- Strategy 抽象策略:定义算法接口。
- ConcreteStrategy 具体策略:实现具体算法。
- Context 上下文:持有策略对象,并调用策略完成任务。
结构如下:
1 2 3 4 5 6 7 8 9 10 11 12
| Client │ ▼ Context │ ▼ Strategy ▲ │ ├── ConcreteStrategyA ├── ConcreteStrategyB └── ConcreteStrategyC
|
比如订单折扣:
1 2 3 4 5
| DiscountStrategy ├── NormalDiscountStrategy ├── VipDiscountStrategy ├── NewUserDiscountStrategy └── CouponDiscountStrategy
|
客户端只依赖:
具体使用哪种折扣策略,可以根据用户类型、活动类型、渠道类型动态选择。
chapter 2:策略模式解决什么问题
策略模式主要解决的是:
同一类行为有多种实现方式,并且需要在运行时选择其中一种。
例如:
1. 支付方式
2. 优惠计算
1 2 3 4 5
| 普通折扣 会员折扣 满减折扣 新人折扣 优惠券折扣
|
3. 文件导出
1 2 3 4
| Excel 导出 CSV 导出 PDF 导出 JSON 导出
|
4. 消息发送
5. 排序算法
6. 风控规则
1 2 3 4
| 黑名单校验 频率校验 设备风险校验 IP 风险校验
|
这些场景都有一个共同点:
目标一致,但实现方式不同。
这就是策略模式的主场。
chapter 3:不用策略模式会怎样
假设我们要根据用户类型计算折扣。
最直接的写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import java.math.BigDecimal;
public class DiscountService {
public BigDecimal calculate(String userType, BigDecimal amount) { if ("NORMAL".equals(userType)) { return amount; }
if ("VIP".equals(userType)) { return amount.multiply(new BigDecimal("0.90")); }
if ("SVIP".equals(userType)) { return amount.multiply(new BigDecimal("0.80")); }
if ("NEW_USER".equals(userType)) { return amount.subtract(new BigDecimal("20")); }
throw new IllegalArgumentException("Unsupported user type: " + userType); } }
|
这个方法刚开始还行。
但随着规则增加,会出现问题:
- 折扣规则堆在一个方法里;
- 新增规则要修改原方法;
- 规则之间互相影响;
- 单元测试要覆盖所有分支;
- 方法越来越长;
- 违反开闭原则。
策略模式的做法是:
每一种折扣规则一个策略类。
例如:
1 2 3 4
| NormalDiscountStrategy VipDiscountStrategy SvipDiscountStrategy NewUserDiscountStrategy
|
新增策略时新增类,不修改原来的核心计算方法。
chapter 4:策略模式的核心思想
策略模式的核心思想可以概括为:
把会变化的算法封装起来,让调用方依赖抽象策略,而不是依赖具体算法。
比如:
1 2 3 4
| public interface DiscountStrategy {
BigDecimal calculate(BigDecimal amount); }
|
不同策略:
1 2 3 4 5 6
| public class VipDiscountStrategy implements DiscountStrategy { @Override public BigDecimal calculate(BigDecimal amount) { return amount.multiply(new BigDecimal("0.90")); } }
|
调用方:
1 2 3
| DiscountStrategy strategy = getStrategy(userType);
BigDecimal result = strategy.calculate(amount);
|
调用方不关心折扣怎么算。
它只关心:
这就是面向接口编程。
chapter 5:策略模式的经典结构
策略模式一般写成这样。
抽象策略:
1 2 3 4
| public interface Strategy {
void execute(); }
|
具体策略 A:
1 2 3 4 5 6 7
| public class ConcreteStrategyA implements Strategy {
@Override public void execute() { System.out.println("执行策略 A"); } }
|
具体策略 B:
1 2 3 4 5 6 7
| public class ConcreteStrategyB implements Strategy {
@Override public void execute() { System.out.println("执行策略 B"); } }
|
上下文:
1 2 3 4 5 6 7 8 9 10 11 12
| public class Context {
private final Strategy strategy;
public Context(Strategy strategy) { this.strategy = strategy; }
public void executeStrategy() { strategy.execute(); } }
|
客户端:
1 2 3 4 5 6 7 8 9 10
| public class StrategyDemo {
public static void main(String[] args) { Strategy strategy = new ConcreteStrategyA();
Context context = new Context(strategy);
context.executeStrategy(); } }
|
这就是策略模式最基础的形式。
但在真实 Java 后端项目中,我们更常用 Spring 容器来管理策略对象。
chapter 6:案例背景:订单优惠折扣策略
下面用一个 Java 后端常见案例来讲策略模式:订单优惠折扣计算。
假设订单系统需要根据优惠类型计算最终应付金额。
优惠类型包括:
- 无优惠;
- VIP 九折;
- SVIP 八折;
- 新人立减 20;
- 满 100 减 30;
- 优惠券抵扣。
需求如下:
- 每种优惠规则独立封装;
- 新增优惠规则不修改原有计算逻辑;
- 支持通过优惠类型选择策略;
- 可以在 Spring Boot 中自动注册策略;
- 金额计算使用
BigDecimal。
chapter 7:定义优惠类型枚举
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public enum DiscountType {
NONE,
VIP,
SVIP,
NEW_USER,
FULL_REDUCTION,
COUPON }
|
这只是优惠类型标识。
真正计算逻辑放到策略类中。
chapter 8:定义订单上下文 OrderDiscountContext
折扣计算需要一些上下文信息。
例如:
- 订单原始金额;
- 用户 ID;
- 优惠券金额;
- 是否新人;
- 用户等级。
定义上下文对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import java.math.BigDecimal;
public class OrderDiscountContext {
private final Long userId;
private final BigDecimal originalAmount;
private final BigDecimal couponAmount;
public OrderDiscountContext(Long userId, BigDecimal originalAmount, BigDecimal couponAmount) { this.userId = userId; this.originalAmount = originalAmount; this.couponAmount = couponAmount; }
public Long getUserId() { return userId; }
public BigDecimal getOriginalAmount() { return originalAmount; }
public BigDecimal getCouponAmount() { return couponAmount; } }
|
这里为了演示只保留三个字段。
真实项目中上下文可能还包含:
- 商品类别;
- 活动 ID;
- 渠道;
- 用户等级;
- 城市;
- 会员权益;
- 优惠券信息;
- 营销活动信息。
chapter 9:定义折扣结果 DiscountResult
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| import java.math.BigDecimal;
public class DiscountResult {
private final BigDecimal originalAmount;
private final BigDecimal discountAmount;
private final BigDecimal payableAmount;
private final String description;
public DiscountResult(BigDecimal originalAmount, BigDecimal discountAmount, BigDecimal payableAmount, String description) { this.originalAmount = originalAmount; this.discountAmount = discountAmount; this.payableAmount = payableAmount; this.description = description; }
public BigDecimal getOriginalAmount() { return originalAmount; }
public BigDecimal getDiscountAmount() { return discountAmount; }
public BigDecimal getPayableAmount() { return payableAmount; }
public String getDescription() { return description; } }
|
折扣结果包括:
chapter 10:定义策略接口 DiscountStrategy
1 2 3 4 5 6
| public interface DiscountStrategy {
DiscountType type();
DiscountResult calculate(OrderDiscountContext context); }
|
每个折扣策略都需要返回自己支持的类型:
这样后面可以用 Map<DiscountType, DiscountStrategy> 做自动路由。
chapter 11:定义无优惠策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import java.math.BigDecimal;
public class NoneDiscountStrategy implements DiscountStrategy {
@Override public DiscountType type() { return DiscountType.NONE; }
@Override public DiscountResult calculate(OrderDiscountContext context) { BigDecimal originalAmount = context.getOriginalAmount();
return new DiscountResult( originalAmount, BigDecimal.ZERO, originalAmount, "无优惠" ); } }
|
无优惠策略就是原价。
chapter 12:定义 VIP 折扣策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import java.math.BigDecimal; import java.math.RoundingMode;
public class VipDiscountStrategy implements DiscountStrategy {
private static final BigDecimal RATE = new BigDecimal("0.90");
@Override public DiscountType type() { return DiscountType.VIP; }
@Override public DiscountResult calculate(OrderDiscountContext context) { BigDecimal originalAmount = context.getOriginalAmount();
BigDecimal payableAmount = originalAmount.multiply(RATE) .setScale(2, RoundingMode.HALF_UP);
BigDecimal discountAmount = originalAmount.subtract(payableAmount);
return new DiscountResult( originalAmount, discountAmount, payableAmount, "VIP 九折优惠" ); } }
|
这里使用 BigDecimal,并设置精度。
金额计算不要用 double。
这不是强迫症,是保命。
chapter 13:定义 SVIP 折扣策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import java.math.BigDecimal; import java.math.RoundingMode;
public class SvipDiscountStrategy implements DiscountStrategy {
private static final BigDecimal RATE = new BigDecimal("0.80");
@Override public DiscountType type() { return DiscountType.SVIP; }
@Override public DiscountResult calculate(OrderDiscountContext context) { BigDecimal originalAmount = context.getOriginalAmount();
BigDecimal payableAmount = originalAmount.multiply(RATE) .setScale(2, RoundingMode.HALF_UP);
BigDecimal discountAmount = originalAmount.subtract(payableAmount);
return new DiscountResult( originalAmount, discountAmount, payableAmount, "SVIP 八折优惠" ); } }
|
chapter 14:定义新人优惠策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import java.math.BigDecimal;
public class NewUserDiscountStrategy implements DiscountStrategy {
private static final BigDecimal DISCOUNT_AMOUNT = new BigDecimal("20.00");
@Override public DiscountType type() { return DiscountType.NEW_USER; }
@Override public DiscountResult calculate(OrderDiscountContext context) { BigDecimal originalAmount = context.getOriginalAmount();
BigDecimal payableAmount = originalAmount.subtract(DISCOUNT_AMOUNT);
if (payableAmount.compareTo(BigDecimal.ZERO) < 0) { payableAmount = BigDecimal.ZERO; }
BigDecimal actualDiscountAmount = originalAmount.subtract(payableAmount);
return new DiscountResult( originalAmount, actualDiscountAmount, payableAmount, "新人立减 20 元" ); } }
|
新人优惠要注意不能减成负数。
chapter 15:定义满减策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import java.math.BigDecimal;
public class FullReductionDiscountStrategy implements DiscountStrategy {
private static final BigDecimal THRESHOLD = new BigDecimal("100.00");
private static final BigDecimal DISCOUNT_AMOUNT = new BigDecimal("30.00");
@Override public DiscountType type() { return DiscountType.FULL_REDUCTION; }
@Override public DiscountResult calculate(OrderDiscountContext context) { BigDecimal originalAmount = context.getOriginalAmount();
if (originalAmount.compareTo(THRESHOLD) < 0) { return new DiscountResult( originalAmount, BigDecimal.ZERO, originalAmount, "未满足满 100 减 30" ); }
BigDecimal payableAmount = originalAmount.subtract(DISCOUNT_AMOUNT);
return new DiscountResult( originalAmount, DISCOUNT_AMOUNT, payableAmount, "满 100 减 30" ); } }
|
chapter 16:定义优惠券策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| import java.math.BigDecimal;
public class CouponDiscountStrategy implements DiscountStrategy {
@Override public DiscountType type() { return DiscountType.COUPON; }
@Override public DiscountResult calculate(OrderDiscountContext context) { BigDecimal originalAmount = context.getOriginalAmount();
BigDecimal couponAmount = context.getCouponAmount();
if (couponAmount == null || couponAmount.compareTo(BigDecimal.ZERO) <= 0) { return new DiscountResult( originalAmount, BigDecimal.ZERO, originalAmount, "无可用优惠券" ); }
BigDecimal payableAmount = originalAmount.subtract(couponAmount);
if (payableAmount.compareTo(BigDecimal.ZERO) < 0) { payableAmount = BigDecimal.ZERO; }
BigDecimal actualDiscountAmount = originalAmount.subtract(payableAmount);
return new DiscountResult( originalAmount, actualDiscountAmount, payableAmount, "优惠券抵扣 " + actualDiscountAmount + " 元" ); } }
|
优惠券策略需要从上下文中拿优惠券金额。
真实项目中,优惠券信息可能需要先经过校验:
- 是否过期;
- 是否已使用;
- 是否适用商品;
- 是否满足门槛;
- 是否适用用户;
- 是否和其他活动互斥。
这些可以放在策略内部,也可以放在优惠券领域服务中。
chapter 17:定义策略上下文 DiscountCalculator
现在定义一个折扣计算器,用来根据优惠类型选择策略。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import java.util.List; import java.util.Map; import java.util.stream.Collectors;
public class DiscountCalculator {
private final Map<DiscountType, DiscountStrategy> strategyMap;
public DiscountCalculator(List<DiscountStrategy> strategies) { this.strategyMap = strategies.stream() .collect(Collectors.toUnmodifiableMap(DiscountStrategy::type, strategy -> strategy)); }
public DiscountResult calculate(DiscountType discountType, OrderDiscountContext context) { DiscountStrategy strategy = strategyMap.get(discountType);
if (strategy == null) { throw new IllegalArgumentException("Unsupported discount type: " + discountType); }
return strategy.calculate(context); } }
|
核心是:
1
| Map<DiscountType, DiscountStrategy>
|
以后新增策略,只要加入这个列表即可。
chapter 18:客户端使用策略模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| import java.math.BigDecimal; import java.util.List;
public class StrategyPatternDemo {
public static void main(String[] args) { DiscountCalculator calculator = new DiscountCalculator( List.of( new NoneDiscountStrategy(), new VipDiscountStrategy(), new SvipDiscountStrategy(), new NewUserDiscountStrategy(), new FullReductionDiscountStrategy(), new CouponDiscountStrategy() ) );
OrderDiscountContext context = new OrderDiscountContext( 1L, new BigDecimal("150.00"), new BigDecimal("50.00") );
DiscountResult vipResult = calculator.calculate(DiscountType.VIP, context);
print(vipResult);
DiscountResult couponResult = calculator.calculate(DiscountType.COUPON, context);
print(couponResult); }
private static void print(DiscountResult result) { System.out.println("原始金额:" + result.getOriginalAmount()); System.out.println("优惠金额:" + result.getDiscountAmount()); System.out.println("应付金额:" + result.getPayableAmount()); System.out.println("优惠说明:" + result.getDescription()); System.out.println("----------"); } }
|
输出类似:
1 2 3 4 5 6 7 8 9 10
| 原始金额:150.00 优惠金额:15.00 应付金额:135.00 优惠说明:VIP 九折优惠 ---------- 原始金额:150.00 优惠金额:50.00 应付金额:100.00 优惠说明:优惠券抵扣 50.00 元 ----------
|
客户端没有 if else。
不同优惠算法由不同策略类处理。
chapter 19:新增策略时怎么扩展
假设现在新增“会员日七折”活动。
只需要新增一个策略类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import java.math.BigDecimal; import java.math.RoundingMode;
public class MemberDayDiscountStrategy implements DiscountStrategy {
private static final BigDecimal RATE = new BigDecimal("0.70");
@Override public DiscountType type() { return DiscountType.MEMBER_DAY; }
@Override public DiscountResult calculate(OrderDiscountContext context) { BigDecimal originalAmount = context.getOriginalAmount();
BigDecimal payableAmount = originalAmount.multiply(RATE) .setScale(2, RoundingMode.HALF_UP);
BigDecimal discountAmount = originalAmount.subtract(payableAmount);
return new DiscountResult( originalAmount, discountAmount, payableAmount, "会员日七折优惠" ); } }
|
然后在枚举中新增:
再注册到策略列表中即可。
如果是 Spring Boot,可以只新增一个 Bean,自动被收集。
这就是策略模式符合开闭原则的地方。
chapter 20:Spring Boot 中落地策略模式
在 Spring Boot 中,策略类可以直接交给容器管理。
策略接口不变:
1 2 3 4 5 6
| public interface DiscountStrategy {
DiscountType type();
DiscountResult calculate(OrderDiscountContext context); }
|
VIP 策略:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| import org.springframework.stereotype.Component;
import java.math.BigDecimal; import java.math.RoundingMode;
@Component public class VipDiscountStrategy implements DiscountStrategy {
private static final BigDecimal RATE = new BigDecimal("0.90");
@Override public DiscountType type() { return DiscountType.VIP; }
@Override public DiscountResult calculate(OrderDiscountContext context) { BigDecimal originalAmount = context.getOriginalAmount();
BigDecimal payableAmount = originalAmount.multiply(RATE) .setScale(2, RoundingMode.HALF_UP);
BigDecimal discountAmount = originalAmount.subtract(payableAmount);
return new DiscountResult( originalAmount, discountAmount, payableAmount, "VIP 九折优惠" ); } }
|
新人策略:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| import org.springframework.stereotype.Component;
import java.math.BigDecimal;
@Component public class NewUserDiscountStrategy implements DiscountStrategy {
private static final BigDecimal DISCOUNT_AMOUNT = new BigDecimal("20.00");
@Override public DiscountType type() { return DiscountType.NEW_USER; }
@Override public DiscountResult calculate(OrderDiscountContext context) { BigDecimal originalAmount = context.getOriginalAmount();
BigDecimal payableAmount = originalAmount.subtract(DISCOUNT_AMOUNT);
if (payableAmount.compareTo(BigDecimal.ZERO) < 0) { payableAmount = BigDecimal.ZERO; }
return new DiscountResult( originalAmount, originalAmount.subtract(payableAmount), payableAmount, "新人立减 20 元" ); } }
|
策略路由器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import org.springframework.stereotype.Component;
import java.util.List; import java.util.Map; import java.util.stream.Collectors;
@Component public class DiscountStrategyRouter {
private final Map<DiscountType, DiscountStrategy> strategyMap;
public DiscountStrategyRouter(List<DiscountStrategy> strategies) { this.strategyMap = strategies.stream() .collect(Collectors.toUnmodifiableMap(DiscountStrategy::type, strategy -> strategy)); }
public DiscountStrategy route(DiscountType type) { DiscountStrategy strategy = strategyMap.get(type);
if (strategy == null) { throw new IllegalArgumentException("Unsupported discount type: " + type); }
return strategy; } }
|
折扣服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import org.springframework.stereotype.Service;
@Service public class DiscountService {
private final DiscountStrategyRouter strategyRouter;
public DiscountService(DiscountStrategyRouter strategyRouter) { this.strategyRouter = strategyRouter; }
public DiscountResult calculate(DiscountType discountType, OrderDiscountContext context) { DiscountStrategy strategy = strategyRouter.route(discountType);
return strategy.calculate(context); } }
|
这样 Spring 会自动注入所有实现了 DiscountStrategy 的 Bean。
新增策略时,只需要新增一个 @Component。
这就是 Spring Boot 中策略模式最常见的落地姿势。
chapter 21:为什么不直接用 @Qualifier
如果只有少量策略,可以用 @Qualifier。
但策略很多时,不推荐这样:
1 2
| public DiscountService(@Qualifier("vipDiscountStrategy") DiscountStrategy strategy) { }
|
因为这样写死了一个策略。
策略模式的重点是运行时选择不同策略。
更推荐:
1
| Map<DiscountType, DiscountStrategy>
|
或:
然后根据业务类型路由。
chapter 22:策略模式和工厂模式的结合
策略模式经常和工厂模式一起使用。
策略模式负责定义算法。
工厂模式负责选择策略。
例如:
1 2 3 4 5 6 7 8
| public class DiscountStrategyFactory {
private final Map<DiscountType, DiscountStrategy> strategyMap;
public DiscountStrategy getStrategy(DiscountType type) { return strategyMap.get(type); } }
|
严格说,前面的 DiscountStrategyRouter 也带有简单工厂的味道。
真实项目里不用太纠结命名。
关键是职责清楚:
- 策略类负责算法;
- 路由器/工厂负责选择策略;
- 业务服务负责调用策略完成业务。
chapter 23:策略模式和 if else 的关系
策略模式不是完全消灭条件判断。
它是把大量分支判断从业务算法中移出去。
比如原来:
1 2 3 4 5
| if (type == VIP) { } else if (type == NEW_USER) { }
|
变成:
1 2 3
| DiscountStrategy strategy = strategyRouter.route(type);
strategy.calculate(context);
|
路由时仍然需要根据 type 找策略。
但这个查找通常由 Map 完成。
这样新增策略时,不需要修改一堆 if else。
策略模式的目标不是“世界上再也没有 if”。
它的目标是:
让变化的算法独立扩展,不要挤在一个方法里打群架。
chapter 24:策略模式和状态模式的区别
策略模式和状态模式非常像。
它们都把行为封装到不同类中。
但意图不同。
| 对比项 |
策略模式 |
状态模式 |
| 核心目的 |
替换算法 |
对象行为随状态变化 |
| 是否有状态流转 |
通常没有 |
通常有 |
| 策略/状态是否稳定 |
策略由外部选择 |
状态由对象内部维护 |
| Context 角色 |
使用策略 |
持有当前状态 |
| 示例 |
VIP 折扣、满减折扣 |
订单 CREATED、PAID、SHIPPED |
一句话区分:
策略模式是“我选择哪种算法”。
状态模式是“我当前处于什么状态,所以行为不同”。
例如:
1
| discountStrategy.calculate(context);
|
这是策略模式。
订单根据当前状态决定能不能支付,这是状态模式。
chapter 25:策略模式和模板方法模式的区别
模板方法模式是在父类中定义算法骨架,子类实现具体步骤。
策略模式是把完整算法封装成可替换对象。
| 对比项 |
策略模式 |
模板方法模式 |
| 复用方式 |
组合 |
继承 |
| 扩展方式 |
新增策略类 |
新增子类并重写步骤 |
| 算法结构 |
每个策略可以完全不同 |
流程骨架固定 |
| 灵活性 |
更灵活 |
流程更稳定 |
| 示例 |
不同折扣策略 |
文件导入固定流程 |
模板方法适合:
策略模式适合:
例如文件导入:
1
| 读取文件 -> 校验数据 -> 保存数据 -> 生成报告
|
流程固定,只是不同文件格式读取方式不同,模板方法更合适。
如果是多种完全不同折扣算法,策略模式更合适。
chapter 26:策略模式和责任链模式的区别
策略模式和责任链模式都能替代部分 if else,但结构不同。
| 对比项 |
策略模式 |
责任链模式 |
| 执行数量 |
通常选择一个策略执行 |
多个处理器可能依次执行 |
| 核心目的 |
替换算法 |
请求沿链传递处理 |
| 结构 |
Map 路由到一个策略 |
Handler 链 |
| 是否强调顺序 |
不强调 |
强调 |
| 示例 |
选择一种折扣策略 |
下单参数校验 -> 库存校验 -> 风控校验 |
如果一个请求只需要选一种算法,用策略模式。
如果一个请求要经过多个处理器,用责任链模式。
例如:
适合策略模式。
1
| 参数校验 -> 用户校验 -> 商品校验 -> 库存校验
|
适合责任链模式。
chapter 27:策略模式和命令模式的区别
| 对比项 |
策略模式 |
命令模式 |
| 核心目的 |
替换算法 |
把请求封装成对象 |
| 是否保存请求参数 |
通常不保存具体请求生命周期 |
通常保存请求参数 |
| 是否支持排队/撤销 |
不是重点 |
是重点 |
| 调用方式 |
strategy.calculate(context) |
command.execute() |
| 示例 |
DiscountStrategy |
CreateOrderCommand |
策略模式关注“怎么算”。
命令模式关注“要做什么”。
优惠计算用策略。
后台操作任务、撤销、异步执行更适合命令。
chapter 28:策略模式和解释器模式的区别
策略模式适合有限算法。
解释器模式适合可配置表达式。
| 对比项 |
策略模式 |
解释器模式 |
| 规则形式 |
Java 类 |
表达式/DSL |
| 扩展方式 |
新增策略类 |
新增表达式或配置规则 |
| 动态配置 |
不太方便 |
更方便 |
| 复杂度 |
较低 |
较高 |
| 示例 |
VipDiscountStrategy |
amount >= 100 AND userLevel == VIP |
如果优惠规则固定几种,用策略模式。
如果优惠规则需要运营动态配置,解释器模式或规则引擎更合适。
chapter 29:策略模式的优点
1. 消除大量条件分支
把不同算法拆到不同策略类。
2. 符合开闭原则
新增策略时,尽量不修改原有业务逻辑。
3. 单一职责更清晰
每个策略只负责一种算法。
4. 可测试性更好
每个策略可以单独写单元测试。
5. 可扩展性好
新增算法只需要新增策略实现。
6. 可以运行时切换策略
根据用户类型、渠道、配置动态选择策略。
chapter 30:策略模式的缺点
1. 类数量增加
每种算法都可能对应一个策略类。
策略多了之后类会很多。
2. 调用方需要知道策略类型
调用方通常需要传入策略标识。
例如:
3. 策略选择逻辑仍然需要管理
需要工厂、路由器或 Spring 容器来选择策略。
4. 不适合算法特别少的场景
只有两个简单分支时,直接 if else 可能更清晰。
5. 策略之间如果互相依赖,会变复杂
策略应该独立。
如果策略 A 调策略 B,策略 B 又调策略 C,可能说明抽象有问题。
chapter 31:适用场景
策略模式适合以下场景。
1. 同一类行为有多种实现
例如支付、折扣、导出、排序、消息发送。
2. 代码中有大量同类 if else
例如:
1 2 3 4
| if (type == A) { } else if (type == B) { } else if (type == C) { }
|
3. 算法需要灵活替换
运行时根据参数选择不同算法。
4. 算法相对独立
每个策略可以独立完成任务。
5. 新增算法比较频繁
策略模式可以降低修改旧代码的概率。
chapter 32:不适合使用的场景
以下场景不建议使用策略模式。
1. 分支很少且不会变化
两个简单判断直接写就好。
2. 策略之间差异很小
如果只是几个参数不同,可能配置化更合适。
例如折扣率不同,可以用数据库配置折扣率,而不是每个折扣率写一个策略类。
3. 策略组合顺序很重要
如果多个策略要依次执行,责任链可能更合适。
4. 规则需要动态表达
如果规则由运营配置,解释器或规则引擎可能更合适。
5. 策略选择逻辑比策略本身还复杂
如果选择策略需要大量复杂判断,可能需要重新设计业务模型。
chapter 33:真实项目中的实践建议
1. 策略接口要稳定
策略接口不要经常变。
例如:
1
| DiscountResult calculate(OrderDiscountContext context);
|
用 Context 包装参数,可以减少接口变更。
2. 策略类命名要清晰
推荐:
1 2 3 4
| VipDiscountStrategy NewUserDiscountStrategy CouponDiscountStrategy FullReductionDiscountStrategy
|
不推荐:
1 2
| DiscountStrategyImpl1 DiscountStrategyImpl2
|
名字就是文档。
3. 使用 Map 路由策略
推荐:
1
| Map<DiscountType, DiscountStrategy>
|
不要每次都写 if else 查策略。
4. Spring Boot 中策略使用 List 自动注入
1 2
| public DiscountStrategyRouter(List<DiscountStrategy> strategies) { }
|
非常适合策略模式。
5. 策略内部不要做太多无关事情
折扣策略负责计算折扣。
不要在折扣策略里发短信、写审计、扣库存。
6. 策略可以结合配置
例如 VIP 折扣率可以来自配置:
1 2
| discount: vip-rate: 0.9
|
策略类负责算法,配置负责参数。
7. 注意金额计算精度
金额相关策略统一使用 BigDecimal。
并明确:
- 舍入方式;
- 精度;
- 是否允许负数;
- 优惠叠加顺序。
8. 策略之间尽量不要互相调用
如果策略需要组合,考虑组合策略、责任链或规则引擎。
9. 策略选择失败要明确报错
1
| throw new IllegalArgumentException("Unsupported discount type: " + type);
|
不要默认返回某个策略,否则容易隐藏业务问题。
10. 不要为了消灭所有 if 而滥用策略
设计模式是药,不是零食。
小病别吃猛药。
chapter 34:完整案例代码汇总
优惠类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public enum DiscountType {
NONE,
VIP,
SVIP,
NEW_USER,
FULL_REDUCTION,
COUPON }
|
折扣上下文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import java.math.BigDecimal;
public class OrderDiscountContext {
private final Long userId;
private final BigDecimal originalAmount;
private final BigDecimal couponAmount;
public OrderDiscountContext(Long userId, BigDecimal originalAmount, BigDecimal couponAmount) { this.userId = userId; this.originalAmount = originalAmount; this.couponAmount = couponAmount; }
public Long getUserId() { return userId; }
public BigDecimal getOriginalAmount() { return originalAmount; }
public BigDecimal getCouponAmount() { return couponAmount; } }
|
折扣结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| import java.math.BigDecimal;
public class DiscountResult {
private final BigDecimal originalAmount;
private final BigDecimal discountAmount;
private final BigDecimal payableAmount;
private final String description;
public DiscountResult(BigDecimal originalAmount, BigDecimal discountAmount, BigDecimal payableAmount, String description) { this.originalAmount = originalAmount; this.discountAmount = discountAmount; this.payableAmount = payableAmount; this.description = description; }
public BigDecimal getOriginalAmount() { return originalAmount; }
public BigDecimal getDiscountAmount() { return discountAmount; }
public BigDecimal getPayableAmount() { return payableAmount; }
public String getDescription() { return description; } }
|
策略接口
1 2 3 4 5 6
| public interface DiscountStrategy {
DiscountType type();
DiscountResult calculate(OrderDiscountContext context); }
|
无优惠策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import java.math.BigDecimal;
public class NoneDiscountStrategy implements DiscountStrategy {
@Override public DiscountType type() { return DiscountType.NONE; }
@Override public DiscountResult calculate(OrderDiscountContext context) { BigDecimal originalAmount = context.getOriginalAmount();
return new DiscountResult( originalAmount, BigDecimal.ZERO, originalAmount, "无优惠" ); } }
|
VIP 策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import java.math.BigDecimal; import java.math.RoundingMode;
public class VipDiscountStrategy implements DiscountStrategy {
private static final BigDecimal RATE = new BigDecimal("0.90");
@Override public DiscountType type() { return DiscountType.VIP; }
@Override public DiscountResult calculate(OrderDiscountContext context) { BigDecimal originalAmount = context.getOriginalAmount();
BigDecimal payableAmount = originalAmount.multiply(RATE) .setScale(2, RoundingMode.HALF_UP);
BigDecimal discountAmount = originalAmount.subtract(payableAmount);
return new DiscountResult( originalAmount, discountAmount, payableAmount, "VIP 九折优惠" ); } }
|
SVIP 策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import java.math.BigDecimal; import java.math.RoundingMode;
public class SvipDiscountStrategy implements DiscountStrategy {
private static final BigDecimal RATE = new BigDecimal("0.80");
@Override public DiscountType type() { return DiscountType.SVIP; }
@Override public DiscountResult calculate(OrderDiscountContext context) { BigDecimal originalAmount = context.getOriginalAmount();
BigDecimal payableAmount = originalAmount.multiply(RATE) .setScale(2, RoundingMode.HALF_UP);
BigDecimal discountAmount = originalAmount.subtract(payableAmount);
return new DiscountResult( originalAmount, discountAmount, payableAmount, "SVIP 八折优惠" ); } }
|
新人策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import java.math.BigDecimal;
public class NewUserDiscountStrategy implements DiscountStrategy {
private static final BigDecimal DISCOUNT_AMOUNT = new BigDecimal("20.00");
@Override public DiscountType type() { return DiscountType.NEW_USER; }
@Override public DiscountResult calculate(OrderDiscountContext context) { BigDecimal originalAmount = context.getOriginalAmount();
BigDecimal payableAmount = originalAmount.subtract(DISCOUNT_AMOUNT);
if (payableAmount.compareTo(BigDecimal.ZERO) < 0) { payableAmount = BigDecimal.ZERO; }
BigDecimal actualDiscountAmount = originalAmount.subtract(payableAmount);
return new DiscountResult( originalAmount, actualDiscountAmount, payableAmount, "新人立减 20 元" ); } }
|
满减策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import java.math.BigDecimal;
public class FullReductionDiscountStrategy implements DiscountStrategy {
private static final BigDecimal THRESHOLD = new BigDecimal("100.00");
private static final BigDecimal DISCOUNT_AMOUNT = new BigDecimal("30.00");
@Override public DiscountType type() { return DiscountType.FULL_REDUCTION; }
@Override public DiscountResult calculate(OrderDiscountContext context) { BigDecimal originalAmount = context.getOriginalAmount();
if (originalAmount.compareTo(THRESHOLD) < 0) { return new DiscountResult( originalAmount, BigDecimal.ZERO, originalAmount, "未满足满 100 减 30" ); }
BigDecimal payableAmount = originalAmount.subtract(DISCOUNT_AMOUNT);
return new DiscountResult( originalAmount, DISCOUNT_AMOUNT, payableAmount, "满 100 减 30" ); } }
|
优惠券策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| import java.math.BigDecimal;
public class CouponDiscountStrategy implements DiscountStrategy {
@Override public DiscountType type() { return DiscountType.COUPON; }
@Override public DiscountResult calculate(OrderDiscountContext context) { BigDecimal originalAmount = context.getOriginalAmount();
BigDecimal couponAmount = context.getCouponAmount();
if (couponAmount == null || couponAmount.compareTo(BigDecimal.ZERO) <= 0) { return new DiscountResult( originalAmount, BigDecimal.ZERO, originalAmount, "无可用优惠券" ); }
BigDecimal payableAmount = originalAmount.subtract(couponAmount);
if (payableAmount.compareTo(BigDecimal.ZERO) < 0) { payableAmount = BigDecimal.ZERO; }
BigDecimal actualDiscountAmount = originalAmount.subtract(payableAmount);
return new DiscountResult( originalAmount, actualDiscountAmount, payableAmount, "优惠券抵扣 " + actualDiscountAmount + " 元" ); } }
|
策略路由器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import java.util.List; import java.util.Map; import java.util.stream.Collectors;
public class DiscountCalculator {
private final Map<DiscountType, DiscountStrategy> strategyMap;
public DiscountCalculator(List<DiscountStrategy> strategies) { this.strategyMap = strategies.stream() .collect(Collectors.toUnmodifiableMap(DiscountStrategy::type, strategy -> strategy)); }
public DiscountResult calculate(DiscountType discountType, OrderDiscountContext context) { DiscountStrategy strategy = strategyMap.get(discountType);
if (strategy == null) { throw new IllegalArgumentException("Unsupported discount type: " + discountType); }
return strategy.calculate(context); } }
|
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| import java.math.BigDecimal; import java.util.List;
public class StrategyPatternDemo {
public static void main(String[] args) { DiscountCalculator calculator = new DiscountCalculator( List.of( new NoneDiscountStrategy(), new VipDiscountStrategy(), new SvipDiscountStrategy(), new NewUserDiscountStrategy(), new FullReductionDiscountStrategy(), new CouponDiscountStrategy() ) );
OrderDiscountContext context = new OrderDiscountContext( 1L, new BigDecimal("150.00"), new BigDecimal("50.00") );
DiscountResult vipResult = calculator.calculate(DiscountType.VIP, context);
print(vipResult);
DiscountResult couponResult = calculator.calculate(DiscountType.COUPON, context);
print(couponResult); }
private static void print(DiscountResult result) { System.out.println("原始金额:" + result.getOriginalAmount()); System.out.println("优惠金额:" + result.getDiscountAmount()); System.out.println("应付金额:" + result.getPayableAmount()); System.out.println("优惠说明:" + result.getDescription()); System.out.println("----------"); } }
|
chapter 35:一句话总结
策略模式的本质是:
将一组可替换的算法封装成独立策略类,让调用方通过统一接口使用它们,并在运行时选择具体策略。
它特别适合:
- 支付方式选择;
- 折扣优惠计算;
- 文件导出格式;
- 消息发送渠道;
- 排序算法选择;
- 风控规则选择;
- 计费规则选择;
- 不同业务类型处理。
策略模式最重要的不是“多写几个 Strategy 类”,而是:
把变化的算法从主流程中拆出去,让新增算法不再污染原来的业务代码。
好的策略模式像工具箱:每个工具各司其职,需要哪个拿哪个。
坏的策略模式像杂物间:类很多,名字很玄,最后找一个策略比直接写 if else 还痛苦。
所以使用策略模式时,记住一句话:
策略模式适合算法多且变化频繁的场景,不适合为了消灭两个 if 而把简单代码复杂化。
参考资料
- Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software.
- Robert C. Martin. Agile Software Development, Principles, Patterns, and Practices.
- Joshua Bloch. Effective Java.
- Martin Fowler. Patterns of Enterprise Application Architecture.
- Spring Framework Documentation: Core Technologies - The IoC Container.
- Refactoring Guru: Strategy Pattern.
- SourceMaking: Strategy Design Pattern.
启示录
富贵岂由人,时会高志须酬。
能成功于千载者,必以近察远。