1. 为什么我最后选定了策略模式:一个真实的重构现场
1.1 一段让我头皮发麻的 if-else 代码
上个月重构一个营销订单系统时,我在核心的下单方法里看到了一段将近两百行的 if-else 分支。场景很典型:用户下单时要根据不同的促销类型计算优惠金额,促销类型有满减、折扣、赠品、会员价、新人价,后来又加了一个“限时秒杀价”。每次新增促销类型,就要在这个方法里再加一个 else if,然后改动同一个方法的十几个相关变量。当时的状态是:明明只是改一个优惠计算逻辑,却要小心别把下单主流程弄坏,测试要回归整个订单核心链路。
这就是策略模式(Strategy Pattern)最典型的用武之地。简单说,策略模式就是把“做同一件事的不同实现方式”封装成一组独立的类,让调用方在运行时选择用哪一种,而不是在一个方法里用 if-else 把逻辑全写死。它解决的核心问题不是性能,而是代码的可维护性、可测试性和可扩展性。
后来我把那段 if-else 重构为策略模式后,新增一种促销类型时,只需要新增一个策略实现类,再注册一下,改完基本不影响其他任何代码。我自己在多个项目中用策略模式踩了不少坑,也总结出一套比较成熟的落地方法,这篇文章就把从原理到实战的完整路径讲清楚。
1.2 策略模式到底解决了什么问题、没解决什么问题
先说清楚一个很多人容易搞混的点:策略模式不是为了提高代码运行效率,而是为了提高“修改代码的人”的效率。它把“做什么”和“怎么做”拆开了,调用方只关心我要算优惠,但具体是满减还是折扣,由运行时的策略对象决定。
举一个生活化的例子:你出门去公司,导航只告诉你“去公司”,但到底是骑车、坐地铁还是打车,这是你自己根据天气、时间、预算来选择的。导航不用把自己的判断逻辑全写死在地图接口里,你换一种出行方式,导航核心逻辑不用改。策略模式里的 Context(上下文)就是导航,具体的 Strategy(策略)就是骑车/地铁/打车。
但它不是万能的。如果业务分支只有两三个,而且不会有太多新增可能,强行用策略模式反而增加类数量和阅读成本。我一般遵循一个原则:当同类业务分支超过三个,且历史上有过“每两个月加一种新玩法”的记录时,再上策略模式。这个判断标准在后面章节我还会细讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 策略模式的核心角色与三种主流写法详解
2.1 经典接口实现:策略、上下文、具体策略怎么分工
先看最标准的 UML 式写法,这是最正统也最好理解的一种。策略模式里有三个核心角色:
- Strategy(策略接口):定义一类算法的统一入口,比如
calculateDiscount(Order order)。 - ConcreteStrategy(具体策略):实现策略接口,每个具体策略负责一种算法,比如满减策略、折扣策略。
- Context(上下文):持有一个策略对象的引用,负责调用策略。上下文可以自己决定策略,也可以由外部传入策略。
看一个我实际用过的简单例子,订单优惠计算,这是最经典的应用场景。
java复制// 策略接口
public interface DiscountStrategy {
BigDecimal calculateDiscount(Order order);
}
// 满减策略:满100减20
public class FullReductionStrategy implements DiscountStrategy {
@Override
public BigDecimal calculateDiscount(Order order) {
if (order.getAmount().compareTo(new BigDecimal("100")) >= 0) {
return new BigDecimal("20");
}
return BigDecimal.ZERO;
}
}
// 折扣策略:打八折
public class PercentageStrategy implements DiscountStrategy {
@Override
public BigDecimal calculateDiscount(Order order) {
return order.getAmount().multiply(new BigDecimal("0.2"));
}
}
// 上下文:订单优惠计算器
public class OrderDiscountCalculator {
private DiscountStrategy strategy;
public OrderDiscountCalculator(DiscountStrategy strategy) {
this.strategy = strategy;
}
public BigDecimal calculate(Order order) {
return strategy.calculateDiscount(order);
}
}
调用方可以根据不同场景实例化不同的 Strategy,然后传入上下文:
java复制Order order = new Order(new BigDecimal("120"));
DiscountStrategy strategy = new FullReductionStrategy();
OrderDiscountCalculator calculator = new OrderDiscountCalculator(strategy);
BigDecimal discount = calculator.calculate(order);
这段代码的核心价值是把“满减怎么做”“折扣怎么算”彻底从下单主流程里挪走了。以后要加“买二送一”,你只需要新增一个 BuyTwoGetOneStrategy,下单方法一行都不用动。这个思路对系统长期演进特别重要,因为它保证了主流程的稳定。
2.2 枚举策略:适合中小型系统的轻量方案
经典接口实现有个小问题:策略类的数量会随着业务类型增加而增加,而且调用方还要知道每个策略类的名字,不然没法 new 出正确的策略。对一个中小型系统来说,有时候直接用“枚举 + 策略方法”是更轻量实用的做法。
枚举策略的核心思路是:用枚举来定义策略类型,同时把策略的实现逻辑放在枚举内部的方法里。来看这段代码:
java复制public enum DiscountStrategyEnum {
FULL_REDUCTION {
@Override
public BigDecimal calculateDiscount(Order order) {
if (order.getAmount().compareTo(new BigDecimal("100")) >= 0) {
return new BigDecimal("20");
}
return BigDecimal.ZERO;
}
},
PERCENTAGE {
@Override
public BigDecimal calculateDiscount(Order order) {
return order.getAmount().multiply(new BigDecimal("0.2"));
}
};
public abstract BigDecimal calculateDiscount(Order order);
}
这样调用就变成一行:
java复制BigDecimal discount = DiscountStrategyEnum.valueOf(type).calculateDiscount(order);
我把这种写法叫“轻量策略模式”,因为它没有独立的策略类,但依然保留了“策略可替换”“新增策略不改变调用方逻辑”的核心特性。新增一种策略,只需要在枚举里再加一个枚举值,重写抽象方法即可。
不过这种方案也有明显的局限:枚举里的代码如果太复杂,会变成一个大杂烩,违背了单一职责。所以我的建议是:策略内部超过十行业务逻辑,就老老实实拆成独立类;如果只是简单的三五行计算,用枚举完全够。
2.3 函数式风格实现:Java lambda 和 Python dict 也能写策略
如果你用的是 Java 8 以上版本,或者写得是 Python、JavaScript 这类支持函数式编程的语言,策略模式还可以更轻——直接用函数对象或者 lambda 表达式来充当策略。
Java 里可以用 Map<String, Function<Order, BigDecimal>> 把策略代码塞进一个 Map 里:
java复制Map<String, Function<Order, BigDecimal>> strategies = new HashMap<>();
strategies.put("FULL_REDUCTION", order ->
order.getAmount().compareTo(new BigDecimal("100")) >= 0 ? new BigDecimal("20") : BigDecimal.ZERO
);
strategies.put("PERCENTAGE", order -> order.getAmount().multiply(new BigDecimal("0.2")));
调用时一行搞定:
java复制BigDecimal discount = strategies.getOrDefault(type, order -> BigDecimal.ZERO).apply(order);
Python 里更简练,直接用字典映射函数:
python复制def full_reduction(order_amount):
return 20 if order_amount >= 100 else 0
def percentage(order_amount):
return order_amount * 0.2
discount_strategies = {
"FULL_REDUCTION": full_reduction,
"PERCENTAGE": percentage
}
discount = discount_strategies.get(type, lambda x: 0)(order_amount)
函数式实现的最大优点是非常直观,策略代码和调用逻辑放在同一个文件里,对于脚本型项目或者快速迭代的模块特别方便。但它也有明显的缺点:策略逻辑如果复杂了,单个函数内部会膨胀,而且没有独立的类注释和单元测试入口,长期维护时不如类策略清晰。我自己的习惯是:策略逻辑简单可以用函数式,一旦单个策略超过五到十行,就建议升级为类实现。
三种写法我整理成一个对比表格,方便你按项目情况选型:
| 实现方式 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 经典接口实现 | 职责清晰、可单独测试、易扩展 | 类数量多、调用方需要知道具体策略类 | 大型系统、策略逻辑复杂、需要长期维护 |
| 枚举策略 | 轻量、类型安全、代码集中 | 枚举内部逻辑过多时难维护 | 中小型系统、策略简单、类型固定 |
| 函数式实现 | 代码简洁、直观、上手快 | 复杂逻辑难组织、单元测试粒度粗 | 脚本项目、快速原型、逻辑简单的策略 |
3. 完整实战:从需求分析到可上线的订单优惠策略系统
3.1 需求梳理:折扣、满减、赠品,三种策略要怎么抽象
现在我用一个完整的例子,把策略模式的整个落地过程走一遍。需求背景是这样的:我正在重构一个电商下单系统,目前优惠类型有以下三种:
- 满减:订单满 100 减 20,满 200 减 50。
- 折扣:统一打八折。
- 赠品:订单满 300 送一个赠品,赠品需要进入订单明细。
赠品比较特殊,它不只是算优惠金额,还影响订单的商品明细。这就牵扯到一个很关键的设计决策:策略接口的返回值该怎么设计?
很多新手在这里会犯一个错误——把所有可能的计算需求全塞进一个方法里,比如策略接口直接定义成 calculate(Order order, DiscountResult result),让每个策略传一个结果对象进来往里面塞数据。这听起来灵活,实际用起来非常痛苦:每个策略都需要知道自己该填哪些字段,调用方要时刻关注返回结果里哪些字段有效,极容易漏判。
我的设计方法是:策略接口只负责返回一个统一的结果对象 DiscountResult,这个对象包含“优惠金额”和“赠品列表”两个字段。满减和折扣策略只填优惠金额,赠品策略金额填零,但把赠品列表填上。调用方拿到结果后统一处理,不需要关心每个策略内部到底怎么填的。这样所有策略都有统一的行为契约,代码好读,测试也好写。
3.2 接口定义、上下文设计、策略注册与调用
先定义结果对象:
java复制public class DiscountResult {
private BigDecimal discountAmount = BigDecimal.ZERO;
private List<String> giftItems = new ArrayList<>();
public DiscountResult addDiscount(BigDecimal amount) {
this.discountAmount = this.discountAmount.add(amount);
return this;
}
public DiscountResult addGift(String item) {
this.giftItems.add(item);
return this;
}
public BigDecimal getDiscountAmount() {
return discountAmount;
}
public List<String> getGiftItems() {
return giftItems;
}
}
接口定义为:
java复制public interface DiscountStrategy {
DiscountResult calculate(Order order);
}
三个具体策略实现:
java复制public class FullReductionStrategy implements DiscountStrategy {
@Override
public DiscountResult calculate(Order order) {
BigDecimal amount = order.getAmount();
if (amount.compareTo(new BigDecimal("200")) >= 0) {
return new DiscountResult().addDiscount(new BigDecimal("50"));
}
if (amount.compareTo(new BigDecimal("100")) >= 0) {
return new DiscountResult().addDiscount(new BigDecimal("20"));
}
return new DiscountResult();
}
}
public class PercentageStrategy implements DiscountStrategy {
@Override
public DiscountResult calculate(Order order) {
BigDecimal discount = order.getAmount().multiply(new BigDecimal("0.2"));
return new DiscountResult().addDiscount(discount);
}
}
public class GiftStrategy implements DiscountStrategy {
@Override
public DiscountResult calculate(Order order) {
if (order.getAmount().compareTo(new BigDecimal("300")) >= 0) {
return new DiscountResult().addGift("优惠礼品");
}
return new DiscountResult();
}
}
到这里,策略类本身已经写完了,但还没完——调用方怎么知道该用哪个策略?这时候就需要一个“策略工厂”或者叫“策略注册表”的东西。它本质上是一个 Map,key 是策略类型,value 是策略实例。
java复制public class DiscountStrategyFactory {
private static final Map<String, DiscountStrategy> STRATEGIES = new HashMap<>();
static {
STRATEGIES.put("FULL_REDUCTION", new FullReductionStrategy());
STRATEGIES.put("PERCENTAGE", new PercentageStrategy());
STRATEGIES.put("GIFT", new GiftStrategy());
}
public static DiscountStrategy getStrategy(String type) {
DiscountStrategy strategy = STRATEGIES.get(type);
if (strategy == null) {
throw new IllegalArgumentException("不支持的优惠类型: " + type);
}
return strategy;
}
}
下单主流程只需要这样调用:
java复制DiscountStrategy strategy = DiscountStrategyFactory.getStrategy(order.getPromotionType());
DiscountResult result = strategy.calculate(order);
到了这一步,原来的两百行 if-else 就基本收缩成三行了。主流程不需要知道满减怎么算、折扣是打几折、赠品长什么样,它只关心“拿到一个策略,算出一个结果”。
3.3 扩展新策略:不改调用方代码的一次真实操作记录
现在重点来了,你新增一个“限时秒杀价”的策略,验证这套设计的扩展性。限时秒杀规则是:每天 10 点开始,前一百单打五折,并且每人限购一件。
我只需要做两件事:
第一步,新建一个 SeckillStrategy 类:
java复制public class SeckillStrategy implements DiscountStrategy {
@Override
public DiscountResult calculate(Order order) {
LocalTime now = LocalTime.now();
if (now.isAfter(LocalTime.of(10, 0)) && order.getUser().getSeckillCount() < 1) {
BigDecimal discount = order.getAmount().multiply(new BigDecimal("0.5"));
return new DiscountResult().addDiscount(discount);
}
return new DiscountResult();
}
}
第二步,在 DiscountStrategyFactory 的静态代码块里加一行注册:
java复制STRATEGIES.put("SECKILL", new SeckillStrategy());
然后让商家后台配置优惠类型时把 SECKILL 这个 key 存进去,全链路就能跑了。下单主流程代码一行都不用改,测试也只需要新增针对 SeckillStrategy 的部分。这就是开闭原则在现实中的体现——对扩展开放,对修改关闭。
我实际在做这个扩展时发现,赠品策略和秒杀策略如果同时生效,需要一个“组合策略”来处理优先级。这个不是策略模式本身要管的事,但我会在策略工厂里做一个 CompositeStrategy,内部按顺序执行多个策略,把结果合并。实现的思路就是写一个策略类,内部持有一个策略列表,循环调用 calculate 并合并结果。这里不展开了,但你要知道,策略模式的设计并不阻止你继续嵌套策略,灵活性正是它的核心优势。
4. 实战中躲不开的常见问题与排查技巧
4.1 策略类爆炸了怎么办:从代码组织到动态装载
策略模式用多了,最直接的问题就是类数量膨胀。一个系统里如果有一百种促销策略,那就是一百个策略类,管理起来确实费劲。有几个实用的缓解手段:
第一种是包结构规范。我通常把所有策略类放在同一个 package 下,命名统一用 业务类型 + Strategy 后缀,比如 FullReductionStrategy、SeckillStrategy。这样 IDE 里按 package 一看就能找到全部策略,不会散落在业务代码里。好处是直观,缺点是仍然是手工管理。
第二种是结合 Spring 框架做自动注册。如果你用 Spring Boot,可以把策略类都注册成 Bean,然后用 List<DiscountStrategy> 一次性注入,再按各自的类型标识注册到 Map 里:
java复制@Component
public class DiscountStrategyRegistry {
private final Map<String, DiscountStrategy> strategyMap = new HashMap<>();
public DiscountStrategyRegistry(List<DiscountStrategy> strategies) {
for (DiscountStrategy strategy : strategies) {
RegisterInfo info = strategy.getClass().getAnnotation(RegisterInfo.class);
if (info != null) {
strategyMap.put(info.type(), strategy);
}
}
}
public DiscountStrategy getStrategy(String type) {
DiscountStrategy strategy = strategyMap.get(type);
if (strategy == null) {
throw new IllegalArgumentException("不支持的优惠类型: " + type);
}
return strategy;
}
}
你只需要在每个策略类上标一个 @RegisterInfo(type = "SECKILL") 注解,Spring 容器启动时会自动把所有策略实现注册进去,以后新增策略就是一个新类加一个注解,工厂代码一行都不用再动。我自己实际项目里用的就是这一套,非常省心。
第三种是策略本身可以做成数据驱动的。比如折扣策略和满减策略,其实差别只是“门槛金额”和“优惠金额”这两个参数,完全没必要每个参数组合都写一个类。把这些参数放到配置表里,用一个 ParameterizedStrategy 读取配置来计算,这样才能真正控制类数量。
4.2 策略没生效、注入为空:三个高频排错方向
策略模式在项目里最容易出的问题就是“某个策略没生效”“对象注入进来是 null”。根据我自己的踩坑经验,绝大多数是以下三个原因:
第一个是策略工厂里没有注册。尤其是新人接手项目,新增了一个策略类,但忘了在 DiscountStrategyFactory 的 Map 里 put,或者忘了写 @Component 注解。这个问题的排查方式很简单:先看工厂或者注册表里有没有对应的 key,再查策略类是不是被 Spring 扫描到了。建议写一个启动自检的测试,把所有策略 key 和配置表里允许出现的类型做一次交叉验证,保证两边对得上。
第二个是策略选择时的入参 key 不匹配。数据库里存的是 full_reduction,代码里注册的 key 却是 FULL_REDUCTION,这能不报错吗?而且有的系统不抛异常,反而走默认逻辑,数据就直接算错了。我的做法是统一用一个枚举管理 key,存数据库之前先做一次合法性校验,不能让脏数据绕过校验层。
第三个是策略内部依赖的其他服务注入为空。如果一个策略类里注入了 OrderQueryService,但这个策略类本身是用 new 关键字在工厂里创建的,那这个注入必然为 null。这是新手最容易踩的坑——用了 Spring 的注入,却用 new 来创建对象。解决方式很简单:策略类如果依赖其他 Bean,就走 Spring 的 @Component 自动注册;如果非要手动 new,就把依赖通过构造方法传进去,别用注入。
4.3 策略模式被滥用:什么时候不要用它
策略模式不是银弹,我见过很多为了用而用的反面案例。最典型的就是:某个业务只有两种类型,逻辑总共三行,也强行抽象接口加两个实现类。结果是代码量翻倍,读起来反而更绕。
我的经验是,出现以下情况时不要用策略模式:
- 分支数量很少(两个及以内),且非常稳定,一年半载不会新增。 直接用 if-else 比策略模式更直观,不需要为“可能的扩展”提前交税。
- 策略之间共享大量私有状态,且状态会随时变化。 策略对象被设计成无状态的,如果每个策略内部都要维护一堆上下文状态,那多策略之间的边界会变得模糊,代码会很难调。
- 业务逻辑强依赖先后顺序和状态流转。 比如一个复杂的审批流,前一步的结果会直接影响后一步,这不是策略选择的问题,而是流程编排的问题。这时候用状态模式或者责任链模式更合适。
还有一种情况,几个分支看起来像是策略,但实际它们的输入输出差异极大,比如一个分支返回优惠金额,另一个分支返回的是物流配送区域。这时候硬套策略模式会让接口变得很脏,全是“if (result instanceof XXX)”这种判断。遇到这种情况,我会考虑策略模式里只放最通用的算法抽象,而差异大的部分放到单独的业务方法里。
我自己的选型经验是:当“这个类型的数量会持续增长”这个判断成立时,才值得用策略模式;否则尽量保持简单。编程很多时候是在跟“过度设计”作斗争,策略模式也一样。它应该让代码更简单,而不是让项目里多出几十个无人问津的类。
另外,测试是很容易被忽略但又特别重要的环节。策略模式带来的一个巨大好处就是每个策略可以独立做单元测试,不用跑整个下单主流程。我在团队里会要求每个策略类都有对应的测试用例,输入构造数据,验证计算结果。尤其是金额计算,一定要把边界条件测到,比如满减策略里订单金额刚好等于 100、等于 99.99、等于 200 这些临界值。测试用例覆盖全了,后续扩展策略的时候才敢放手去做。
根据我个人经验,策略模式最舒服的用法就是跟 Spring 的 Bean 管理结合起来:策略类用 @Component 注册,用注册表统一管理,策略内部尽量保持无状态。踩过几次坑之后,我现在写策略模式时会默认做四件事:定义统一结果对象、用枚举管理策略类型、策略工厂做注册和校验、每个策略配套单元测试。这套组合拳下来,代码几乎不会因为新增策略而出问题。希望这篇文章能帮你把策略模式真正用到自己的项目里,少走那些我走过的弯路。
