Java八股设计模式从拼团优惠系统理解策略模式:客户端选择、工厂模式与配置驱动
YYT从拼团优惠系统理解策略模式:客户端选择、工厂模式与配置驱动
在实际业务开发中,我们经常会遇到这样一种场景:同一个业务流程中,会根据不同条件执行不同的算法逻辑。
比如在一个拼团系统里,不同活动可能有不同的优惠方式:
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 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 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); }
|
这样,下单逻辑就不需要关心具体创建哪个策略对象了。
它只需要做三件事:
工厂模式是不是只是把 if else 抽出去了?
是的。
工厂模式并没有神奇地消灭 if else,它只是把 if else 从业务代码里抽出来,集中放到工厂类中。
但是这个抽离是有价值的。
原来可能是:
1 2 3 4
| 下单接口有一份 if else 订单预览有一份 if else 退款重算有一份 if else 后台试算有一份 if else
|
使用工厂后,变成:
1 2
| 所有地方都调用 DiscountStrategyFactory 策略选择逻辑只维护一份
|
所以工厂模式解决的是:
它不是让代码完全没有判断,而是让判断逻辑集中起来。
优点
工厂模式比客户端直接选择更清晰。
业务代码只负责业务流程,工厂负责策略选择。
缺点
如果新增一种全新的策略,比如“买三免一”,还是需要修改工厂类。
比如要新增:
1 2 3
| else if ("BUY_THREE_FREE_ONE".equals(discountType)) { return new BuyThreeFreeOneStrategy(); }
|
所以工厂模式适合策略种类比较稳定,但希望业务代码更清晰的场景。
四、方式三:配置驱动
配置驱动是更接近真实业务系统的一种方式。
在拼团系统中,优惠活动往往是运营人员配置的,而不是开发人员每次写死在代码里的。
比如可以设计一张拼团活动表:
| 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 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 ↓ 通过工厂找到对应策略 ↓ 把活动里的优惠参数传给策略 ↓ 计算最终价格
|
配置驱动的核心价值
配置驱动解决的是:
比如现在运营想新增一个活动:
如果系统已经支持满减策略,那么只需要在数据库里新增一条记录:
| 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(); }
|
特点:
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 2 3 4 5 6
| 满 100 减 30 满 200 减 50 直减 20 直减 50 打 9 折 打 8 折
|
因为它们都属于已有策略,只是参数不同。
但是如果要新增一种全新的优惠算法,比如:
这时候还是需要新增策略类。
比如新增“买三免一”策略:
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); } }
|
这种写法相当于:
它比传统工厂更优雅,因为不需要在工厂里写一堆 if else。
每新增一个策略,只需要:
1 2 3 4
| 新增一个策略类 实现 DiscountStrategy 接口 加上 @Component 返回自己的 discountType
|
系统启动时,Spring 会自动把所有策略注入到 DiscountStrategyContext 里。
八、最终总结
策略模式解决的是:
比如拼团系统中的:
都可以封装成不同策略类。
工厂模式解决的是:
它的本质还是 if else,只是把判断逻辑集中管理,避免散落在多个业务代码中。
配置驱动解决的是:
它通常需要活动表或优惠规则表,把活动和策略的关系从代码里挪到数据库中。
所以在拼团优惠系统里,可以这样理解:
1 2 3 4 5
| 满减、直减、折扣这些类,是策略模式。
根据 FULL_REDUCE 找到 FullReduceStrategy,是工厂模式或策略上下文。
数据库里配置某个拼团活动使用 FULL_REDUCE,满100减30,是配置驱动。
|
如果系统很简单,优惠策略很少,可以直接用客户端选择。
如果系统中等复杂,可以用工厂模式统一管理策略。
如果活动经常变化,优惠参数经常调整,就更适合使用配置驱动。
真实项目中比较推荐的结构是:
也就是:
1 2 3 4
| 不同优惠算法写成不同策略类; 所有策略交给 Spring 容器管理; 活动表或规则表配置具体使用哪种策略; 业务代码只负责查询活动、获取策略、执行计算。
|
这样既保持了代码清晰,也方便后续扩展。