设计模式:策略模式

欢迎你来读这篇博客,这篇博客主要是关于策略模式
其中包括策略模式的核心思想、适用场景、优缺点、与状态模式、模板方法模式、工厂模式、责任链模式的区别,以及 Java 后端开发中订单优惠折扣策略的完整案例。

序言

在 Java 后端开发中,我们经常会遇到这样一种代码:

1
2
3
4
5
6
7
8
9
if ("NORMAL".equals(userType)) {
// 普通用户折扣
} else if ("VIP".equals(userType)) {
// VIP 用户折扣
} else if ("SVIP".equals(userType)) {
// SVIP 用户折扣
} else if ("NEW_USER".equals(userType)) {
// 新用户折扣
}

刚开始只有两三种情况,这样写很正常。

但业务发展后,条件越来越多:

  • 普通用户折扣;
  • VIP 用户折扣;
  • SVIP 用户折扣;
  • 新用户首单折扣;
  • 满减活动折扣;
  • 会员日折扣;
  • 优惠券折扣;
  • 积分抵扣;
  • 渠道专属折扣;
  • 黑五活动折扣。

最后一个方法里堆满了 if else

更麻烦的是,每新增一种折扣规则,就要修改原来的代码。

这会带来几个问题:

  • 方法越来越长;
  • 分支越来越多;
  • 修改旧代码容易引入 bug;
  • 单元测试越来越难;
  • 违反开闭原则;
  • 业务规则耦合在一起;
  • 新增算法不够优雅。

策略模式就是为了解决这类问题:

定义一系列算法,把每个算法封装起来,并且让它们可以相互替换。

简单说:

策略模式就是把一堆 if else 中的不同算法拆成一个个策略类。

调用方不关心具体怎么算。

调用方只需要选择一个策略,然后执行。

这就像出门有多种交通方式:

  • 步行;
  • 骑车;
  • 打车;
  • 地铁;
  • 高铁;
  • 飞机。

它们都能完成“到达目的地”这个目标,但实现方式不同。

策略模式就是把这些“不同实现方式”封装起来。

正文

chapter 1:什么是策略模式

策略模式,英文是 Strategy Pattern,属于行为型设计模式。

它的定义是:

定义一系列算法,将每一个算法封装起来,并使它们可以相互替换。策略模式让算法的变化独立于使用算法的客户端。

这个定义里有三个关键点:

  1. 一系列算法
  2. 每个算法独立封装
  3. 客户端可以替换算法

在 Java 中,策略模式通常包含三个角色:

  1. Strategy 抽象策略:定义算法接口。
  2. ConcreteStrategy 具体策略:实现具体算法。
  3. 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

客户端只依赖:

1
DiscountStrategy

具体使用哪种折扣策略,可以根据用户类型、活动类型、渠道类型动态选择。

chapter 2:策略模式解决什么问题

策略模式主要解决的是:

同一类行为有多种实现方式,并且需要在运行时选择其中一种。

例如:

1. 支付方式

1
2
3
4
支付宝支付
微信支付
银行卡支付
余额支付

2. 优惠计算

1
2
3
4
5
普通折扣
会员折扣
满减折扣
新人折扣
优惠券折扣

3. 文件导出

1
2
3
4
Excel 导出
CSV 导出
PDF 导出
JSON 导出

4. 消息发送

1
2
3
4
短信发送
邮件发送
企业微信发送
钉钉发送

5. 排序算法

1
2
3
快速排序
归并排序
堆排序

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);

调用方不关心折扣怎么算。

它只关心:

1
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 后端常见案例来讲策略模式:订单优惠折扣计算。

假设订单系统需要根据优惠类型计算最终应付金额。

优惠类型包括:

  1. 无优惠;
  2. VIP 九折;
  3. SVIP 八折;
  4. 新人立减 20;
  5. 满 100 减 30;
  6. 优惠券抵扣。

需求如下:

  • 每种优惠规则独立封装;
  • 新增优惠规则不修改原有计算逻辑;
  • 支持通过优惠类型选择策略;
  • 可以在 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);
}

每个折扣策略都需要返回自己支持的类型:

1
DiscountType type();

这样后面可以用 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,
"会员日七折优惠"
);
}
}

然后在枚举中新增:

1
MEMBER_DAY

再注册到策略列表中即可。

如果是 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>

或:

1
List<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) {
// 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);

这是策略模式。

1
order.pay();

订单根据当前状态决定能不能支付,这是状态模式。

chapter 25:策略模式和模板方法模式的区别

模板方法模式是在父类中定义算法骨架,子类实现具体步骤。

策略模式是把完整算法封装成可替换对象。

对比项 策略模式 模板方法模式
复用方式 组合 继承
扩展方式 新增策略类 新增子类并重写步骤
算法结构 每个策略可以完全不同 流程骨架固定
灵活性 更灵活 流程更稳定
示例 不同折扣策略 文件导入固定流程

模板方法适合:

1
流程固定,部分步骤变化

策略模式适合:

1
整体算法可替换

例如文件导入:

1
读取文件 -> 校验数据 -> 保存数据 -> 生成报告

流程固定,只是不同文件格式读取方式不同,模板方法更合适。

如果是多种完全不同折扣算法,策略模式更合适。

chapter 26:策略模式和责任链模式的区别

策略模式和责任链模式都能替代部分 if else,但结构不同。

对比项 策略模式 责任链模式
执行数量 通常选择一个策略执行 多个处理器可能依次执行
核心目的 替换算法 请求沿链传递处理
结构 Map 路由到一个策略 Handler 链
是否强调顺序 不强调 强调
示例 选择一种折扣策略 下单参数校验 -> 库存校验 -> 风控校验

如果一个请求只需要选一种算法,用策略模式。

如果一个请求要经过多个处理器,用责任链模式。

例如:

1
VIP 折扣 / 新人折扣 / 满减折扣

适合策略模式。

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. 调用方需要知道策略类型

调用方通常需要传入策略标识。

例如:

1
DiscountType.VIP

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.

启示录

富贵岂由人,时会高志须酬。

能成功于千载者,必以近察远。


设计模式:策略模式
https://allendericdalexander.github.io/2026/04/01/java/design/22strategy-pattern-blog/
作者
AtLuoFu
发布于
2026年4月1日
许可协议