从拼团优惠系统理解策略模式:客户端选择、工厂模式与配置驱动

从拼团优惠系统理解策略模式:客户端选择、工厂模式与配置驱动

在实际业务开发中,我们经常会遇到这样一种场景:同一个业务流程中,会根据不同条件执行不同的算法逻辑。

比如在一个拼团系统里,不同活动可能有不同的优惠方式:

1
2
3
4
普通拼团:直减 20 元
新人拼团:满 100 减 30 元
会员拼团:打 9 折
限时拼团:满 200 减 50 元

这些优惠方式本质上都是“计算优惠价格”的不同算法。

如果我们直接把所有判断都写在下单代码里,代码很快就会变得臃肿。策略模式就是为了解决这类问题而出现的。


一、什么是策略模式?

策略模式的核心思想是:

1
把不同算法封装成不同的策略类,并让它们实现同一个接口。

放到拼团系统里,就是把“满减”“直减”“折扣”等优惠方式分别封装成不同策略。

比如定义一个优惠策略接口:

1
2
3
4
public interface DiscountStrategy {

BigDecimal calculate(BigDecimal originPrice, DiscountRule rule);
}

然后不同优惠方式分别实现这个接口。

1. 直减策略

1
2
3
4
5
6
7
public class DirectReduceStrategy implements DiscountStrategy {

@Override
public BigDecimal calculate(BigDecimal originPrice, DiscountRule rule) {
return originPrice.subtract(rule.getDiscountValue());
}
}

2. 满减策略

1
2
3
4
5
6
7
8
9
10
public class FullReduceStrategy implements DiscountStrategy {

@Override
public BigDecimal calculate(BigDecimal originPrice, DiscountRule rule) {
if (originPrice.compareTo(rule.getThreshold()) >= 0) {
return originPrice.subtract(rule.getDiscountValue());
}
return originPrice;
}
}

3. 折扣策略

1
2
3
4
5
6
7
public class RateDiscountStrategy implements DiscountStrategy {

@Override
public BigDecimal calculate(BigDecimal originPrice, DiscountRule rule) {
return originPrice.multiply(rule.getDiscountValue());
}
}

这样做之后,每一种优惠算法都有自己独立的类,互相之间不会混在一起。

这就是策略模式的核心。

但是有了策略类之后,还会出现一个新的问题:

1
系统运行时,到底应该选择哪一个策略?

这就引出了三种常见的策略选择方式。


二、方式一:客户端直接选择

最简单的方式,就是调用方自己判断应该使用哪个策略。

比如下单接口里直接写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public BigDecimal calculatePrice(String discountType, BigDecimal originPrice) {
DiscountStrategy strategy;

if ("DIRECT_REDUCE".equals(discountType)) {
strategy = new DirectReduceStrategy();
} else if ("FULL_REDUCE".equals(discountType)) {
strategy = new FullReduceStrategy();
} else if ("RATE_DISCOUNT".equals(discountType)) {
strategy = new RateDiscountStrategy();
} else {
throw new RuntimeException("未知优惠类型");
}

DiscountRule rule = getDiscountRule(discountType);

return strategy.calculate(originPrice, rule);
}

这种方式很好理解:

1
2
3
如果是直减,就 new DirectReduceStrategy
如果是满减,就 new FullReduceStrategy
如果是折扣,就 new RateDiscountStrategy

优点

客户端直接选择的优点是简单、直接,适合策略数量很少的场景。

比如系统里只有两三种优惠方式,而且短期内不会频繁增加,那么这样写也不是不能接受。

缺点

问题也很明显:判断逻辑和业务流程耦合在一起了。

比如下单接口、订单预览接口、退款重算接口都需要计算优惠,那么这些地方可能都会写类似的 if else

一旦新增一个优惠策略,比如:

1
2
3
阶梯满减
买三免一
第二件半价

就可能要改很多地方。

所以客户端直接选择的问题是:

1
策略选择逻辑分散,维护成本高。

三、方式二:工厂模式选择

工厂模式的思路是:客户端不要自己判断,把选择策略的逻辑交给工厂。

先写一个策略工厂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class DiscountStrategyFactory {

public static DiscountStrategy getStrategy(String discountType) {
if ("DIRECT_REDUCE".equals(discountType)) {
return new DirectReduceStrategy();
} else if ("FULL_REDUCE".equals(discountType)) {
return new FullReduceStrategy();
} else if ("RATE_DISCOUNT".equals(discountType)) {
return new RateDiscountStrategy();
}

throw new RuntimeException("未知优惠类型:" + discountType);
}
}

然后下单代码就可以变成:

1
2
3
4
5
6
7
public BigDecimal calculatePrice(String discountType, BigDecimal originPrice) {
DiscountStrategy strategy = DiscountStrategyFactory.getStrategy(discountType);

DiscountRule rule = getDiscountRule(discountType);

return strategy.calculate(originPrice, rule);
}

这样,下单逻辑就不需要关心具体创建哪个策略对象了。

它只需要做三件事:

1
2
3
拿到优惠类型
通过工厂获取策略
执行优惠计算

工厂模式是不是只是把 if else 抽出去了?

是的。

工厂模式并没有神奇地消灭 if else,它只是把 if else 从业务代码里抽出来,集中放到工厂类中。

但是这个抽离是有价值的。

原来可能是:

1
2
3
4
下单接口有一份 if else
订单预览有一份 if else
退款重算有一份 if else
后台试算有一份 if else

使用工厂后,变成:

1
2
所有地方都调用 DiscountStrategyFactory
策略选择逻辑只维护一份

所以工厂模式解决的是:

1
策略对象如何统一创建和管理。

它不是让代码完全没有判断,而是让判断逻辑集中起来。

优点

工厂模式比客户端直接选择更清晰。

业务代码只负责业务流程,工厂负责策略选择。

缺点

如果新增一种全新的策略,比如“买三免一”,还是需要修改工厂类。

比如要新增:

1
2
3
else if ("BUY_THREE_FREE_ONE".equals(discountType)) {
return new BuyThreeFreeOneStrategy();
}

所以工厂模式适合策略种类比较稳定,但希望业务代码更清晰的场景。


四、方式三:配置驱动

配置驱动是更接近真实业务系统的一种方式。

在拼团系统中,优惠活动往往是运营人员配置的,而不是开发人员每次写死在代码里的。

比如可以设计一张拼团活动表:

1
group_activity
activity_id activity_name discount_type threshold discount_value
1 普通拼团 DIRECT_REDUCE null 20
2 新人拼团 FULL_REDUCE 100 30
3 会员拼团 RATE_DISCOUNT null 0.9
4 限时拼团 FULL_REDUCE 200 50

这里的核心字段是:

1
2
3
discount_type
threshold
discount_value

它们分别表示:

1
2
3
discount_type:使用哪种优惠策略
threshold:优惠门槛
discount_value:优惠值

比如:

1
新人拼团:FULL_REDUCE,threshold = 100,discount_value = 30

意思就是:

1
新人拼团使用满减策略,满 100 减 30。

下单时,代码不再写死哪个活动用哪个策略,而是先查数据库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public BigDecimal calculateGroupPrice(Long activityId, BigDecimal originPrice) {
GroupActivity activity = groupActivityMapper.selectById(activityId);

DiscountStrategy strategy =
DiscountStrategyFactory.getStrategy(activity.getDiscountType());

DiscountRule rule = new DiscountRule();
rule.setThreshold(activity.getThreshold());
rule.setDiscountValue(activity.getDiscountValue());

BigDecimal finalPrice = strategy.calculate(originPrice, rule);

return finalPrice.max(BigDecimal.ZERO);
}

这时候代码的流程变成:

1
2
3
4
5
6
7
8
9
10
11
用户参加拼团

根据 activityId 查询活动配置

拿到 discount_type

通过工厂找到对应策略

把活动里的优惠参数传给策略

计算最终价格

配置驱动的核心价值

配置驱动解决的是:

1
哪个活动使用哪个策略,策略参数是多少。

比如现在运营想新增一个活动:

1
端午拼团:满 300 减 80

如果系统已经支持满减策略,那么只需要在数据库里新增一条记录:

activity_id activity_name discount_type threshold discount_value
5 端午拼团 FULL_REDUCE 300 80

不需要改代码。

这就是配置驱动的价值。

配置驱动是不是只多了一张活动表?

可以这么理解,但更准确地说:

1
配置驱动是把“活动和策略的关系”从代码中移到了数据库或配置文件中。

在简单系统里,一张活动表就够了。

在复杂系统里,可能会拆成两张表:

1
2
group_activity:拼团活动表
discount_rule:优惠规则表

例如:

活动表 group_activity

activity_id activity_name rule_id start_time end_time
1 新人拼团 101 2026-06-01 2026-06-30
2 会员拼团 102 2026-06-01 2026-06-30

优惠规则表 discount_rule

rule_id discount_type threshold discount_value
101 FULL_REDUCE 100 30
102 RATE_DISCOUNT null 0.9

这样设计的好处是:活动信息和优惠规则信息更清晰,后期也更容易扩展。


五、三种方式的区别总结

1. 客户端直接选择

客户端自己写判断逻辑:

1
2
3
if ("FULL_REDUCE".equals(type)) {
strategy = new FullReduceStrategy();
}

特点:

1
2
3
简单直接
耦合较高
适合策略少、变化少的场景

2. 工厂模式选择

把判断逻辑放到工厂:

1
DiscountStrategy strategy = DiscountStrategyFactory.getStrategy(type);

特点:

1
2
3
4
本质还是 if else
但是把选择逻辑集中管理
业务代码更清晰
适合中等复杂度的系统

3. 配置驱动选择

把活动和策略的关系放到数据库:

1
2
新人拼团 -> FULL_REDUCE -> 满100减30
会员拼团 -> RATE_DISCOUNT -> 9折

特点:

1
2
3
活动优惠可以通过配置调整
新增同类型活动不需要改代码
适合活动多、规则经常变化的系统

六、需要注意:配置驱动不是永远不用改代码

很多人容易误解配置驱动,以为只要用了配置,以后就完全不用改代码。

这个理解是不准确的。

配置驱动只能解决:

1
已有策略类型下,新增活动或修改参数不用改代码。

比如系统已经支持:

1
2
3
满减
直减
折扣

那么新增下面这些活动通常不需要改代码:

1
2
3
4
5
6
满 100 减 30
满 200 减 50
直减 20
直减 50
打 9 折
打 8 折

因为它们都属于已有策略,只是参数不同。

但是如果要新增一种全新的优惠算法,比如:

1
2
3
4
买三免一
第二件半价
阶梯满减
组合优惠

这时候还是需要新增策略类。

比如新增“买三免一”策略:

1
2
3
4
5
6
7
8
public class BuyThreeFreeOneStrategy implements DiscountStrategy {

@Override
public BigDecimal calculate(BigDecimal originPrice, DiscountRule rule) {
// 这里需要根据商品数量、最低价商品等信息计算优惠
return originPrice;
}
}

然后再把这个策略注册到工厂或策略容器中。

所以配置驱动不是消灭代码开发,而是减少重复修改代码的次数。


七、真实项目中更推荐的写法

在真实 Java 项目,尤其是 Spring Boot 项目中,一般不会每次都手动 new 策略对象,而是交给 Spring 容器管理。

可以定义接口:

1
2
3
4
5
6
public interface DiscountStrategy {

String getDiscountType();

BigDecimal calculate(BigDecimal originPrice, DiscountRule rule);
}

满减策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public class FullReduceStrategy implements DiscountStrategy {

@Override
public String getDiscountType() {
return "FULL_REDUCE";
}

@Override
public BigDecimal calculate(BigDecimal originPrice, DiscountRule rule) {
if (originPrice.compareTo(rule.getThreshold()) >= 0) {
return originPrice.subtract(rule.getDiscountValue());
}
return originPrice;
}
}

直减策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class DirectReduceStrategy implements DiscountStrategy {

@Override
public String getDiscountType() {
return "DIRECT_REDUCE";
}

@Override
public BigDecimal calculate(BigDecimal originPrice, DiscountRule rule) {
return originPrice.subtract(rule.getDiscountValue());
}
}

然后定义一个策略上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class DiscountStrategyContext {

private final Map<String, DiscountStrategy> strategyMap = new HashMap<>();

public DiscountStrategyContext(List<DiscountStrategy> strategies) {
for (DiscountStrategy strategy : strategies) {
strategyMap.put(strategy.getDiscountType(), strategy);
}
}

public DiscountStrategy getStrategy(String discountType) {
DiscountStrategy strategy = strategyMap.get(discountType);

if (strategy == null) {
throw new RuntimeException("未知优惠策略:" + discountType);
}

return strategy;
}
}

业务代码就可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
public class GroupOrderService {

@Autowired
private GroupActivityMapper groupActivityMapper;

@Autowired
private DiscountStrategyContext discountStrategyContext;

public BigDecimal calculateGroupPrice(Long activityId, BigDecimal originPrice) {
GroupActivity activity = groupActivityMapper.selectById(activityId);

DiscountStrategy strategy =
discountStrategyContext.getStrategy(activity.getDiscountType());

DiscountRule rule = new DiscountRule();
rule.setThreshold(activity.getThreshold());
rule.setDiscountValue(activity.getDiscountValue());

BigDecimal finalPrice = strategy.calculate(originPrice, rule);

return finalPrice.max(BigDecimal.ZERO);
}
}

这种写法相当于:

1
策略模式 + Spring 容器 + 配置驱动

它比传统工厂更优雅,因为不需要在工厂里写一堆 if else

每新增一个策略,只需要:

1
2
3
4
新增一个策略类
实现 DiscountStrategy 接口
加上 @Component
返回自己的 discountType

系统启动时,Spring 会自动把所有策略注入到 DiscountStrategyContext 里。


八、最终总结

策略模式解决的是:

1
不同算法如何封装。

比如拼团系统中的:

1
2
3
4
5
满减
直减
折扣
买三免一
第二件半价

都可以封装成不同策略类。

工厂模式解决的是:

1
如何根据类型找到对应策略对象。

它的本质还是 if else,只是把判断逻辑集中管理,避免散落在多个业务代码中。

配置驱动解决的是:

1
哪个活动使用哪个策略,以及策略参数是多少。

它通常需要活动表或优惠规则表,把活动和策略的关系从代码里挪到数据库中。

所以在拼团优惠系统里,可以这样理解:

1
2
3
4
5
满减、直减、折扣这些类,是策略模式。

根据 FULL_REDUCE 找到 FullReduceStrategy,是工厂模式或策略上下文。

数据库里配置某个拼团活动使用 FULL_REDUCE,满100减30,是配置驱动。

如果系统很简单,优惠策略很少,可以直接用客户端选择。

如果系统中等复杂,可以用工厂模式统一管理策略。

如果活动经常变化,优惠参数经常调整,就更适合使用配置驱动。

真实项目中比较推荐的结构是:

1
策略模式 + 策略上下文 + 数据库配置

也就是:

1
2
3
4
不同优惠算法写成不同策略类;
所有策略交给 Spring 容器管理;
活动表或规则表配置具体使用哪种策略;
业务代码只负责查询活动、获取策略、执行计算。

这样既保持了代码清晰,也方便后续扩展。