1. 一段让人头疼的 if-else成了策略模式的最好引子我敢打赌每个 Java 开发者都遇到过这样的代码一个方法里堆了七八个 if-else每个分支处理一种类型处理逻辑各不相同。一开始只有两三种写起来还挺开心后来需求越加越多代码膨胀到几百行改一个分支要小心翼翼翻半天测试一跑其他分支又坏了。这种“改一处炸一片”的痛苦几乎是所有人意识到设计模式价值的第一课。策略模式就是专门解决这类问题的。它的核心思想非常朴素把每一个“可以互换的处理逻辑”单独封装成一个对象然后在运行时由客户端决定用哪一个。代码层面看起来是加法——多了好几个类但维护成本上是减法——新增一种规则时你不需要去动原来的逻辑只需要加一个类然后告诉上下文“我要用这个新策略”。聊到这儿你可能已经猜到了策略模式属于行为型设计模式。它解决的不是对象怎么创建也不是类怎么继承而是“算法族”如何组织和扩展的问题。很多高手常说理解策略模式的关键不在于类图而在于“把变的部分和不变的部分拆开”。这句话我一会儿还会反复提到。1.1 为什么“多写几个 if 分支”会让你越写越慌当业务规则还少的时候if-else 是最直观、最好懂的写法。比如一个订单系统最初只有普通订单和 VIP 订单给一个价格普通订单不打折VIP 订单打 8 折。代码写出来非常清晰public BigDecimal calculatePrice(BigDecimal originalPrice, String userType) { if (NORMAL.equals(userType)) { return originalPrice; } else if (VIP.equals(userType)) { return originalPrice.multiply(new BigDecimal(0.8)); } throw new IllegalArgumentException(未知用户类型); }三个字挺好啊。可半个月后产品经理说“要加超级 VIP打 7 折”你在 else if 后面再补一行也能接受。再后来又推出了“满 500 减 80”“新用户首单减 20”“618 大促全场 75 折”……这些东西开始交叉组合你发现这个“无敌大 if”已经膨胀得没人愿意碰了。真正让人崩溃的还不是行数多而是逻辑被穷举式地堆在一个方法里。每加一种规则你都要重新审视整段代码生怕影响了已有的分支。而且这种写法把“选择策略的代码”和“策略本身的实现”完全耦合在一起。你没法单独测试一种计价方式也没法把某一种规则剥出来复用。时间一长这个方法的圈复杂度会警告拉满静态检查工具天天提醒你“too complex”。1.2 策略模式的官方定义到底在说什么策略模式的定义翻译过来是定义一族算法把每个算法都封装起来并且使它们之间可以互相替换。注意几个关键词一族算法比如各种打折、满减、补贴本质上是同一类行为——价格计算。各自封装每个算法一个类内部数据、方法自己管。可以互相替换客户端拿着同一个“策略接口”可以在运行时切换具体实现。网络上还流行一句话“策略模式将算法的变化独立于使用它的客户”。意思是调用方只需面对一个稳定的接口算法细节完全被隔离开。今天你用来计算价格的是 A 策略明天换成 B 策略调用方代码不需要改甚至策略对象可以在运行期动态替换。理解清楚定义再看类图就顺了。策略模式下有三个角色策略接口、具体策略、上下文。上下文负责持有策略接口的引用并把外部请求转发给策略对象。客户端决定用哪种策略并通过构造器、setter 或者工厂注入上下文。2. 策略模式的类图角色与设计原则很多教程喜欢一开始就把 Uml 类图扔出来但我更愿意先用一张“文字版类图”把角色关系理清楚。假设我们做一个支付模块策略接口是PayStrategy具体策略有AlipayStrategy、WechatPayStrategy、CardPayStrategy上下文是PayContext。PayContext里有一个PayStrategy字段和pay()方法客户端在构造PayContext时传入具体策略。类关系就是PayContext —— PayStrategy 接口 ▲ | --------------------------------- | | | AlipayStrategy WechatPayStrategy CardPayStrategy这不是继承下来的“父子关系”而是 组合/聚合 关系。上下文只依赖抽象接口具体策略实现接口。这个结构最关键的地方在于增加一种支付方式不需要动PayContext也不影响其他策略类。2.1 三个角色策略接口、具体策略、上下文第一个角色是策略接口。它可以是interface也可以是抽象类。强烈建议用interface因为策略实现通常没有共享的状态接口更干净。接口里的方法就是整个策略族的“统一动作”例如discount(BigDecimal amount)、pay(Order order)、export(Data data)。第二个角色是具体策略。每个具体策略实现策略接口包含自己的算法逻辑可以有自己的私有字段。比如打折策略需要折扣率满减策略需要门槛和减免金额。你可以通过构造器传入这些参数策略对象被创建后就固定了自己的“配置”。第三个角色是上下文。上下文是策略的“使用方”它内部持有一个策略接口引用并对外暴露业务方法。注意上下文本身不实现具体算法它只做“转发”。这是策略模式和模板方法模式很大的区别模板方法是父类定义骨架、子类实现步骤策略模式则完全把算法抽出去父类上下文不参与算法实现的任何细节。2.2 为什么组合优于继承面向对象里常讲“组合优于继承”策略模式就是这句话的典型例子。如果把各种计价规则写成父子类你会发现要么父类越来越臃肿要么子类之间代码重复严重而且很难在运行时改变对象的行为——继承关系编译期就定死了。用组合就是另一回事了上下文里塞一个策略接口引用你可以在运行期随时替换它。比如一个订单正在计算价格发现用户身份变了可以立刻换一个计价策略。这种动态性是继承很难做到的。另外组合让类的职责变得非常单一上下文只负责流程策略只负责算法。以后测试的时候你可以为每个策略单独写单元测试——这正是策略模式在工程上最大的隐性收益。2.3 一个完整可运行的 Java 实现纸上谈兵没意思写一个最小但完整的 Demo。场景是价格计算三种策略无折扣、VIP 打八折、满 500 减 80。先定义策略接口import java.math.BigDecimal; public interface DiscountStrategy { BigDecimal calculate(BigDecimal amount); }然后写三个具体策略public class NoDiscountStrategy implements DiscountStrategy { Override public BigDecimal calculate(BigDecimal amount) { return amount; } } public class VipDiscountStrategy implements DiscountStrategy { private static final BigDecimal VIP_DISCOUNT new BigDecimal(0.8); Override public BigDecimal calculate(BigDecimal amount) { return amount.multiply(VIP_DISCOUNT); } } public class FullReductionStrategy implements DiscountStrategy { private final BigDecimal threshold; private final BigDecimal reduction; public FullReductionStrategy(BigDecimal threshold, BigDecimal reduction) { this.threshold threshold; this.reduction reduction; } Override public BigDecimal calculate(BigDecimal amount) { if (amount.compareTo(threshold) 0) { return amount.subtract(reduction); } return amount; } }再定义上下文public class OrderPriceCalculator { private final DiscountStrategy strategy; public OrderPriceCalculator(DiscountStrategy strategy) { this.strategy strategy; } public BigDecimal calculate(BigDecimal amount) { return strategy.calculate(amount); } }客户端调用public class Client { public static void main(String[] args) { BigDecimal price new BigDecimal(600); OrderPriceCalculator noDiscount new OrderPriceCalculator(new NoDiscountStrategy()); System.out.println(noDiscount.calculate(price)); // 600 OrderPriceCalculator vip new OrderPriceCalculator(new VipDiscountStrategy()); System.out.println(vip.calculate(price)); // 480.0 OrderPriceCalculator fullReduction new OrderPriceCalculator( new FullReductionStrategy(new BigDecimal(500), new BigDecimal(80))); System.out.println(fullReduction.calculate(price)); // 520 } }注意我在 FullReductionStrategy 里把门槛和减免金额做成了构造器参数而不是硬编码。这一点非常重要。策略对象应该是可配置的而不是把所有参数写死。如果你把阈值写在类里面那每来一个新的“满 300 减 30”需求你就得再写一个类策略类会爆炸。这段代码也展示了策略模式的最小结构。它本身并不复杂真正复杂的是如何选择和获取策略。这就涉及注册表和容器的部分后面专门讲。3. 实战用策略模式重构购物车计价模块搞懂 Demo 只是开胃菜重点在于怎么用策略模式重构真实业务。我拿一个电商购物车来举例。这个例子我实际用来带过不少新人反馈非常直接。3.1 需求多种促销规则随时上新假设购物车里有多个商品最终结算时要计算总价。促销规则来自运营后台经常变化可能是“满 200 减 30”可能是“打 85 折”也可能是“第二件半价”。这些规则还可能叠加比如“先满减再打折”。如果用 if-else 写你会忍不住写出一串if (promotionType.equals(MANJIAN)) {...} else if (promotionType.equals(DISCOUNT)) {...}。更糟糕的是叠加规则会让代码嵌套越来越深。策略模式的做法是把“满减”“打折”“第二件半价”各自封装成策略类。购物车结算的主流程不需要知道具体规则它只调用策略接口的方法。最后你再写一个策略工厂或者注册表根据促销类型返回对应的策略对象。3.2 代码演进先写接口再写策略最后挂到上下文先定义促销策略接口注意方法参数。购物车要知道商品明细所以参数是Cart返回值是调整后的金额或优惠明细public interface PromotionStrategy { BigDecimal apply(Cart cart); }然后实现打折策略和满减策略。打折策略里需要折扣率字段满减策略需要门槛字段和减免字段public class DiscountPromotion implements PromotionStrategy { private final BigDecimal rate; public DiscountPromotion(BigDecimal rate) { this.rate rate; } Override public BigDecimal apply(Cart cart) { BigDecimal total cart.getTotalPrice(); return total.multiply(rate); } } public class FullReductionPromotion implements PromotionStrategy { private final BigDecimal threshold; private final BigDecimal reduction; public FullReductionPromotion(BigDecimal threshold, BigDecimal reduction) { this.threshold threshold; this.reduction reduction; } Override public BigDecimal apply(Cart cart) { BigDecimal total cart.getTotalPrice(); if (total.compareTo(threshold) 0) { return total.subtract(reduction); } return total; } }然后是叠加策略。所谓叠加本质也是一个策略它内部维护一个策略列表逐个执行。这就是策略模式的组合用法写起来很优雅public class CompositePromotion implements PromotionStrategy { private final ListPromotionStrategy strategies; public CompositePromotion(ListPromotionStrategy strategies) { this.strategies strategies; } Override public BigDecimal apply(Cart cart) { BigDecimal result cart.getTotalPrice(); for (PromotionStrategy strategy : strategies) { result strategy.apply(cart); } return result; } }最后结算上下文只关心一件事从外部拿到策略然后执行。public class CheckoutService { private final PromotionStrategy promotionStrategy; public CheckoutService(PromotionStrategy promotionStrategy) { this.promotionStrategy promotionStrategy; } public BigDecimal checkout(Cart cart) { return promotionStrategy.apply(cart); } }看到没CheckoutService是一个稳定不变的壳。以后运营加了新活动你只需要新增一个PromotionStrategy实现类CheckoutService一行都不用改。3.3 进阶用策略注册表彻底干掉类型判断前面只是用策略模式替代了业务逻辑里的 if-else但如果你在客户端还是要写new FullReductionPromotion(...)那“选择策略”的 if-else 实际上还存在。怎么消灭它常见做法是引入策略注册表。一个简单的注册表可以用MapString, PromotionStrategy存储所有策略然后提供一个get(String promotionType)方法public class PromotionStrategyRegistry { private final MapString, PromotionStrategy registry new HashMap(); public void register(String type, PromotionStrategy strategy) { registry.put(type, strategy); } public PromotionStrategy get(String type) { PromotionStrategy strategy registry.get(type); if (strategy null) { throw new IllegalArgumentException(不支持的促销类型: type); } return strategy; } }在系统启动时把所有策略注册进去PromotionStrategyRegistry registry new PromotionStrategyRegistry(); registry.register(DISCOUNT_85, new DiscountPromotion(new BigDecimal(0.85))); registry.register(FULL_200_MINUS_30, new FullReductionPromotion(new BigDecimal(200), new BigDecimal(30)));这样在业务代码里你只需要PromotionStrategy strategy registry.get(promotionType); BigDecimal result new CheckoutService(strategy).checkout(cart);不再有任何 if-else新增策略只需要注册一行。这个模式在 Spring 里还能更优雅后面我讲框架里的策略模式时会提到。当然注册表也有滥用风险。如果你的策略只有一两个非搞个注册表纯属过度设计。判断标准是策略种类是否频繁增减以及是否需要“按某个 key 动态选择策略”。如果这两个答案都是否老老实实写 if-else 反而更清晰。4. 在主流框架里找到策略模式的样子学设计模式最大的成就感不是背出定义而是在看开源框架时突然惊呼原来这里就是策略模式下面我举几个 Java 生态里最典型的案例。4.1 JDK / Java 生态中的案例最经典的当然是java.util.Comparator。Collections.sort()接受一个 Comparator 参数具体的排序算法由这个策略对象决定。比如正序、倒序、按某个字段排序只需要传入不同的Comparator实现。这个设计让sort方法摆脱了排序规则的硬编码。另一个常见的例子是ThreadPoolExecutor的拒绝策略。RejectedExecutionHandler就是策略接口AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy是四个具体策略。你创建线程池时传入其中一个决定了任务队列满后到底怎么处理。再看 Java 8 的Function、Predicate、Consumer等函数式接口本质上也是策略模式的函数式变体。当你把一个 Lambda 传到方法里你就是在动态地传递一个算法。很多 “Comparator” 都可以直接用 Lambda 语法简写因为它们是函数式接口。4.2 Spring Boot 自动注入策略 Map如果你的项目用了 Spring那策略模式写起来会爽很多。Spring 容器有一个特性如果一个接口有多个实现 Bean你可以把它们一次性注入到一个MapString, 接口类型中Map 的 key 是 Bean 名称。默认 Bean 名是对应类的首字母小写你也可以自己指定。举个例子你有两个价格策略实现类VipDiscountStrategy和FullReductionStrategy都实现了DiscountStrategy接口并且都加了 Component 注解。在另一个地方注入Service public class PriceService { private final MapString, DiscountStrategy strategyMap; public PriceService(MapString, DiscountStrategy strategyMap) { this.strategyMap strategyMap; } public BigDecimal calculate(BigDecimal amount, String strategyType) { DiscountStrategy strategy strategyMap.get(strategyType); if (strategy null) { throw new IllegalArgumentException(未知策略类型: strategyType); } return strategy.calculate(amount); } }这段代码可读性强扩展性也好。新策略加一个 Component 类就自动出现在 Map 里不需要手动维护注册表。但要注意Bean 名称拼写容易出问题。比如类名是VipDiscountStrategyBean 名默认是vipDiscountStrategy你传的strategyType得匹配。为了避免这种隐式约定出错我会在策略类上显式指定名字Component(VIP) public class VipDiscountStrategy implements DiscountStrategy { // ... }这样调用方传来 “VIP” 就能命中代码读起来更像业务语义。4.3 结合枚举和注解做一套可插拔的支付路由前面说到了 Spring 的 Map 注入再进一步可以把策略类型定义成枚举配合自定义注解指定策略编号。比如public enum PayChannel { ALIPAY, WECHAT, CARD }定义支付策略接口public interface PayStrategy { void pay(BigDecimal amount); }然后定义一个注解PayCode(PayChannel.ALIPAY)标注在每个策略实现类上。接着写一个PayStrategyFactory利用 Spring 初始化时收集所有带注解的 Bean放进路由表Component public class PayStrategyFactory { private final MapPayChannel, PayStrategy strategyMap new EnumMap(PayChannel.class); Autowired public PayStrategyFactory(ListPayStrategy strategies) { for (PayStrategy strategy : strategies) { PayCode payCode strategy.getClass().getAnnotation(PayCode.class); if (payCode ! null) { strategyMap.put(payCode.value(), strategy); } } } public PayStrategy get(PayChannel channel) { return strategyMap.get(channel); } }调用方式PayStrategy strategy payStrategyFactory.get(PayChannel.ALIPAY); strategy.pay(new BigDecimal(100));这样新增支付渠道时只需新增一个实现类 一个枚举值 注解标注业务代码不需要改。这种玩法在大厂里很常见本质上就是“策略 注册表 元注解”的组合拳。5. 策略模式实战避坑指南好话说了不少策略模式也不是银弹。我在项目里见过不少用策略模式用歪的例子列出几个高频大坑都是我从真实代码里总结出来的。5.1 这几个坑我基本每个项目都踩过第一个坑是策略类爆炸。每加一种规则就新增一个类这没问题但如果规则的变化点不是算法而是参数你就没必要疯狂造类。比如满减策略满 200 减 30 和满 300 减 50可以复用同一个FullReductionPromotion只是构造参数不同。很多新人不懂组合把每个参数组合都写成一个类结果策略类数量飞快膨胀维护成本反而变高。第二个坑是上下文变得臃肿。有些同学把很多和策略无关的操作塞进上下文比如在OrderPriceCalculator里写日志、更新数据库、发送消息。上下文是“代理商”不是“杂货铺”。它应该只负责持有策略、调用策略方法。把额外依赖塞进去会让上下文难以测试也不好复用。第三个坑是策略对象内部持有可变状态。策略通常是无状态的或者只在构造时设置参数。如果你在策略里加了一个private BigDecimal lastResult;之类的字段并且方法执行后会修改它那么这个策略在多线程环境下就会出现数据互串。解决方案很简单要么把策略做成无状态的要么在方法参数里传入需要的中间结果。第四个坑是过度设计。如果业务只有两种固定玩法以后也不打算加用策略模式就是浪费。设计模式不是越多越好写代码要讲平衡。5.2 策略模式与状态模式的那些年很多人会把策略模式和状态模式搞混因为它们的类结构很像都有一个接口、多个实现类对象内部持有接口引用。但两者意图完全不同。策略模式中策略的选择是由客户端决定而且策略之间没有“状态转移”关系。一个订单今天用 VIP 策略明天可能换成满减策略这种切换是外部环境造成的和对象内部状态没有必然关系。状态模式则把对象的核心行为绑定到什么发生了什么上个有价值。比如一个订单的状态有“待支付”“已支付”“已发货”“已完成”。不同状态下cancel()的做法完全不同“待支付”允许取消“已支付”取消需要退款“已发货”取消需要拦截物流。状态模式让对象在内部状态改变时自动切换行为而不是由外部的 if 判断状态后再执行不同逻辑。一句话记忆法策略模式是“换算法”状态模式是“换状态”。策略模式的切换方在外部客户端状态模式的切换逻辑由对象内部状态机驱动。5.3 面试题一问策略模式一定比 if-else 好吗这个问题面试官真的太爱问了。我的回答从来都是不一定。策略模式的本质是“封装变化”它的优势建立在“变化频繁”的前提下。如果一段分支逻辑只有两三个而且一年到头都不变写 if-else 更简单、更好读。滥用策略模式会引入过多类让项目更复杂。但是只要分支开始多起来——我这里说的不是简单判断而是每种分支里有大量属于自己的逻辑——那策略模式的收益就很明显了。你可以单元测试每个策略可以在 Spring 中通过配置切换策略可以随时加一个策略实现而不影响其他代码这些在 if-else 世界里都是一场灾难。面试时如果你能结合我前面说的“组合优于继承”“开闭原则”“策略注册表”来讲再举一个线程池拒绝策略的例子面试官大概率会觉得你是真用过而不是背概念的。6. 最后给新手的几条实操经验我见过很多初学者看了一堆设计模式教程合上书还是不会写。原因很简单缺少一个“从问题出发”的触发点。如果你问我什么时候最适合用策略模式我建议从这些信号开始留意代码里出现了成块的可替换逻辑而且每一块内部的代码量都不少。你发现自己经常在一个方法里用if (type ...)来选择不同的处理流程。产品经理说以后会经常加新的规则希望代码改动越小越好。当你识别出第一个信号后别急着重构。先把“算法接口”定义出来也就是抽出一个统一的动词比如calculate、pay、export。再为每种情况写一个类实现这个接口。最后找一个地方组装策略无论用 new、工厂、注册表还是 Spring 注入都行。在我实际开发里Spring 项目最舒服的写法就是策略接口 多个 Component 自动注入 Map。这套组合拳代码量不大可读性高扩展还特别方便。但如果你是写纯 Java 的小项目完全可以先用手写注册表。等你意识到需要框架的时候再把注册表替换成 Spring 的 Map 注入也不难。再说一个小技巧。策略模式可以和 Java 8 的 Lambda 配合。如果策略接口本身就是函数式接口例如只有一个抽象方法你完全可以用 Lambda 代替具体的策略类DiscountStrategy simpleVip amount - amount.multiply(new BigDecimal(0.8));但这里要小心Lambda 适合一次性、逻辑很简单的策略。如果策略本身有状态比如需要配置参数、需要组合、需要被复用那就老老实实建一个类。不能为了省几个 class 文件把代码可读性给牺牲掉。我在项目里还有一个习惯给每一个策略类取“业务友好”的名字。要么是类名直接表达业务含义比如SecondHalfPromotion要么通过注解或注册 key 来表达。很多人写策略类喜欢叫什么StrategyA、StrategyB这种名字三个月后连你自己都看不懂。策略模式已经帮你把逻辑拆成了独立单元你总得给这些单元起个有温度的名字吧。另外策略模式加上单元测试非常香。每个策略一个测试类输入输出一目了然。我一般还会在测试里覆盖边界值零元订单、恰好阈值、超大金额。这样一旦某个策略被改跑一遍测试就知道有没有出了问题。毫不夸张地说策略模式配合单元测试是重构遗留系统最好的组合之一。再补充一个很多人忽略的细节策略接口的异常设计。如果所有策略都可能抛出某种业务异常就把异常声明放到接口里或者在策略结果中包装返回。否则你在上下文里捕获异常时会发现不同策略抛了不同的异常处理起来很别扭。统一异常也是策略模式工程化的一部分。如果你现在正被一段巨长的 if-else 困扰可以动手试试策略模式。不用一步到位先抽出一个策略把最复杂的分支改掉观察代码结构的变化。设计模式这种事看一百篇文章不如亲手重构一次踩过那些“坑”之后你才能真正体会到策略模式的妙处。