
做业务系统时间久了你会发现一个规律需求文档里那句支持多种结算方式、按不同用户等级给折扣、多种导出格式最后十有八九会变成一坨失控的 if-else。我接手过一个促销模块接手时代码里躺着一百多行的条件分支每加一种活动类型就要动一次核心方法改完还得全量回归。后来用策略模式重写核心方法瘦到十几行新增活动类型只需要加一个类加一个注册。这篇文章就把策略模式Strategy Pattern从头到尾捋一遍——它是什么、什么场景该用、什么时候不该用、Java 和 C 的代码示例怎么写、上线后容易踩哪些坑。不管你是刚开始学设计模式的学生还是正在重构老代码的工程师看完都能直接抄作业。1. 策略模式到底在解决什么问题1.1 先从一段让人头皮发麻的 if-else 说起先看一段几乎每个后端都写过的代码一个订单价格计算逻辑public BigDecimal calcPrice(Order order) { if (NORMAL.equals(order.getType())) { return order.getAmount(); } else if (VIP.equals(order.getType())) { return order.getAmount().multiply(new BigDecimal(0.9)); } else if (SVIP.equals(order.getType())) { return order.getAmount().multiply(new BigDecimal(0.8)); } else if (PROMOTION.equals(order.getType())) { // 满减逻辑 if (order.getAmount().compareTo(new BigDecimal(300)) 0) { return order.getAmount().subtract(new BigDecimal(50)); } return order.getAmount(); } else if (COUPON.equals(order.getType())) { // 优惠券逻辑 // ... } throw new IllegalArgumentException(未知类型); }这段代码刚写出来的时候没什么问题逻辑清晰、一眼能看懂。真正的麻烦是在后面每种类型的计算规则会被产品反复调整VIP 折扣从 9 折改到 85 折、满减门槛从 300 改到 200、优惠券要叠加使用……每改一次这个方法的圈复杂度就往上窜一点测试同学被迫把整个方法的所有分支重新跑一遍。问题不在 if-else 本身而在变化被压在了一个方法里。策略模式的思路很朴素把每一种算法单独拎出来封装成一个类让它们实现同一个接口运行时由调用方决定用哪一个。调用方只认接口不认具体实现。这样变化被限制在各自的类里新增一种算法就是新增一个类不用再动已有的代码。从设计原则的角度讲这正好命中了开闭原则对扩展开放、对修改关闭和单一职责一个类只管一种算法。很多人背设计模式原则背得很熟但一到写业务代码就忘了本质上就是没有在真实场景里体会过改一处崩三处的痛。1.2 策略模式的三个核心角色抛开各种变体策略模式最核心的就三个角色记住这三个后面所有写法都是它们的排列组合策略接口Strategy定义所有算法必须遵守的契约通常是接口或抽象类声明一个执行方法。比如PriceStrategy里有个calculate(Order order)。具体策略ConcreteStrategy接口的真正实现每个类负责一种具体算法。VipPriceStrategy、FullReductionStrategy各管一摊。上下文Context持有策略接口的引用对外提供统一入口内部把请求转发给当前策略。它不关心具体是哪个策略只负责调度。用生活化的类比策略接口就像支付方式这个概念具体策略是微信、支付宝、银行卡上下文是收银台。收银台只负责说一句请付款至于你用什么方式付是顾客自己决定的收银台不需要为每种支付方式单独开一个窗口。关键在于上下文和具体策略之间是组合关系不是继承关系。这一点经常被初学者搞混。如果你用的是继承那子类只能在编译期确定行为用组合行为可以在运行时动态替换。这个差别看似小实际上决定了你的代码能不能做到用户点一下下拉框就切换算法。1.3 别把策略模式和简单工厂、模板方法搞混面试和实际工作里这三个模式经常被混着问。简单区分一下模式核心目的关注点典型信号策略模式封装可互换的算法行为怎么变多种算法按条件切换简单工厂封装对象的创建对象从哪来不想在调用方 new模板方法固定骨架、变化步骤流程骨架不变步骤固定但细节不同简单工厂解决的是创建的问题策略模式解决的是选择和执行的问题两者经常配合使用——用工厂来生产策略用策略来执行算法。而模板方法用的是继承父类定义流程骨架子类重写其中某几步它的变化维度是流程内的步骤策略模式的变化维度是整个算法。你如果在业务里看到一段代码既有固定流程又有多种变体很可能是两者结合的场景比如导出报表的流程固定取数、组装、输出但取数方式因数据源而异这时候流程用模板方法取数用策略。我个人的经验是判断该不该用策略模式就看那段变化的逻辑有没有被独立替换的需求。如果只是两三个固定分支、几个月都不改一次硬套策略模式反而增加类数量得不偿失。这个边界感比背模式定义重要得多。2. 哪些场景真的适合上策略模式2.1 高频适用场景清单结合我做过和看过的项目策略模式最名正言顺的场景大概是这几类计费与定价会员折扣、阶梯价、满减、组合优惠。这是最经典的场景因为规则变化频繁且各规则之间相对独立。支付渠道下单后走微信、支付宝、余额、积分支付每种渠道的调用参数和回调处理都不一样。多种导出/解析格式把数据导出成 CSV、Excel、PDF或者解析不同来源的报文。风控与校验规则不同业务线走不同的校验策略比如实名认证、额度校验、黑名单校验。压缩/加密算法选择同一个接口根据配置选择不同的压缩级别或加密方式。路由与负载均衡策略轮询、随机、加权、一致性哈希这些都是策略的教科书级例子。这些场景有共同特征算法之间互斥、逻辑独立、新增频率高、且调用方不需要知道内部细节。只要满足这几条用策略模式几乎不会出错。2.2 别硬上不适用策略模式的信号反过来下面这几种情况我劝你别上策略模式硬上只会让代码更难维护分支只有两三个且几乎不变。比如一个布尔开关用 if-else 就两行抽成两个类纯属给自己添堵。各分支之间共享大量状态。如果每种算法的实现都要读写同一个复杂的上下文对象独立成类反而会引入各种传递和同步问题。分支逻辑很短但互相纠缠。这时候该做的是拆方法、抽函数而不是抽类。性能极度敏感的循环内。策略模式多了一层接口调用多态分发虽然 JIT 一般能优化掉但在超高频的热点路径上仍然要实测。一个很实在的判断方法如果你数出来的分支数量少于 4而且未来半年内没有明显扩张预期先别动。设计模式是解药不是保健品没病别乱吃。2.3 策略模式配简单工厂的经典组合单独用策略模式有个小尴尬调用方得知道有哪些策略、怎么选。这时候通常配一个工厂或者注册表把选择这件事也封装掉。基本结构是这样// 策略接口 public interface PriceStrategy { String getType(); BigDecimal calculate(Order order); } // 上下文 public class PriceContext { private final MapString, PriceStrategy strategies new HashMap(); public PriceContext(ListPriceStrategy list) { for (PriceStrategy s : list) { strategies.put(s.getType(), s); } } public BigDecimal calc(String type, Order order) { PriceStrategy strategy strategies.get(type); if (strategy null) { throw new IllegalArgumentException(不支持的策略: type); } return strategy.calculate(order); } }这样调用方只需要传一个 type上下文自己去 map 里找。新增策略时只要实现接口并注册进去calc方法一行都不用改。这就是开闭原则落地的样子。我试过更进一步的写法把 type 判断完全去掉用 Spring 的Component自动扫描靠 Bean 的名字或者自定义注解来标识类型这样连注册列表都不用手动维护新增策略文件一放就生效。这个后面代码示例部分会细讲。提示策略的 key 一定要做空值和重复校验。我见过两个同事各自加了个策略type 字符串撞车了线上直接按 Map 后写入的覆盖排查了半天。启动时做一次校验能省掉这种破事。3. 优缺点掰开揉碎讲3.1 它真正带来的收益策略模式的优点在教科书上写得很标准但我更想从实际收益角度说第一可读性提升是最直接的。原本一个方法里塞着七八种算法读者要在一屏屏幕里来回跳拆成策略类后每个类的名字就说明了它干什么VipPriceStrategy一眼就知道是干嘛的。新人接手时的心智负担下降非常明显。第二扩展成本被大幅压平。老代码里加一种类型你要找到那个巨型方法、小心翼翼地插入一个 else if、然后祈祷不破坏原有分支。策略模式下新增一个类、注册一下、写个单测就完事改动是局部的、可预测的。第三可测试性变好。每个策略类可以独立写单元测试不用为了测一种算法去构造整个上下文的全部依赖。我们做重构时最先感受到的好处就是这个——原来测 VIP 折扣需要跑通整条链路现在直接 new 一个策略类 assert 结果就行。第四支持运行时切换。这是继承方案给不了的。用户在下拉框里选按月导出还是按季度导出策略对象可以在运行时替换代码里不需要任何条件判断。3.2 被低估的代价与埋坑优点说完得泼点冷水。策略模式的代价在教科书里往往一笔带过实际用起来才知道疼类数量膨胀。这是最直观的问题。五种算法就是五个类十个算法就是十个类再加工厂、上下文、配置。一个功能模块动不动十几个文件对不熟悉的人反而更难建立整体认知。这个问题的缓解办法是用枚举 lambda 简化或者只在算法确实复杂、确实需要独立修改时才落地成类。调用方需要理解策略的存在。虽然封装了选择逻辑但调用方通常还是要知道有哪些 type 可用。如果 type 字符串散落在各处业务方用错字符串的 bug 会层出不穷。解决办法是把 type 收敛到枚举或常量类里禁止裸字符串。策略之间如果存在共享状态处理起来别扭。有种情况是多个策略需要用到同一个计算结果比如都依赖用户画像。这时候要么在上下文里预先算好传进去要么抽个公共父类但抽父类又会引入继承耦合。这个没有银弹得看实际情况权衡。调试链路变长。断点调试时请求从上下文跳到接口再跳到具体实现调用栈比一层 if-else 长。排查问题时如果对代码不熟会觉得怎么绕来绕去。这是我刚用策略模式时最大的不适应后来习惯了反而觉得层次清楚。一个务实的建议先写单元测试再重构。把原巨型方法的行为用测试锁定住再往外抽策略类每抽一个跑一遍测试。这样能保证重构过程中行为不变比凭感觉改安全得多。4. Java 代码示例从能跑到能上线4.1 基础版接口加实现类先把最朴素的版本写出来理解骨架。定义接口public interface DiscountStrategy { BigDecimal apply(BigDecimal amount); }两个实现public class VipDiscount implements DiscountStrategy { Override public BigDecimal apply(BigDecimal amount) { return amount.multiply(new BigDecimal(0.9)).setScale(2, RoundingMode.HALF_UP); } } public class SvipDiscount implements DiscountStrategy { Override public BigDecimal apply(BigDecimal amount) { return amount.multiply(new BigDecimal(0.8)).setScale(2, RoundingMode.HALF_UP); } }上下文持有策略public class DiscountContext { private DiscountStrategy strategy; public DiscountContext(DiscountStrategy strategy) { this.strategy strategy; } public void setStrategy(DiscountStrategy strategy) { this.strategy strategy; } public BigDecimal execute(BigDecimal amount) { return strategy.apply(amount); } }调用侧DiscountContext ctx new DiscountContext(new VipDiscount()); BigDecimal price ctx.execute(new BigDecimal(1000));这个版本的好处是结构清晰、看懂只需几分钟坏处是选择逻辑还留在调用方跟没重构差不太多。所以它一般只用于教学或者策略选择极简单的场景。4.2 枚举 工厂注册版实战里更常用的是把策略注册到一张表里对外只暴露一个统一的执行入口。用枚举来收敛 type避免裸字符串public enum OrderType { NORMAL, VIP, SVIP, FULL_REDUCTION }策略接口带上类型标识public interface PriceStrategy { OrderType type(); BigDecimal calculate(BigDecimal amount); }具体策略实现类这里举个满减的例子Component public class FullReductionStrategy implements PriceStrategy { private static final BigDecimal THRESHOLD new BigDecimal(300); private static final BigDecimal REDUCE new BigDecimal(50); Override public OrderType type() { return OrderType.FULL_REDUCTION; } Override public BigDecimal calculate(BigDecimal amount) { if (amount.compareTo(THRESHOLD) 0) { return amount.subtract(REDUCE); } return amount; } }注册表用 Spring 收集所有实现Component public class PriceStrategyRegistry { private final MapOrderType, PriceStrategy map new EnumMap(OrderType.class); public PriceStrategyRegistry(ListPriceStrategy strategies) { for (PriceStrategy s : strategies) { PriceStrategy prev map.put(s.type(), s); if (prev ! null) { throw new IllegalStateException(重复的策略类型: s.type()); } } } public PriceStrategy get(OrderType type) { PriceStrategy s map.get(type); if (s null) { throw new IllegalArgumentException(未注册的策略: type); } return s; } public BigDecimal calc(OrderType type, BigDecimal amount) { return get(type).calculate(amount); } }这个版本是我强烈推荐的落地形态。几个细节值得注意用EnumMap而不是HashMap前者对枚举 key 有空间和性能优化也更语义化。构造时做重复校验启动即失败比线上运行时才发现好一万倍。新增策略只需新建一个类加Component注册表自动收集业务代码不用改。4.3 参数计算中容易翻车的点金额计算有几个必须盯紧的地方跟策略模式本身无关但放在一起讲更实用金额不要用 double。浮点误差在金融场景是灾难必须用BigDecimal。我见过有人图省事用 double 算折扣结果测试环境 100.00 变成 99.999999一下午都在查这个。除法要指定精度和舍入模式。BigDecimal做除法不指定scale会直接抛ArithmeticException因为它不知道该保留几位。统一用setScale(2, RoundingMode.HALF_UP)具体位数按业务约定来。策略里不要隐藏副作用。好的策略应该是输入金额、输出金额的纯函数式逻辑。如果某个策略里偷偷改了数据库、发了消息那它就不仅仅是策略了调试时会非常痛苦。要保持策略的职责单一副作用交给上层编排。优先级的处理。如果多种策略需要叠加先打折再满减别在一个策略里硬塞而是用一个组合策略把多个策略串起来这也是策略模式的一个常见扩展。4.4 C 代码示例C 里策略模式通常用抽象基类加智能指针实现。下面是个压缩策略的例子#include iostream #include memory #include string // 策略接口 class CompressStrategy { public: virtual ~CompressStrategy() default; virtual std::string compress(const std::string data) const 0; }; // 具体策略 class ZipStrategy : public CompressStrategy { public: std::string compress(const std::string data) const override { return [zip] data; } }; class GzipStrategy : public CompressStrategy { public: std::string compress(const std::string data) const override { return [gzip] data; } }; // 上下文 class Compressor { public: explicit Compressor(std::unique_ptrCompressStrategy s) : strategy_(std::move(s)) {} void setStrategy(std::unique_ptrCompressStrategy s) { strategy_ std::move(s); } std::string run(const std::string data) const { return strategy_-compress(data); } private: std::unique_ptrCompressStrategy strategy_; }; int main() { Compressor c(std::make_uniqueZipStrategy()); std::cout c.run(payload) std::endl; c.setStrategy(std::make_uniqueGzipStrategy()); std::cout c.run(payload) std::endl; return 0; }C 版本有两个关键点要留心必须给基类加虚析构函数。因为要 delete 指向基类的指针没有虚析构会导致派生类的析构不被调用是内存泄漏的常见原因也是面试高频考点。用unique_ptr管理所有权别用裸指针。栈上直接构造或者用make_unique都比new安全异常安全也有保障。编译时如果用了较新的标准记得开-stdc14以上。这套写法在实际工程里很常见很多库的内部就是靠策略接口切换不同算法实现的。5. 常见问题与排查技巧实录5.1 常见问题速查表下面这张表是我在实际工作里踩过或者帮同事排查过的问题按出现频率排现象常见原因排查/解决思路抛未注册策略type 字符串写错、枚举新增未实现检查枚举与策略type()是否一致启动加校验两个策略效果互相覆盖注册表的 key 重复构造时检测重复并快速失败金额计算出现小数尾巴用了 double 或未指定 scale全链路改BigDecimal并统一舍入Spring 注入报多个 bean策略接口被多处实现且按类型注入改成按 name 注入或用ListT收集新增策略不生效类没加Component或不在扫描包检查包路径与注解或查看注册表日志调试时请求绕来绕去上下文与策略层次多在注册表get处打日志打印实际命中的策略单元测试难以隔离策略里塞了外部依赖把副作用抽离策略只做纯计算5.2 我踩过的几个真实的坑坑一type 用了裸字符串拼错无感知。早期版本里我用VIP、vip混着写测试环境能过是因为只覆盖了一种。后来把所有 type 收敛成枚举编译期就能发现拼写错误。这一条教训让我从此拒绝在业务代码里裸写状态字符串。坑二策略里做了数据库查询。有个折扣策略内部查了下用户等级结果每次计算都产生一次数据库 IO压测时 QPS 上不去。后来改成在入口处一次性查好作为参数传给策略策略保持无状态性能立刻恢复。策略尽量设计成无状态、纯计算这个原则省钱又省心。坑三把策略当成了万能解药。有段时间我看谁家的多分支都想抽策略结果一个只有三个分支的小模块被拆成了六个文件团队里怨声载道。后来自己定了个规矩分支少于四个、且没有明显扩展预期的坚决不抽。这个门槛帮我省下了不少过度设计的时间。坑四忽略了线程安全。如果上下文对象被多个线程共享而setStrategy又会在运行时被调用那就有并发问题。要么把上下文做成无状态的每次调用传入策略要么保证策略设置只在初始化时发生。Spring 默认单例的 bean 更要小心这一点。5.3 一点实操心得分享一个我在重构时的做法先不动生产代码而是给原来的巨型方法写一组参数化测试把各种输入输出的组合全部固定下来。这一步做完重构就有了安全网。然后一小步一小步地往外抽策略类每抽一个跑一次测试。整个过程没有一次线上事故测试同学也放心。另外一个细节是策略的命名。别用Strategy1、StrategyA这种名字要用业务语言命名NewUserDiscountStrategy、FestivalFullReductionStrategy。名字本身就是文档团队里看类名就知道这个策略干什么减少大量口头沟通。最后说一个扩展方向。策略模式往后走还有几种玩法一是传参用函数式接口Java 8 之后可以直接用FunctionBigDecimal, BigDecimal或者BiFunction把简单的策略写成 lambda只有复杂策略才落地成类这样能在灵活性和类数量之间找个平衡二是引入责任链把多个策略串成链依次执行适合多重校验这种场景三是用配置驱动把策略组合关系扔到配置中心运行时动态调整而不需要发版。这些都要看业务复杂度别为了炫技而堆模式。选型这件事说到底还是那句老话模式是工具不是目的。能跑、好改、别人接手看得懂就是好代码。策略模式在你需要同一件事有多种做法且会不断新增做法的时候是首选其他时候保持克制。