新闻详情

新闻详情

首页 / 资讯中心 / 详情

策略模式实战:告别if-else,用接口+工厂+Spring优雅实现算法扩展

发布时间:2026/10/2 19:51:11来源:尧图网络
策略模式实战:告别if-else,用接口+工厂+Spring优雅实现算法扩展
1. 策略模式到底解决什么问题1.1 从一段不断膨胀的if-else说起几乎每个后端程序员都经历过这种时刻一个计价方法、一个订单处理流程或者一个消息推送逻辑本来只有一两个分支后来业务方不断提需求代码就变成了下面这样。以最常见的电商折扣计算为例public BigDecimal calculateFinalPrice(Order order) { if (VIP.equals(order.getUserType())) { return order.getAmount().multiply(new BigDecimal(0.85)); } else if (SVIP.equals(order.getUserType())) { return order.getAmount().multiply(new BigDecimal(0.75)); } else if (NEW_USER.equals(order.getUserType())) { return order.getAmount().multiply(new BigDecimal(0.9)); } else if (DISABLED.equals(order.getUserType())) { return order.getAmount().multiply(new BigDecimal(0.95)); } else { return order.getAmount(); } }这段代码初看没毛病逻辑清晰、可读性尚可。但业务不可能停在四五个分支过几天加了“店庆折扣”再隔两周加了“会员日叠加折扣”“内购折扣”到时候这栋if-else大楼会越盖越高最终长成下面这样判断条件嵌套着判断条件每个分支里再塞一段几十行的业务逻辑。后续维护的人想看清楚全貌都要来回滚动好几屏改一个分支怕影响另一个分支测试用例补了一堆还是心里没底。这种代码最要命的地方不在于“长”而在于每一次新增规则都要去改动一个已经稳定运行的公共方法。今天改折扣明天影响支付明天加渠道后天影响报表。这就是我们常说的“违反开闭原则”——对扩展不友好对修改太开放。而策略模式就是专门用来收拾这种局面的。1.2 策略模式的三个角色和核心思路策略模式的思路用一句话概括定义一组算法把每个算法封装到独立的类里让它们可以互相替换。它把一个大流程里的“可变部分”抽出来变成一个个独立策略对象原来的大流程只负责持有策略并调用不关心具体怎么算。具体由三个部分组成策略接口Strategy定义算法家族的统一调用入口。比如上面折扣计算可以定义一个calculate(Order order)方法。具体策略ConcreteStrategy每种算法一个类。VIP折扣一个类、SVIP折扣一个类、新用户折扣一个类。上下文Context持有一个策略引用把客户端请求转发给策略执行。上下文本身不实现任何具体算法它更像一个“中间人”。打个比方策略模式就像给机器换插头。主机上下文只认“插头规格”你插两脚的还是三角的都能通电具体什么电压转换那是每个插头自己负责的事。而if-else那种写法相当于直接在主机里焊死一套电路换个电器就要拆开重新焊。这种设计带来的好处非常直接新增算法不需要改动已有代码。再增加一个折扣类型只需要新增一个策略类原有代码一行都不用改。每个策略可以独立测试。想验证VIP折扣逻辑直接拿VIP策略类单测不需要整个流程跑通。运行时可以动态替换。只要在调用前把上下文里的策略引用换掉下一次计算就走新逻辑非常适合配置化、规则化的场景。1.3 别滥用策略模式策略模式虽好但也不是万能的。我得泼一盆冷水如果你系统里这个分支只有两三种情况而且基本不会增加用策略模式反而增加复杂度。原本一个if就完事现在要写一个接口、两三个实现类、一个上下文类数量翻了几倍新手来看代码还要绕半天才能找到真正的算法在哪。还有一个常见的误区是“硬套策略模式”。比如某个策略类和另一个策略类的差别仅仅是数值参数不同。这时候把它们拆成两个类纯属自找麻烦不如一个类做成参数化。我在下文“策略类爆炸”那条坑里会专门展开。那么到底什么时候值得用我的判断标准有两个这个分支在未来大概率会持续增加或者产品明确说了接下来半年要上多个版本活动规则。这些分支里的算法比较复杂不是一行公式就能算完的而是各有一段相对完整的业务处理流程。如果你看到的是那种一年动不了几次的“恒定分支”老老实实写if-else反而更实在。设计模式的初衷是应对变化不是为了写代码写得有仪式感。2. 一个完整的最小实现从接口到上下文2.1 定义策略接口与具体策略类先看接口怎么定义。接着上面折扣计算的场景我定义一个计价用的策略接口public interface DiscountStrategy { BigDecimal calculate(Order order); }接口就一个方法输入完整订单信息返回折后金额。为什么要传整个订单而不是传金额因为真实的策略往往需要看多个字段用户等级、订单金额、商品品类、下单时间。如果接口只传金额后面想加规则就得改接口所有实现类全部跟着改那接口就变成瓶颈了。来两个具体策略public class VipDiscountStrategy implements DiscountStrategy { private static final BigDecimal DISCOUNT_RATE new BigDecimal(0.85); Override public BigDecimal calculate(Order order) { return order.getAmount().multiply(DISCOUNT_RATE); } } public class NewUserDiscountStrategy implements DiscountStrategy { private static final BigDecimal DISCOUNT_RATE new BigDecimal(0.90); Override public BigDecimal calculate(Order order) { return order.getAmount().multiply(DISCOUNT_RATE); } }有个细节值得注意我把折扣率定义为static final常量。这是因为这些策略类本身是无状态的——内部没有任何可变字段谁调用都只产出结果不记录中间状态。无状态的类天然线程安全完全可以做成全局共享的实例连new都省了。这一点在后面的工程化实践里会再提到。2.2 上下文类客户端只接触的门面有了策略实现还需要一个上下文。我用一个PriceCalculator来充当这个角色public class PriceCalculator { private DiscountStrategy strategy; public void setStrategy(DiscountStrategy strategy) { this.strategy strategy; } public BigDecimal calculate(Order order) { if (strategy null) { throw new IllegalStateException(策略未设置); } return strategy.calculate(order); } }上下文类的职责非常纯粹持有当前策略把计算请求转发出去。它不知道也不关心当前用的是哪个具体策略。客户端调用时先决定用什么策略然后塞给上下文再触发计算PriceCalculator calculator new PriceCalculator(); calculator.setStrategy(new VipDiscountStrategy()); BigDecimal price calculator.calculate(order);这样的好处是客户端和具体算法之间隔了一层上下文客户端手里始终拿的是PriceCalculator而不是直接依赖VipDiscountStrategy这种具体类。将来如果上下文的流程里需要在计算前后加日志、加埋点、加事务都只改上下文一处不用动任何策略类。2.3 用Map替代if-else进行策略路由上面的例子实现了策略模式但你会发现客户端仍然要自己写new哪个策略。如果客户端那边又冒出了一长串switch来判断用户类型那我们只是把if-else搬了个家复杂度依然还在客户端。工程上解决这个问题最朴素也最好用的办法就是用Map做策略注册表MapString, DiscountStrategy strategyMap new HashMap(); strategyMap.put(VIP, new VipDiscountStrategy()); strategyMap.put(NEW_USER, new NewUserDiscountStrategy()); strategyMap.put(SVIP, new SVipDiscountStrategy()); // 使用时 DiscountStrategy strategy strategyMap.get(order.getUserType()); if (strategy ! null) { return strategy.calculate(order); } return order.getAmount(); // 默认无折扣这样一来原来if-else一层层比较逻辑变成了“查Map拿实例”的统一动作。新增一种折扣类型就是往Map里多放一条put连方法体都不用进。查找的时间复杂度是O(1)效率也比一串if判断高。这其实就是策略模式强大的地方它把“算法的扩展”从修改代码变成添加代码配合Map等注册机制客户端不需要感知策略具体类只需要知道一个类型编码。类似思路可以套用在渠道商选择、支付路由、消息推送渠道、审批流节点等各种各样的业务场景。3. 工程实战策略模式和工厂、Spring的组合玩法3.1 策略工厂把创建策略的职责收拢起来实际项目里Map常驻内存的注册表往往直接放在一个工厂类里。这个工厂负责两件事接收类型编码返回对应策略实例。它的存在让策略的“查找逻辑”从客户端代码中剥离客户端只需要记住一个最大概的接口名称public class DiscountStrategyFactory { private static final MapString, DiscountStrategy STRATEGIES new HashMap(); static { STRATEGIES.put(VIP, new VipDiscountStrategy()); STRATEGIES.put(SVIP, new SVipDiscountStrategy()); STRATEGIES.put(NEW_USER, new NewUserDiscountStrategy()); STRATEGIES.put(DISABLED, new DisabledDiscountStrategy()); } public static DiscountStrategy getStrategy(String userType) { return STRATEGIES.get(userType); } }然后客户端就变成了DiscountStrategy strategy DiscountStrategyFactory.getStrategy(order.getUserType()); if (strategy ! null) { return strategy.calculate(order); } return order.getAmount();这时候你再回头看最初的if-else代码行数少了职责也清晰了。而且关键的一点以后加策略只需要改动工厂的静态初始化块以及新增类客户端、上下文、其他策略类全部不用动。工厂里也可以加一个保底逻辑如果查到null就返回一个“默认无折扣”策略而不是把null抛给调用方。这样做的好处是业务方不用到处判空兜底行为统一由工厂负责错误概率也低很多。3.2 有状态策略的线程安全问题我在前面提过常见策略都可以做成无状态、全局共享实例。但你必须清楚一个前提如果某个策略内部定义了可变的成员变量比如把某次计算的临时结果存在字段里那在多线程并发访问同一个共享实例时就会互相串数据引发诡异的问题。举一个错误示范Service public class VipDiscountStrategy implements DiscountStrategy { private BigDecimal currentAmount; // 危险成员变量被多个线程共享 Override public BigDecimal calculate(Order order) { this.currentAmount order.getAmount().multiply(...); return currentAmount; } }如果两个请求同时进来A请求刚写入currentAmountB请求立刻覆盖A后续再读时拿到B的数据。这种Bug极难复现因为你得靠运气才能抓到线程切换的时机。正确的做法也很简单策略类里只定义不变量所有临时计算结果都在方法内部作为局部变量。局部变量存活于线程栈天然隔离。如果实在需要携带请求上下文那就先new一个新策略实例再调用或者把临时数据封装在一个传入传出的上下文对象里。3.3 Spring项目里的最优雅实现如果项目用了Spring那连手写工厂的静态Map都不用直接利用Spring的依赖注入把策略接口的所有实现类收集成一个Map按Bean名字或者自定义注解关联业务编码。这里有两种常见做法做法一用BeanName做keyService public class DiscountService { private final MapString, DiscountStrategy strategyMap; // Spring会把这个接口的所有实现类注入进来 // key 默认是 Bean 的首字母小写所以 get(vipDiscountStrategy) public DiscountService(MapString, DiscountStrategy strategyMap) { this.strategyMap strategyMap; } public BigDecimal calculate(Order order) { String key strategyKey(order.getUserType()); DiscountStrategy strategy strategyMap.get(key); if (strategy null) { throw new IllegalArgumentException(不支持的策略类型); } return strategy.calculate(order); } }做法二用自定义注解做key定义注解Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) public interface StrategyType { String value(); }策略类上标注StrategyType(VIP) Service public class VipDiscountStrategy implements DiscountStrategy { ... }Service里注入所有策略自己组装Mappublic DiscountService(ListDiscountStrategy strategies) { this.strategyMap strategies.stream() .collect(Collectors.toMap( s - s.getClass().getAnnotation(StrategyType.class).value(), Function.identity() )); }用注解的好处在于key是显式的不再依赖类名业务编码和策略类的对应关系一眼就能看懂。新增策略时只需要写新类、加注解其他代码全部不用改。这种“自动发现 按编码路由”的组合是我在业务项目里最推荐的一种落地方案。4. 常见问题与实操心法4.1 坑一策略接口的方法参数不够用很多初学者把策略接口的方法参数定得太“死”比如只传一个BigDecimal amount。结果做第二个策略时发现还需要用户等级、商品品类等字段只能改接口连带着所有实现类一起改。改一次两次还能接受改得多了你就会发现接口形同虚设。更好的做法是考虑用一个上下文对象来传递所有可能用到的输入像我的例子里直接传整个Order。如果业务结构复杂还可以单独定义一个DiscountContextpublic class DiscountContext { private User user; private Order order; private ListCartItem items; // 各种业务输入 }策略方法签名统一是calculate(DiscountContext ctx)具体某个策略需要哪些字段自己从ctx里取。这样即使以后增加新输入也不用改策略接口只需要在上下文对象里加字段。代价是策略类之间复用上下文时会有一定程度的字段冗余——但比起改所有策略类签名这点冗余完全可接受。4.2 坑二Map查不到策略时直接返回null有些团队在工厂里很随意地return STRATEGIES.get(type)客户端判断if (strategy ! null)后走默认逻辑。这其实是把“null处理”的责任扔给了调用方调用方一旦漏判线上就会出现空指针异常。我的建议是工厂里内置一个兜底策略public class DefaultDiscountStrategy implements DiscountStrategy { Override public BigDecimal calculate(Order order) { return order.getAmount(); // 不打折 } } // 工厂中 public static DiscountStrategy getStrategy(String userType) { return STRATEGIES.getOrDefault(userType, new DefaultDiscountStrategy()); }这样下游永远不会拿到null也不会因为某个未知渠道突然打过来而直接报错。所谓兜底策略本质上是把“未知情况的处理策略”也显式建模了非常符合策略模式“一切算法都可封装”的思想。4.3 坑三策略类爆炸和过度拆分策略模式如果用得过头会带来另一种坏味道每个策略一行算法却拆出十几个类。这么搞代码确实“符合模式”了但可维护性没有提升反而让阅读代码的人需要打开一堆文件才能拼凑出全貌。我的经验是判断两个策略是否应该合并看它们的差异点是什么。如果差异只是数值参数不同合并成一个类内部放参数即可。比如VIP 9折、SVIP 8折、企业客户7折完全可以做成同一个“等级折扣策略”构造时传入折扣率public class LevelDiscountStrategy implements DiscountStrategy { private final BigDecimal rate; public LevelDiscountStrategy(BigDecimal rate) { this.rate rate; } Override public BigDecimal calculate(Order order) { return order.getAmount().multiply(rate); } }但注意这里的“合并”指的是折扣这一类算法完全相同的场景。如果各个策略的处理流程本身差异很大比如一个是“按比例打折”另一个是“满减后叠加免邮”那就不要强行合并硬合并的结果是策略类里塞满if退回到了起点。4.4 常见症状排查速查表症状可能原因排查顺序调用策略后结果和预期不符策略Map里的key映射错了先确认策略类上的注解值或BeanName再确认传入的业务编码偶发数据串味策略类内部有可变成员变量检查策略类所有字段临时数据必须局部变量化新增策略不生效Spring没扫到新类或Map没更新先看类有没有加 Service再看策略名是否重复客户端抛空指针工厂直接返回null改用 getOrDefault 默认策略策略类数量爆炸参数化策略被拆成多个类合并逻辑相同、仅参数不同的策略为带参数的单类4.5 判断是否值得改造的实用建议最后分享一个我自己的判断公式不是教条是长期看代码后的直觉。面对一个系统要不要引入策略模式我会先问三个问题这个分支以后会不会继续增加产品说“现在就这样了以后不好说”那多半会加。每一个分支的算法是不是都在50行以上如果每个分支简单到只有一行返回那重构带来的收益抵不上类数量增加的成本。团队里其他人能不能一眼看懂这个Map的注册方式模式是给团队用的不是展示给代码检查工具看的。如果团队普遍不熟悉策略模式引入前最好先做一次内部分享不然下一个接手的人会觉得你在炫技。按我的经验最值得改造的典型场景是订单计价、优惠券计算、支付渠道路由、消息推送渠道选择、审批流节点处理、数据导出格式转换、异常告警级别判定。这类场景共性很强规则多、变化频繁、而且往往要求后续扩展不影响核心链路。我去年接手过一个老系统里面一个订单计价方法有将近五百行嵌套了十几层if-else各种活动规则交叉作用。当时带着两个人把计价流程按“基础价计算、活动优惠、优惠券叠加、运费计算”四层拆分每一层内部再用策略模式整理整个过程大概花了两周。改完之后同一段逻辑的测试用例从原来很难构造变成了每个策略类独立构造、独立验证。上线后新增了三次营销活动都是只加类、注册一条Map核心流程再也没碰过。那次彻底让我确信真正好用的策略模式不是写出来的是应对变化逼出来的。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

GPT-5.6 Sol Ultra 模式跑一周:4 个 Agent 并行实测与 TaoToken 统一 Key 接入 2026/10/2 20:38:29

GPT-5.6 Sol Ultra 模式跑一周:4 个 Agent 并行实测与 TaoToken 统一 Key 接入

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
AI Coding 零基础实战教程|第五部分:完整项目案例实操:用 TaoToken 统一 Key 跑通 Next.js + TypeScript + Prisma 全流程 2026/10/2 20:38:29

AI Coding 零基础实战教程|第五部分:完整项目案例实操:用 TaoToken 统一 Key 跑通 Next.js + TypeScript + Prisma 全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
真机无线测试实战:TaoToken 统一 Key 打通 Android APK 局域网调试链路 2026/10/2 20:38:29

真机无线测试实战:TaoToken 统一 Key 打通 Android APK 局域网调试链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Delphi中Chrome Chromium、Cef3学习笔记(五):把Cef3的缓存与Cookie路径改到TaoToken统一通道 2026/10/2 20:38:29

Delphi中Chrome Chromium、Cef3学习笔记(五):把Cef3的缓存与Cookie路径改到TaoToken统一通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
六类高频陷阱与规避方案:单元测试如何从“测实现”到“测行为” 2026/10/2 20:38:22

六类高频陷阱与规避方案:单元测试如何从“测实现”到“测行为”

说句实话,在一线写代码这么多年,我见过太多把单元测试当成绩效考核应付的项目了——测试覆盖率报表全线飘绿,一上线照样出故障;随便重构一个方法,测试文件立刻红成一片;到最后团队受不了,干脆把…

阅读更多 →
【LeetCode Hot100】199.二叉树的右视图和56.合并区间 2026/10/2 20:38:16

【LeetCode Hot100】199.二叉树的右视图和56.合并区间

【LeetCode Hot100】199.二叉树的右视图和56.合并区间 摘要 这篇文章用来记录我在练习 hot100 中题号199和题号56的做题过程。 199. 二叉树的右视图 先来看199题——二叉树的右视图。题目见下图:第一次思路 我第一次的做题思路是既然我们是要右视图,那么…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉