策略模式(Strategy Pattern)实战拆解:从if-else泥潭到优雅策略的完整演进
先聊个场景:你维护一个订单系统,里面的折扣计算逻辑已经从最初的3个档位膨胀到了十几个。每次产品经理提需求,你都得在那个几百行的if-else里找一个合适的位置小心翼翼地插入新分支,改完还要提心吊胆地跑一遍全部回归。我敢打赌,任何在中大型项目里摸爬过几年的人,都见过甚至亲手写过这种代码。这种时候,策略模式(Strategy Pattern)就是那个能把烂摊子理顺的利器。
作为一个程序员,我大概从第四年写代码的时候才真正把策略模式用明白。这篇文章我会用实际项目的视角,把策略模式从概念到落地、从代码到经验,完整拆给你看。
1. 策略模式的核心思路:它到底解决什么问题
先搞清楚一件事:策略模式不是在炫技,它是为了解决实际问题的。我见过太多人为了用设计模式而用设计模式,结果把简单业务搞得无比复杂,这是完全走偏了才会干的事。
1.1 问题本质:算法的易变性与客户端耦合
任何一个业务系统,从诞生的第一天起就在面临变化。电商的促销规则、派送公司的运费计算、银行的手续费费率、内容平台的推荐权重,这些东西有一个共同特点:规则本身容易变,而且版本会越来越多。
如果你把所有这些规则都写成if-else或者switch-case塞在一个方法里,你会同时遇到三个麻烦:
第一个麻烦是难以维护。当方法体超过两百行,当嵌套条件超过四层,再勤快的程序员打开这套代码都会头大。第二个麻烦是违反开闭原则。每次新增规则,你必须修改已有的代码,也就是说你每次都在重新测试老功能。第三个麻烦是职责混乱。一个方法干了太多事情,既在做选择,又在做计算,耦合度高得吓人。
策略模式的核心思想说白了也很简单:定义一族算法,把它们一个个封装起来,并且使它们可以互相替换。它就是用“组合 + 委托”来替代继承和条件分支,把"做什么"和"怎么做"拆开,让调用方只关心策略的入口,不关心策略内部的操作细节。
1.2 策略模式的基本角色拆解
先看三个核心角色。
Context(上下文)是策略的使用者,它持有一个策略接口的引用,负责把请求委托给具体的策略对象。它不关心策略到底怎么实现,只知道自己可以调用策略的方法。
Strategy(策略接口)定义了一组算法的公共接口。不管具体策略怎么折腾,对Context来说,你只需要遵循这个契约。
ConcreteStrategy(具体策略)是策略接口的具体实现类,每个类封装了一种具体的算法或行为。需要新增规则时,加一个实现类就行,不需要动Context和其他策略。
光说概念有点绕,我用一个通俗类比帮大家建立直观印象:你出门上班,交通方式就是策略。地铁、公交、打车、骑车,每种方式都能把你送到公司,但它们的时间、成本、体验完全不同。出门前你要根据天气、时间、预算在它们之间做选择。这里"你"就是Context,"到达公司这个行为"是策略接口,"坐地铁/打车/骑车"就是一个个具体策略。你今天选地铁不会影响明天选打车,策略之间天然就是相互独立的。
2. 策略模式的代码落地:从订单折扣场景写起
概念理解了之后,最关心的肯定是代码怎么写。我用一个实际场景来演示:电商订单系统的折扣计算。
2.1 第一版:典型的if-else写法(痛点现场)
我们先把大家最眼熟的版本写出来,先感受一下痛点:
java复制// 第一版:传统if-else,这段代码已经是精简版了
public BigDecimal calculateDiscount(String userLevel, BigDecimal amount) {
if ("VIP".equals(userLevel)) {
return amount.multiply(new BigDecimal("0.8"));
} else if ("GOLD".equals(userLevel)) {
return amount.multiply(new BigDecimal("0.85"));
} else if ("SILVER".equals(userLevel)) {
return amount.multiply(new BigDecimal("0.9"));
} else if ("NEW_USER".equals(userLevel)) {
return amount.multiply(new BigDecimal("0.95"));
} else if ("BIRTHDAY_MONTH".equals(userLevel)) {
return amount.multiply(new BigDecimal("0.7"));
} else {
return amount;
}
}
这段代码初看好像还行,但如果业务再膨胀一下:权重叠加、满减优惠、店铺级折扣、会员等级动态调整、不同支付方式额外折扣,这个方法的行数会迅速飙升。更麻烦的是,这个类不光是电脑折扣,还有优惠券计算、运费减免,它们可能散落在不同的Service里,改起来每处都要动一遍。
2.2 第二版:策略模式重构,一个接口加N个实现
现在用策略模式重构。先定义一个策略接口:
java复制public interface DiscountStrategy {
/**
* 计算折扣后的金额
*
* @param orderAmount 订单原始金额
* @return 折扣后的金额
*/
BigDecimal discount(BigDecimal orderAmount);
/**
* 每个策略应该知道自己适用哪种用户(或场景)
*/
String strategyType();
}
这里我故意多加了一个strategyType()方法,后面会讲到这是一个非常实用的设计,能帮你省掉一整个工厂类的代码。
接着是具体策略实现。我写四个典型的,展示不同策略类之间的独立性:
java复制public class VipDiscountStrategy implements DiscountStrategy {
@Override
public BigDecimal discount(BigDecimal orderAmount) {
// VIP用户统一8折,满1000再减50
BigDecimal afterDiscount = orderAmount.multiply(new BigDecimal("0.8"));
if (orderAmount.compareTo(new BigDecimal("1000")) >= 0) {
afterDiscount = afterDiscount.subtract(new BigDecimal("50"));
}
return afterDiscount;
}
@Override
public String strategyType() {
return "VIP";
}
}
java复制public class GoldDiscountStrategy implements DiscountStrategy {
@Override
public BigDecimal discount(BigDecimal orderAmount) {
// 黄金会员8.5折,不叠加其他优惠
return orderAmount.multiply(new BigDecimal("0.85"));
}
@Override
public String strategyType() {
return "GOLD";
}
}
java复制public class NewUserDiscountStrategy implements DiscountStrategy {
@Override
public BigDecimal discount(BigDecimal orderAmount) {
// 新用户首单满100减20
if (orderAmount.compareTo(new BigDecimal("100")) >= 0) {
return orderAmount.subtract(new BigDecimal("20"));
}
return orderAmount;
}
@Override
public String strategyType() {
return "NEW_USER";
}
}
再写一个Context类,把策略的选择和调用封装起来:
java复制import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class DiscountContext {
private final Map<String, DiscountStrategy> strategyMap = new ConcurrentHashMap<>();
/**
* 注册策略
*/
public void registerStrategy(DiscountStrategy strategy) {
strategyMap.put(strategy.strategyType(), strategy);
}
/**
* 获取某类用户的折扣后金额
*/
public BigDecimal execute(String userLevel, BigDecimal amount) {
DiscountStrategy strategy = strategyMap.get(userLevel);
if (strategy == null) {
throw new IllegalArgumentException("未找到对应的折扣策略: " + userLevel);
}
return strategy.discount(amount);
}
}
测试一下:
java复制public class StrategyDemo {
public static void main(String[] args) {
DiscountContext context = new DiscountContext();
// 注册策略(正常情况下,这些注册动作可以在Spring配置中完成)
context.registerStrategy(new VipDiscountStrategy());
context.registerStrategy(new GoldDiscountStrategy());
context.registerStrategy(new NewUserDiscountStrategy());
BigDecimal vipPrice = context.execute("VIP", new BigDecimal("1200"));
System.out.println("VIP用户1200元订单,折后价格: " + vipPrice);
BigDecimal newUserPrice = context.execute("NEW_USER", new BigDecimal("180"));
System.out.println("新用户180元订单,折后价格: " + newUserPrice);
}
}
你看,重构之后DiscountContext里的执行逻辑变得很干净,新增一个策略只需要重新实现接口并注册进去,以前那段几百行的if-else就彻底消失了。
2.3 为什么strategyType()方法这么重要
我把这个设计单独拿出来讲,因为它在实际项目中价值很高。
传统策略模式的例子通常会单独写一个工厂类,用一个静态switch-case来决定返回哪个策略实例。这样做确实比在主业务里写if-else好一点,但工厂类本身还是避不开条件分支。你加了新策略,工厂还得跟着改。
而让每个策略自己声明strategyType(),配合Spring的依赖注入或者简单的"注册表模式",你的策略工厂就变成了一个完全开放式的东西:来了新策略,实现接口、标注类型、注册进Map,完事。工厂代码再也不用动了。
如果你在用Spring框架,甚至可以更暴力一点:
java复制@Service
public class DiscountStrategyRegistry {
private final Map<String, DiscountStrategy> strategyMap = new ConcurrentHashMap<>();
public DiscountStrategyRegistry(List<DiscountStrategy> strategies) {
strategies.forEach(strategy -> strategyMap.put(strategy.strategyType(), strategy));
}
public DiscountStrategy getStrategy(String strategyType) {
return strategyMap.get(strategyType);
}
}
让Spring自动把你所有的DiscountStrategy实现类注入到这个Registry的构造函数里,由构造函数完成注册动作。后续新增策略,只需要加一个带@Service注解的实现类,其它代码全部不用动。
3. 策略模式的高级玩法:从函数式写法到无状态策略
前面这些是策略模式的经典Java写法。如果你接触语言比较杂,可能已经发现,在支持函数式编程的现代语言里,策略模式有个更轻量的变体:直接用函数对象代替一个个策略类。
3.1 Java 8+的函数式写法:把策略瘦身成Lambda
对于只有一个抽象方法的策略接口,Java里可以直接用Lambda表达式来写。这个技巧在业务相对简单的场景下特别好用,能省掉不少样板代码。
java复制// 定义策略接口,只有一个抽象方法
@FunctionalInterface
public interface CalculateStrategy {
int calculate(int a, int b);
}
java复制// 使用Java 8的Lambda表达式即可完成策略定义
public class Calculator {
// 直接使用Lambda作为策略实现
public static int execute(int a, int b, CalculateStrategy strategy) {
return strategy.calculate(a, b);
}
public static void main(String[] args) {
// 加法策略
int sum = execute(10, 5, (a, b) -> a + b);
// 减法策略
int diff = execute(10, 5, (a, b) -> a - b);
// 乘法策略
int product = execute(10, 5, (a, b) -> a * b);
System.out.println("sum=" + sum + ", diff=" + diff + ", product=" + product);
}
}
跑一下输出:
code复制sum=15, diff=5, product=50
这种写法虽然没有经典策略模式那么"正规",但它有一个天然优势:策略只存在于需要它的地方,不用的策略不需要单独建类。对于那种"临时组合一下算法"的场景,比如计算器、报表导出格式、校验规则组合,Lambda写法往往更清爽。
3.2 无状态策略与有状态策略的区别
写策略的时候,还有一个设计细节值得注意:尽量让你的策略类保持无状态。
无状态策略类不持有业务相关的成员变量(或者只有常量),各个线程可以安全地共享同一个实例。这样你在注册表里维护的单例策略对象就没有并发问题,也不用每次创建新对象。我前面写的VIP折扣策略就是无状态的,它的所有计算都是基于方法入参完成的,这种策略你用单例模式去管理完全没问题。
有状态策略则相反,它会在成员变量里保存一些状态,比如一个累加器、一个临时缓存、一个最后操作时间。这种策略一般不适合放在并发共享的注册表里,每次使用最好new新的实例。
我在实际项目中见过一个反面教材:某团队把一个"短信发送策略"写成了有状态策略,里面存了一个当前重试次数计数器。多个线程并发使用时,计数器互相踩踏,重试逻辑全乱套了。排查了半天,浪费了一整天的工时,改回无状态设计后问题才消失。这教训够深。
所以我的建议是:策略类的成员变量尽量用final修饰,策略方法尽量只依赖方法参数,不要依赖实例状态。如果你发现自己在一个策略里需要保存中间状态,果断改成"每次使用新建实例"或"把状态封装成上下文对象传入方法"。
4. 策略模式适用场景与不适用场景
策略模式不是银弹,用错了地方反而会坑你。我把自己这些年的经验整理成一份实用指南。
4.1 五个典型的适用场景
第一个是同一行为有多个实现方式,且调用方需要在运行时动态决定使用哪一种。支付渠道选择就是经典例子:用户选择微信支付、支付宝支付还是银行卡支付,支付策略各自独立,互不干扰。
第二个是系统中存在大量if-else或switch-case,且每个分支做的事情都很类似(都是同一接口的不同实现)。这时候策略模式能帮你消灭大部分分支逻辑。
第三个是算法或行为经常新增、变更。比如营销系统的促销规则,经常"周五上新玩法",策略模式让新增玩法变成一个"新增类"的动作,而不是"修改老代码"的动作。
第四个是不希望算法和业务逻辑混在一起。把算法抽到独立的策略类里,业务层只负责"选策略 -> 执行策略",职责清晰,测试也更方便。
第五个是系统有多个相似但又不完全相同的类,它们之间的区别仅在行为上。策略模式可以把这些行为抽取出来,减少重复代码。
4.2 策略模式不能碰的三个场景
不过下面这三种情况,我强烈建议你不要硬套策略模式。
第一种:策略数量极少,而且永远不打算增加。如果只有两种选择,几年都不会变,你非得上策略模式,那纯粹是给代码加复杂度的耍流氓行为。一个简单的if-else可能更好维护。
第二种:策略之间具有高度依赖关系。策略模式要求策略彼此独立,但如果你的"策略A"在计算时依赖"策略B"的结果,那它们本质上不应该是并列的策略,而更可能是责任链、模板方法或者状态机的问题。
第三种:业务规则是零散的点状逻辑,而不是成系列的算法。如果你发现某个所谓"策略"只在一处使用,以后也没有复用的机会,那把它放在原来的位子,别为了模式而模式。
4.3 策略模式 vs 模板方法模式:一字之差,应用场景完全不同
有一个经常和策略模式混淆的模式叫模板方法模式。这两个模式都处理"算法"问题,但它们的关注点有本质区别。
策略模式关注算法之间的替换:你有多个算法,可以换着用,算法的契约是相同的,算法内部的自有逻辑完全由策略自己决定。它倾向用组合。
模板方法模式关注算法的骨架复用:你把一件事情的步骤框架放在父类里定死,具体每一步的实现延迟到子类去做。它倾向用继承。
举个例子,做数据报表导出。导出的流程固定是三步:查询数据 -> 格式化数据 -> 输出到文件。用模板方法模式,你写一个ReportExporter抽象类,三步流程在export()方法里定死,子类分别实现三步的具体细节。而如果你有"导出Excel"和"导出CSV"两种完全不同的方式,每种方式的整体流程都不同,那就适合用策略模式。
这两者在实际项目中经常配合使用。模板方法模式定义主流程骨架,策略模式填充骨架中的某个变化环节,非常常见。
5. 策略模式的踩坑经验与代码坏味道
这一part分享我个人在工作里踩过的坑,以及如何识别代码中已经存在的"策略模式坏味道"。这些东西书里一般不会写,却是最值钱的经验。
5.1 坑一:策略类爆炸,一个策略一个类,类数量失控
策略模式最容易被吐槽的点就是"类太多":策略接口、N个具体策略,加上上下文、工厂,动不动就是十几个类。如果每个策略类代码就五六行,那你可能遇到了"过度设计"。
我处理这个问题的办法有两个。第一,在Java里配合Lambda使用,把一些特别小的策略直接内联,像我在3.1节展示的那样。第二,如果策略之间的差异只在某些参数上(比如折扣率不同),可以考虑让一个策略类接收参数,而不是创建一堆几乎一样的类。比如"满100减20"和"满200减50"这种,完全可以用一个FullReductionStrategy类,传入满减阈值参数就好。
5.2 坑二:策略类的选择逻辑反而变得更加混乱
正常情况下策略模式能把条件分支消灭掉,但我见过一种反例:有人在业务代码里这样使用策略:
java复制if ("VIP".equals(type)) {
strategy = vipStrategy;
} else if ("GOLD".equals(type)) {
strategy = goldStrategy;
} else {
strategy = normalStrategy;
}
那就等于把if-else从"折扣计算"挪到了"策略选择",问题没解决,反而多了一层类。这就是典型的"为了模式而模式"。
正确的做法是像2.2节那样,用注册表+策略自报类型的方式来消灭选择逻辑。如果你的语言和框架支持依赖注入、反射或者注解,尽量利用起来,让框架完成策略的装配工作。
5.3 坑三:策略接口设计得过于宽泛或过于狭窄
策略接口的设计是整件事成败的关键。接口太宽泛(方法太多)会让每个策略类都承担不必要的实现负担;接口太狭窄(只定义一个方法但参数不够)又会让策略之间无法灵活表达差异。
我的经验是:策略接口里的方法,入参最好是"上下文对象"而不是零散的多个参数。比如折扣策略,入参是Order对象,而不是老老实实地传orderAmount、userLevel、couponId、channelCode等一堆参数。因为随着业务发展,策略可能需要越来越多的信息,如果你一开始传的是一整颗对象,后续加参数就不用改接口了;你要是只传了几个零散参数,后续每次变都得动接口和所有实现类,那叫一个酸爽。
5.4 坑四:策略与其它模式的边界被模糊
策略模式经常和工厂模式、状态模式、命令模式一起出现,但它们解决的是不同层面的问题。
有一回我review代码,看到一个叫OrderStateStrategy的类,它既处理订单的不同状态流转,又处理不同状态下的操作,代码越写越难维护。本质上,状态流转做的是"根据当前状态决定下一个状态",这是一张状态迁移表,应该用状态模式;策略模式做的事是"在给定条件时执行不同算法",这是行为选择工具。
模糊这个边界的结果就是:策略类里开始出现大量的if (state == xxx)判断,又退回了老路。
5.5 如何识别代码中的策略模式坏味道
说了这么多,我送你几个快速识别的信号:
- 看到类名里有"Strategy"但类里却有大段if-else的。
- 看到某个
switch-case的case分支里调用的方法,每个分支的方法签名高度相似。 - 看到一个方法已经超过50行,且代码中根据类型字段(type/level/channel)在做分支分发。
- 看到多个类之间的核心方法代码逻辑相同,只有某一段计算规则不相同。
出现上面任何一个信号,都说明"可能"该用策略模式来整理一下了。但记住,是"可能",具体还是要结合业务有没有扩展需求来定。
6. 策略模式在真实框架中的应用:你看过的代码里到处都是它
策略模式之所以经典,是因为它无处不在。即使你没有在项目里显式地写过策略类,你日常用的开源框架里也大量使用了这个模式。
6.1 Spring框架中的策略模式影子
Spring的BeanFactory本身就是策略模式的应用:它只定义一个getBean()接口,底层用DefaultListableBeanFactory等不同实现来处理不同来源的Bean定义。不管是XML配置还是注解扫描,对使用方来说,getBean()的调用方式完全一样。
Spring MVC里的HandlerMapping更是策略模式的教科书案例:一个接口,多个实现,RequestMappingHandlerMapping负责处理@RequestMapping注解的请求,SimpleUrlHandlerMapping处理静态资源映射。框架根据请求信息选择合适的HandlerMapping去执行任务。
Resource接口也是策略模式思想:ClassPathResource、FileSystemResource、UrlResource都是Resource的具体策略实现,对使用者而言"读取资源"的行为是一致的,但底层的来源千差万别。
6.2 MyBatis、JDK源码中你能直接看到的策略应用
JDK的Comparator接口是策略模式最常见的应用。list.sort()方法接收一个Comparator作为排序策略,你可以传自然排序、倒序、按长度排序等等,排序方法本身不关心具体排序策略,只调用compare()方法。
MyBatis的Executor接口有三个实现:SimpleExecutor、ReuseExecutor、BatchExecutor,它们分别用不同的执行策略处理SQL。开发者在配置文件里指定ExecutorType,MyBatis就在运行时创建对应的Executor实例。
Java并发包里的ThreadPoolExecutor也用到了策略模式思想:它的RejectedExecutionHandler接口有AbortPolicy、CallerRunsPolicy、DiscardPolicy等实现,分别定义线程池拒绝任务时的不同处理策略。AbortPolicy是直接抛异常,CallerRunsPolicy是让提交任务的线程自己跑这个任务。这样线程池核心代码不用改,只要换一个拒绝策略实现,行为就完全变了。
这些框架案例其实都是在告诉我一个事:策略模式是让框架保持开放性的地基。框架作者没法预知所有用户的行为需求,但他们可以提供一套稳定的接口契约,让用户通过传入自定义策略来扩展框架能力。
7. 策略模式最佳实践:如何优雅地在一个项目里持续演进
最后聊聊,在一个真实项目里,策略模式怎么用才能越用越顺手,而不是一上来就整一堆类把自己埋了。
7.1 从重构出发,而非从设计出发
我的建议是:不要在项目最开始没有变化苗头的时候就堆策略模式。更务实的做法是,让if-else先写着,等你发现第三个相似的分支出现、或者产品开始频繁改规则时,再执行一次"以策略模式为核心的重构"。这样既能保证初始开发效率,又能在需要的时候果断还技术债。
这个思路是我在维护几个中型项目之后总结出来的:设计模式是重构的工具箱,不是开发的起点。新写一段代码时,业务逻辑就一行,你硬套策略模式确实显得多余;但业务逻辑反复膨胀的转折点一旦出现,策略模式就是收拾残局的最好工具。
7.2 策略与配置驱动相结合
把策略的注册和选择做成配置驱动,是一个很有意思的演进方向。比如把策略类型与业务条件的对应关系放在配置中心里(Nacos、Apollo或者简单的数据库表),运营通过改配置就能切换策略,而不是发版才能改策略。
我做过一个实践:让一个订单的优惠策略由"用户等级 + 订单来源 + 优惠券类型"三个维度决定。如果这三者的映射关系写死在后端代码里,每调整一次组合就要改动代码。后来我把这个映射关系放到配置表里,DiscountContext通过读取配置来决定选择哪个策略。运营人员直接在配置中心里调整策略组合,连发版都省了。这个设计让业务方体验极佳,上线后几乎没有再因为规则调整而打扰开发了。
7.3 配合枚举、Map与工厂模式做策略管
理策略的选择和装配,我建议用一个专门的Registry或Factory来管理,配合枚举定义策略类型更安全。比如:
java复制public enum DiscountType {
VIP("VIP"),
GOLD("GOLD"),
NEW_USER("NEW_USER");
private final String code;
DiscountType(String code) {
this.code = code;
}
public String getCode() {
return code;
}
}
使用枚举而不是裸字符串,能有效避免大小写不一致、拼写错误等问题,在编译期就能发现错误。配合Map做策略注册,代码非常稳健。
7.4 别忘了一切模式的根本:使变化可控
策略模式的本质,不是"消灭条件判断",而是"让变化变得可控"。它让新增策略的成本从"改老代码+回归"变成"新写一个类+注册",这中间的维护成本和风险差异是巨大的。但也要清醒认识到,敏捷开发的项目里代码永远在变,没有一劳永逸的设计。过一阵子你回头看现在的设计,可能又会觉得"这里应该用别的方式更好",这太正常了,只要保持学习和重构的心态就好。
最后分享一点个人经验
这些年做系统重构,策略模式是我用得最频繁的设计模式之一。回看最初写的那一版订单折扣if-else,重构后不单是代码短了,更重要的是团队新成员看代码的认知负担降了一大截。每次产品说"再新增一个促销玩法"时,团队再也不用翻那个几百行的方法了,新写一个类就好,代码审查也更轻松了。
如果你准备在自己的项目里引入策略模式,我建议步子稳一点:先从变化最频繁、条件分支最密集的那段业务开始,小范围重构,充分测试。等团队成员都熟悉了这种写法,再逐步推广到其它类似场景。设计模式这种东西,最重要的是用对地方,而不是用得越多越好。
