
1. 为什么说策略模式是消灭if-else的终极武器如果你写过一段时间的业务代码一定遇到过这种情况一个方法里密密麻麻排了十几个if-else每来一个新需求就往里加一个分支。刚接手的时候还能看懂半年之后再回去看自己都想抽当时的自己。我有一个做电商系统的朋友光是订单折扣这一块的代码就有两百多行里面嵌套了七八层判断。每次产品经理过来说加一个新活动他都要在那坨代码里找半天该往哪里塞。这个问题的本质是你把算法的选择和算法的实现耦合在了一起。而策略模式一个很朴素的想法就是把这个状态拆开。在23种设计模式里策略模式属于行为型模式名字叫Strategy。它做的事情一句话就能概括定义一族算法把它们分别封装起来并且让它们之间可以互相替换。这个模式的别名也叫Policy它的核心思想是把变化的逻辑抽出去让稳定的部分不再依赖具体实现而是依赖一个抽象接口。听起来有点绕但对一个需要写代码挣钱吃饭的人来说理解它最好的方式就是动手写一版对比。这个系列写到这里前面的创建型、结构型模式已经覆盖得差不多了到了行为型策略模式是一个绕不开的入口。我没有按照正统教材的顺序把它放在行为型的最后一篇是因为它在真实项目里的出场率实在太高了——支付渠道切换、折扣规则计算、校验逻辑、排序策略、文件解析格式几乎每一个涉及多种算法可替换的业务场景底子上都是策略模式那套骨架。这篇博文我会用一个电商订单的折扣计算案例从头到尾拆一遍包含完整C实现、Java实现和Python实现再聊一聊它在Spring里的典型应用形态最后把我在实际项目里踩过的坑一并交代清楚。内容适合设计模式入门的新手也适合写了两三年业务代码想系统整理一下思路的同学。2. 策略模式的本质把变和不变分开2.1 一段最原始的代码看看问题在哪先看一个最典型的反面教材。假设现在电商系统里有一个订单类需要根据用户类型计算折扣。刚起步的时候逻辑很简单普通用户不打折会员打九折VIP打八折。新手很容易写出下面这种代码public double calculatePrice(Order order, User user) { if (user.getType().equals(NORMAL)) { return order.getAmount() * 1.0; } else if (user.getType().equals(MEMBER)) { return order.getAmount() * 0.9; } else if (user.getType().equals(VIP)) { return order.getAmount() * 0.8; } else { return order.getAmount() * 1.0; } }这段代码跑起来没有问题但它有几个隐患藏得很深。第一违背了开闭原则。哪天公司要做个88VIP双重叠加折扣你得改这个方法而改一个到处被调用的公共方法有可能把其他分支的逻辑顺带弄挂。第二分支多到一定规模就不可维护。当判断条件从3个涨到10个这个方法的圈复杂度已经高到任何测试都难以覆盖全路径。第三算法本身和调用方强绑定。折扣规则属于业务策略有可能在多个模块订单、售后、对账里都要复用现在它被困死在一个方法中。实际业务里我还见过更离谱的版本判断用户类型用的是字符串拼接条件里再套条件那已经不是if-else了那是俄罗斯方块。2.2 策略模式怎么解开这个死结要解开这个死结策略模式引入了三个角色。Context上下文持有一个策略对象的引用负责调用策略。它不关心策略内部怎么实现只关心算法对外暴露的接口长什么样。对你来说这个Context就是那个做计算的入口比如OrderService。Strategy抽象策略定义一个公共接口让所有具体策略都实现这个接口。它约束了算法长什么样但是不管算法怎么实现。ConcreteStrategy具体策略实现抽象策略接口的具体算法类。每个类封装一种算法互不干扰。用一句话来理解这个结构Context是导演Strategy是剧本大纲ConcreteStrategy是不同版本的剧本。导演只看大纲演戏至于剧本里具体写了什么台词导演不关心你给他哪本他就演哪本。以后来了新剧本导演不用改直接换一本就行。2.3 为什么这个模式能干掉if-else关键在于选择逻辑被转移了。原来的代码里调用方自己做判断自己选算法自己执行算法。改造之后调用方只需要在创建Context的时候传入一个具体的策略对象算法选择的职责被移交给了外部。判断从在方法内部变成了在调用前决定传哪个对象进去这看起来像是把问题换了个位置但实际上完全不同。原来算法A和算法B挤在一个方法里它们共享同一个局部变量的命名空间改A容易碰坏B。现在算法A和算法B是两个物理隔离的类各自维护各自的逻辑测试互不影响新增算法不用动旧算法。另外一点很务实策略模式天然符合依赖倒置原则。上层模块不再依赖底层具体实现而是依赖抽象接口。这在做单元测试的时候就舒服了——你可以很方便地替换一个Mock策略不用为了测试某条业务链路而把真实的算法类加载起来。3. 用一个电商案例带你手写一遍完整改造流程3.1 定义策略接口让所有算法长得一样先说设计思路。基于常见的实践我先定义一个统一的折扣策略接口让计算折扣后金额这个语义变得统一。无论后面来了多少种活动接口签名不需要再变。public interface DiscountStrategy { /** * 计算折扣后的订单金额 * param amount 原始订单金额 * return 折扣后的金额 */ double applyDiscount(double amount); }这个接口里的方法就是所有策略的契约。这里有一个容易被忽略的点也是我想提醒你的接口方法命名要语义化最好带上业务的意图而非操作。我当时接手过一套代码接口方法叫methodA三个实现类里分别对应三种完全无关的业务策略那才是灾难。接口是给调用方看的起个好名字比什么都重要。3.2 实现三个具体策略每套算法一个类现在实现三个最基础的打折策略普通用户无折扣、会员打九折、VIP打八折。public class NormalDiscount implements DiscountStrategy { Override public double applyDiscount(double amount) { return amount; } } public class MemberDiscount implements DiscountStrategy { Override public double applyDiscount(double amount) { return amount * 0.9; } } public class VipDiscount implements DiscountStrategy { Override public double applyDiscount(double amount) { return amount * 0.8; } }每个类都单独存在于自己的文件中互不影响。这一步看起来平平无奇价值在于后面如果超级会员要在VIP基础上再打一次折你只需要新增一个SuperVipDiscount类其他任何代码都不用改。这是策略模式最基本的一条纪律新算法来了不改老的实现类只新增一个类。如果你发现为了实现新策略得回头改NormalDiscount或者MemberDiscount说明策略接口设计失败了把公共不变的部分和变化的部分混在一起了。3.3 改造上下文封装用策略这件事接下来是Context。它负责持有策略、执行策略。这里有一个经验之谈Context不能只是简单存一个策略字段它应该向调用方屏蔽策略调用的细节。public class OrderService { private DiscountStrategy strategy; // 可以通过构造方法注入也可以提供setter方法切换策略 public OrderService(DiscountStrategy strategy) { this.strategy strategy; } public void setStrategy(DiscountStrategy strategy) { this.strategy strategy; } public double calculate(Order order) { double amount order.getAmount(); // 这里可以加一些公共逻辑比如金额校验、日志记录等 if (amount 0) { throw new IllegalArgumentException(订单金额不能为负数); } System.out.println(使用策略: strategy.getClass().getSimpleName()); return strategy.applyDiscount(amount); } }看到这里你会发现Context做的事情非常薄它只是”持有策略对象并调用“确实如此。但这恰恰是策略模式最重要的一个设计原则——把易变的策略隔离开不让它污染稳定逻辑。在真实项目里这个Context往往不是单独存在的它可能是一个领域服务也可能是一段业务编排逻辑。无论如何稳定的骨架和可替换的策略之间必须有一条清晰的边界。3.4 客户端调用和普通代码有什么不一样最后是客户端的调用方式public class Client { public static void main(String[] args) { Order order new Order(1000.0); // 使用会员折扣策略 OrderService service new OrderService(new MemberDiscount()); System.out.println(会员价格: service.calculate(order)); // 动态切换为VIP策略 service.setStrategy(new VipDiscount()); System.out.println(VIP价格: service.calculate(order)); } }注意看客户端的判断逻辑消失了。原先的calculatePrice方法里的if-else全部消失了。现在决定用哪个策略是在客户端代码里决定的而且这个决定可以发生在运行时——同一个OrderService对象可以随时切换策略这是if-else做不到的。这一点往深处想它是策略模式区别于其他行为型模式比如状态模式的关键。策略模式中切换策略通常是由调用方主动发起的策略之间是平行、互相独立的关系。而状态模式中状态对象自己负责把Context切换到下一个状态状态之间是流转的。4. 多语言视角下策略模式到底怎么落地4.1 Java语境从接口到函数式接口的演进Java里最经典的策略模式范例其实是JDK自带的Comparator接口。你注意Comparator的定义就满足一个算法一个类的结构——按年龄排序是一个Comparator实现按姓名排序是另一个实现。Collections.sort方法就是ContextComparator就是策略接口。Java 8引入了Lambda表达式之后策略模式在Java里的写法发生了微妙的变化。对于只有一个抽象方法的接口函数式接口可以直接用Lambda表达式替换具体策略类不再需要为每个策略单独建一个类public class LambdaClient { public static void main(String[] args) { Order order new Order(1000.0); // 直接用Lambda表达式作为策略对象 OrderService service new OrderService(amount - amount * 0.85); System.out.println(今日活动价: service.calculate(order)); } }这里有一个我实际用下来比较认同的建议策略逻辑如果只有一两行用Lambda没有毛病但策略逻辑超过三行或者这个策略可能被多处复用还是老老实实抽成类。Lambda写起来爽但代码里塞了七八个Lambda表达式之后可读性照样会崩而且没法复用、没法单独测试。模板方法模式往往和策略模式混在一起出现。区别在于策略模式是通过组合委托替换算法模板方法是通过继承重写固定算法骨架。前者的关系是有一个后者的关系是是一个。在Java里尽量多用组合、少用继承这本身就是Effective Java里的一条核心建议。4.2 Python语境函数本身就是最好的策略Python写策略模式有一个天然优势——函数是一等公民。可以不定义策略接口直接拿函数当策略用。# 定义三个策略函数注意它们拥有相同签名 def normal_discount(amount): return amount def member_discount(amount): return amount * 0.9 def vip_discount(amount): return amount * 0.8 class OrderService: def __init__(self, strategy): self.strategy strategy def calculate(self, order): amount order.get_amount() if amount 0: raise ValueError(订单金额不能为负数) return self.strategy(amount)调用方式更直接order Order(1000) service OrderService(member_discount) print(service.calculate(order)) service.strategy vip_discount print(service.calculate(order))这里有个很多Python新手容易误解的地方策略接口并不是必须存在的。Python的鸭子类型决定了只要两个可调用对象签名一致就可以互换使用。但我要给出一点出自实战的提醒——如果项目里有大量字典分发或者多个策略彼此之间有复杂关联仍然推荐建立一个基础类哪怕不写抽象方法只做类型标注至少能给后来的维护者一个明确的语义锚点。Python提供的是自由但自由需要靠纪律来约束。4.3 C语境多态策略与std::function双路径C实现策略模式最经典的方式用多态#include iostream #include memory class DiscountStrategy { public: virtual ~DiscountStrategy() default; virtual double applyDiscount(double amount) const 0; }; class NormalDiscount : public DiscountStrategy { public: double applyDiscount(double amount) const override { return amount; } }; class MemberDiscount : public DiscountStrategy { public: double applyDiscount(double amount) const override { return amount * 0.9; } }; class OrderService { private: std::shared_ptrDiscountStrategy strategy_; public: explicit OrderService(std::shared_ptrDiscountStrategy strategy) : strategy_(std::move(strategy)) {} void setStrategy(std::shared_ptrDiscountStrategy strategy) { strategy_ std::move(strategy); } double calculate(double amount) const { if (amount 0) { throw std::invalid_argument(订单金额不能为负数); } return strategy_-applyDiscount(amount); } };C11之后有了更轻量的玩法用std::function存函数指针或者lambda。当算法足够简单时没必要建类层级直接传递可调用对象#include functional class OrderServiceModern { private: std::functiondouble(double) strategy_; public: explicit OrderServiceModern(std::functiondouble(double) strategy) : strategy_(std::move(strategy)) {} double calculate(double amount) const { return strategy_(amount); } }; // 使用示例 auto vipDiscount [](double amount) { return amount * 0.8; }; OrderServiceModern service(vipDiscount);但请记住一个区分当策略需要持有状态或资源时优先用类实现策略完全无状态且逻辑简单用std::function更合适。这两种风格并不冲突一个成熟的项目里两种都会出现。基底类加函数式回调的组合是C里策略模式的最佳实践形态。5. 高级玩法当策略模式遇到工厂模式、配置文件和Spring5.1 用简单工厂来管理策略对象的创建只看前面的示例可能你还会有一个困惑虽然是消除了if-else但客户端在创建策略对象的时候不是还得自己判断该new哪个类吗这不就是把if-else从计算逻辑挪到了客户端这个问题指向策略模式在实际业务中最常用的一种补充——把策略的选择逻辑收拢到工厂里面。public class DiscountStrategyFactory { public static DiscountStrategy getStrategy(String userType) { switch (userType) { case MEMBER: return new MemberDiscount(); case VIP: return new VipDiscount(); case SUPER_VIP: return new SuperVipDiscount(); default: return new NormalDiscount(); } } }这个工厂方法可能是全局唯一的、会修改的地方。它承担了”根据某种标识决定用哪个策略“的工作。配合策略模式后业务逻辑处的if-else确实没了但工厂类里的switch在增加新策略时依然要改——不过这个改动集中在一个点而且没有把算法逻辑和选择逻辑混在一起是完全可以接受的。如果你用Spring连工厂都可以不手写直接注入一个Map就好了Component public class DiscountStrategyMapper { private final MapString, DiscountStrategy strategyMap; public DiscountStrategyMapper(MapString, DiscountStrategy strategyMap) { this.strategyMap strategyMap; } public DiscountStrategy getStrategy(String userType) { return strategyMap.getOrDefault(userType, new NormalDiscount()); } }Spring会把所有DiscountStrategy类型的Bean注入到这个Map中key就是Bean的名字。新增策略时只需要加一个Component类其他代码完全不用动。这是策略模式在Spring项目里最舒服的落地形态没有之一。5.2 把策略的选择权交给配置在一些更复杂的场景比如对账系统、营销系统中策略的选择可能需要支持动态配置。这时候可以把策略对应的标识比如用户类型字符串和策略类名写到配置文件或者数据库里用反射去加载策略类。Java里类似这种写法// 配置文件: discount.policy.VIPcom.demo.discount.strategy.VipDiscount public class ConfigurableStrategyFactory { public DiscountStrategy create(String userType) throws Exception { Properties props new Properties(); try (InputStream in getClass().getResourceAsStream(/discount.properties)) { props.load(in); } String className props.getProperty(discount.policy. userType); return (DiscountStrategy) Class.forName(className).getDeclaredConstructor().newInstance(); } }这种方式的好处在于新策略上线甚至不需要重启应用改一下配置即可。缺点也很明显反射调用有性能损耗、报错不好查、类名写错要到运行期才能发现。我的建议是除非你的策略真的需要频繁热更新否则老老实实用前面Spring注入Map或者工厂模式就够了别为了炫技而用反射。5.3 和模板方法模式配合使用策略模式在设计上并不是孤立存在的。它经常和模板方法模式组合出现处理一类整体骨架不变个别步骤可变的场景。举个例子在一个订单金额计算链路上骨架可能是校验订单 - 计算原始总价 - 应用折扣策略 - 计算税费 - 返回最终价格其中应用折扣策略这一个步骤就可以抽象为策略接口让不同的折扣算法各自实现。这个组合的优势在于模板方法保证了流程的稳定性策略模式保证了节点的灵活性。你既不用把整个流程推倒重来也不用在流程中间塞一堆if-else每个步骤各司其职后续排查问题时能沿着流程骨架逐步定位。6. 常见问题与排查技巧实录6.1 策略类太多造成类爆炸怎么办这是策略模式被批评最多的地方每增加一种策略就增加一个类策略多了之后自动生成几十个类文件。这是一个真实存在的问题不必回避。我干过的一个项目里折扣策略有二十多个不同的类文件后来归类发现很多策略其实是同一算法的不同参数配置。比如会员8.8折和会员9折区别只有一个折扣率数字。这种场景就不应该建两个类而是提取成一个参数可配置的策略类把折扣率作为构造参数传进去。参数化策略 工厂组合能把类数量压缩得很明显。6.2 策略切换后状态残留策略模式中一个Context对象会在一段时间内持有一个策略引用。如果策略本身是有状态的比如缓存了一些内部数据在切换策略之后旧状态可能泄漏到新策略中。一个比较稳妥的做法是让策略尽量保持无状态如果确实需要维护状态就保证状态在每次调用开始时初始化或者每次创建新的策略实例来切换。有一回我在一个多线程环境下用过共享的策略对象结果两个线程各自调了setStrategy那场面极其混乱——高并发场景下千万不要让策略对象持有可变的共享字段。6.3 策略接口设计得太大还有一些项目把策略接口设计成了上帝接口塞了十几个方法实现类里一半方法是空实现。这是设计上的坏味道。策略接口应该只包含最核心的算法方法如果这些算法需要通过不同方法组织起来可以考虑把策略模式升级为模板方法模式或者把策略拆成多个小接口。6.4 调试技巧让策略对象可观测在实际开发里一个很实用的小技巧是——在Context中打印、记录当前使用的策略类名。这样线上排查问题时日志里直接能看到当前执行的是哪个策略而不用对着代码去猜。我在OrderService里加一行日志就是因为之前线上出了严重的价格计算异常排查时走了很多弯路。加了策略名的日志之后问题定位速度快了三倍。7. 写在最后的经验为什么策略模式值得你花一晚上搞明白我自己的体会是设计模式这个东西看书和上手完全两码事。策略模式看概念十分钟就能懂但真正理解它妙在哪里是要在维护过一个充满if-else的老项目之后才有的。拿我参与过的电商系统的例子来说最早那版折扣代码就是两百行的if-else后来每次上线新活动都要在代码里小心翼翼找一个合适的位置插入新的分支生怕影响了之前的规则。用策略模式重构后核心计算逻辑只剩十行左右每次加新活动只是新增一个类的事。这种由内而外的轻松感只有经历过的人才知道有多值得。如果你现在还在学校准备设计模式的期末考试或者正在写设计模式的大作业我建议你别粘贴复制我上面的代码而是自己动手写一个和折扣完全无关的例子——比如文件导出策略PDF导出一套、Excel导出一套、CSV导出一套它们共用一个导出接口用一个Context切换格式。自己从零写一遍比读十遍别人的例子都有用。最后分享一个我个人的小习惯。刚学设计模式那会儿总觉得要背熟所有的模式才行。后来做了几年开发慢慢发现真正常用的也就是策略模式、工厂模式、模板方法模式、观察者模式那么几个。策略模式是我最推荐的入门模式因为它很直观——把一段不断变化的逻辑从固定逻辑里抽出来用一个接口隔离然后让它们可以互相替换这个思想不仅仅在编程里用得上日常做事也一样把做什么和怎么做分开是最底层的一种抽象能力。刚开始可能会写出一堆笨拙的类没关系多踩几次坑你就能找到那个最舒服的边界了。