资讯动态

Java策略模式实战:告别if-else分支爆炸,轻松实现可扩展设计

发布时间:2026/10/9 16:11:35 来源:尧图企业网站定制
先问自己一个很实际的问题你的项目里是不是有大量if-else或者switch-case每加一种新玩法就要改一次老代码改完还要担心把原来跑得好好的逻辑弄崩如果是那你今天就有救了。这个 Java 项目中策略模式的使用方法正好就是用来收拾这种烂摊子的。策略模式是 Java 设计模式里最容易上手、又最能直接降低项目维护成本的一个它的核心思想简单到有点“朴实无华”把一堆容易变动的算法或业务规则各自封装成一个独立的类再把选择权交给调用方。这样做的好处非常直观——新增一种策略时你只需要新加一个类不需要去动已经写好的逻辑符合开闭原则也让代码变得像搭积木一样清爽。这篇内容我会用最朴素的语言从“为什么要用它”开始再带你写一个最简单的策略模式然后到实战改造一个订单计价场景最后讲讲和 Spring 容器结合的高级玩法以及我实际踩过的几个坑。不管你是刚学 Java 的小白还是写了几年业务代码但没系统整理过设计模式的老手这篇都能直接照着抄作业。1. 什么时候该用策略模式1.1 它到底解决了什么问题很多人一听到“设计模式”四个字就头皮发麻觉得是理论课上的东西和写业务没关系。实际上策略模式的出现就是为了解决代码里最常见的“坏味道”——分支逻辑膨胀。举个例子。你做电商系统的促销模块订单结算时要根据用户类型算折扣。普通用户不打折VIP 用户打 9 折超级会员打 8 折。第一版代码谁都写得出来一个方法就搞定public double calculateDiscount(User user, double amount) { if (NORMAL.equals(user.getLevel())) { return amount; } else if (VIP.equals(user.getLevel())) { return amount * 0.9; } else if (SUPER_VIP.equals(user.getLevel())) { return amount * 0.8; } return amount; }看起来简单问题也不大。但业务永远在变没过多久产品经理说加一个“企业客户折上折”你说行加一个else if又过两周说加“黑卡会员”又加一个else if。三个月后这个方法的判断分支给你写到七八层甚至十几层中间还夹杂着取缓存、查数据库、加日志的代码任何一个分支出错你都得从头到尾捋一遍改的时候手都是抖的。这时候策略模式就派上用场了。它的思路是你要变的东西就不要让它散落在各处把每一种计算折扣的规则都独立成一个类大家共用同一个接口。这样一来新增一种规则就是新增一个类删除一种规则就是删掉一个类压根影响不到其他人。1.2 识别“该上策略模式”的三个信号我做了这么多年项目总结下来不是所有 if-else 都非要改成策略模式。过度设计比不设计还难受。但出现下面这三个信号时就应该认真考虑重构了信号一分支数量超过四个。我个人的经验是四到五个分支是临界点超过五个以后一个方法里塞太多逻辑可读性会急剧下降测试也会变得很痛苦。每个分支都要准备一套输入输出写用例都嫌烦。信号二分支判断的条件不是稳定不变的。如果业务规则基本定死了几年不动一次你用 if-else 也不会出什么大问题毕竟代码是给人看的不需要为了“优雅”牺牲直观。可现实情况往往是规则每个月都在变或者要接不同渠道、不同客户等级、不同活动策略这些高频变化才是策略模式的适用场景。信号三算法之间要相互替换。比如同样一个排序功能数据量小的时候用冒泡排序就行数据量大的时候要换成快速排序。又比如同样是文件上传本地存储策略和 OSS 存储策略之间的切换。这些场景用策略模式就是把不同实现都放进一个容器里运行时按需取用而不是在一段逻辑里反复切来切去。记住一个核心判断标准有没有“可替换的多种实现”。有——考虑策略模式没有——不要硬套。2. 策略模式的核心结构与基础实现2.1 三个角色先分清策略模式的结构非常简单就三个角色策略接口Strategy定义所有策略类必须实现的方法。它是个抽象约束规定“你既然是打折策略就必须能算出一个折扣后的价格”。具体策略类ConcreteStrategy实现策略接口的各个类每个类封装一种具体的算法或规则。比如VipDiscountStrategy、SuperVipDiscountStrategy。上下文类Context持有策略接口的引用负责调用具体的策略。它不关心具体是哪个策略在处理只负责把请求转发出去。理解这三个角色的关系我习惯用一个现实类比你去饭店吃饭点菜的时候告诉服务员要吃“酸甜口的”或者“麻辣口的”服务员把需求转达给后厨具体怎么炒是后厨里不同厨师的事。这里的“点菜需求”就是策略接口“酸甜口厨师”和“麻辣口厨师”是具体策略类服务员就是上下文类。2.2 一个开胃小 Demo先来一个最简单、不掺杂任何框架的策略模式实现把骨架搭起来。策略接口public interface DiscountStrategy { double calculate(double originalAmount); }两个具体策略类public class VipDiscountStrategy implements DiscountStrategy { Override public double calculate(double originalAmount) { return originalAmount * 0.9; } } public class SuperVipDiscountStrategy implements DiscountStrategy { Override public double calculate(double originalAmount) { return originalAmount * 0.8; } }上下文类public class DiscountContext { private DiscountStrategy strategy; public DiscountContext(DiscountStrategy strategy) { this.strategy strategy; } public double execute(double originalAmount) { return strategy.calculate(originalAmount); } public void setStrategy(DiscountStrategy strategy) { this.strategy strategy; } }测试一下public class Demo { public static void main(String[] args) { double amount 1000; DiscountContext context new DiscountContext(new VipDiscountStrategy()); System.out.println(VIP 应付 context.execute(amount)); context.setStrategy(new SuperVipDiscountStrategy()); System.out.println(超级会员应付 context.execute(amount)); } }看到没上下文类里一个 if-else 都没有。它只是持有策略接口的引用具体算法由外部注入。运行结果也非常直观VIP 付 900超级会员付 800。这就是策略模式最基础的形态。2.3 为什么这样设计就好了可能你会问这不就是把 if-else 换成了几个类吗有什么本质区别区别在“扩展方式”上。旧代码加一个新会员等级你得去打开原来的方法在那一长串 if-else 里插入一个新分支。改的是核心业务逻辑风险全都集中在一条链路里。而策略模式加一个新会员等级只需要新建一个类实现DiscountStrategy接口然后把策略对象交给上下文。原来的类不用动一点风险都没有。这就是开闭原则的体现对扩展开放对修改关闭。程序的功能要能轻松加但已有的稳定代码不要随便改。策略模式是践行这条原则最经典的例子。3. 从理论到实战改造订单计价业务3.1 先看一个真实业务场景小 Demo 毕竟是玩具咱们还是看真实项目里的场景怎么落地。假设你在做一个 B 端电商平台订单结算除了按会员等级打折还有几种额外的计费规则普通订单原价不加工。会员订单按会员等级打折。满减订单满 500 减 80满 1000 减 200。限时活动订单秒杀商品直接五折封顶不参与其它优惠。企业订单走协议价每件商品固定单价。如果没有策略模式你的结算方法大概会长这样public double settle(Order order) { double result 0; for (OrderItem item : order.getItems()) { double basePrice item.getPrice(); if (NORMAL.equals(order.getType())) { result basePrice * item.getCount(); } else if (MEMBER.equals(order.getType())) { double memberDiscount getMemberDiscount(order.getUserId()); result basePrice * item.getCount() * memberDiscount; } else if (FULL_REDUCTION.equals(order.getType())) { double subTotal basePrice * item.getCount(); result subTotal - getReduction(subTotal); } else if (SE_KILL.equals(order.getType())) { result basePrice * item.getCount() * 0.5; } else if (ENTERPRISE.equals(order.getType())) { result getContractPrice(order.getEnterpriseId(), item.getProductId()) * item.getCount(); } } return result; }这段代码的问题不用我多说方法越来越长每个分支里可能还有各自独立的数据库查询、缓存读取、异常处理。一旦某个策略出了 bug排查的时候必须盯着一个巨长的方法从上往下看。而且测试的时候你需要为这个单方法构造五种不同的订单类型耦合度非常高。3.2 第一步抽象计费策略接口对应到上面的场景我们需要定义的是订单计费策略。策略接口不要定得太细也不要定得太宽最好封装一个完整的“计算订单总价”的行为public interface OrderPricingStrategy { double calculate(Order order); }这里传入的是整个订单对象而不是单价。因为在真实场景里,满减订单要判断订单总金额企业订单要拿协议价只传一个金额根本不够。接口方法传参的设计要在“通用性”和“可读性”之间做个平衡我的习惯是优先传上下文对象也就是能把这个场景里的关键信息都带上的那个对象。3.3 第二步分别实现各个具体策略普通订单策略这是最简单的public class NormalOrderPricingStrategy implements OrderPricingStrategy { Override public double calculate(Order order) { return order.getItems().stream() .mapToDouble(item - item.getPrice() * item.getCount()) .sum(); } }会员订单策略注意这里涉及获取用户折扣率的逻辑把它收敛在策略内部避免污染外部public class MemberOrderPricingStrategy implements OrderPricingStrategy { private final MemberService memberService; public MemberOrderPricingStrategy(MemberService memberService) { this.memberService memberService; } Override public double calculate(Order order) { double discount memberService.getDiscount(order.getUserId()); return order.getItems().stream() .mapToDouble(item - item.getPrice() * item.getCount() * discount) .sum(); } }满减订单策略满减规则一般有一个专门的处理类策略里只管调用public class FullReductionOrderPricingStrategy implements OrderPricingStrategy { private final ReductionRuleEngine ruleEngine; public FullReductionOrderPricingStrategy(ReductionRuleEngine ruleEngine) { this.ruleEngine ruleEngine; } Override public double calculate(Order order) { double total order.getItems().stream() .mapToDouble(item - item.getPrice() * item.getCount()) .sum(); return total - ruleEngine.getReduction(total); } }限时活动策略和企业订单策略也同理各自实现calculate方法内部爱怎么折腾怎么折腾。我建议每个策略类保持独立、互不引用做到“高内聚低耦合”是一个策略类最理想的状态。3.4 第三步改造上下文和客户端调用有了这些具体策略类以后上下文就非常干净了public class OrderPricingContext { private OrderPricingStrategy strategy; public OrderPricingContext(OrderPricingStrategy strategy) { this.strategy strategy; } public double calculate(Order order) { return strategy.calculate(order); } }调用端怎么用呢如果维护一个“订单类型 - 策略对象”的映射关系客户端代码就能简化成两行MapString, OrderPricingStrategy strategyMap new HashMap(); strategyMap.put(NORMAL, new NormalOrderPricingStrategy()); strategyMap.put(MEMBER, new MemberOrderPricingStrategy(memberService)); strategyMap.put(FULL_REDUCTION, new FullReductionOrderPricingStrategy(reductionRuleEngine)); OrderPricingStrategy strategy strategyMap.get(order.getType()); double result new OrderPricingContext(strategy).calculate(order);以后想加一种“跨境订单策略”只需写一个新的策略类然后在strategyMap里加一行。以前改动要动结算核心逻辑现在只是在装配、注册阶段加一条记录本质完全不同。3.5 一个容易忽略的细节策略命名与分类实战中我强烈建议给策略类命名加上统一的缀比如Strategy后缀或者按业务域叫PricingStrategy、Policy、Handler都行关键是项目里保持统一。我见过一个项目有的类叫XXXStrategy有的叫XXXHandler还有叫XXXService的代码里搜策略实现类出来一大片乱得没法看。同时在包结构上我推荐建一个独立的strategy包或者policy包和业务 service 层隔开。类一多包结构整齐能省很多查找的时间。4. 进阶玩法策略模式和 Spring 容器的结合4.1 为什么要结合 Spring上面咱们写的例子还是纯 Java 的对象手动 new这在真实的 Spring Boot 项目里根本不够看。原因有两个第一策略类可能依赖MemberService、ReductionRuleEngine、OrderMapper这些 Spring 管理的 Bean手动 new 的话依赖全靠自己手动拼拼错了也不知道。第二手动把策略对象放进 Map 里的注册代码本身也会随着策略数量增加而增多仍然没有完全消除“改配置”的成本。结合 Spring 之后我们有两个能力可以利用依赖注入和容器扫描。策略类可以被 Spring 自动创建并且通过某种约定的方式自动注册到 Map 里业务代码只需要从容器里拿就行。4.2 用 Map 自动注入实现策略注册Spring 允许把一个接口的所有实现类自动收集到一个 Map 里key 是 Bean 的名字value 是策略对象。这招用好了连手动往 Map 里 put 的步骤都能省了。先给每个策略类加上Component注解Component(NORMAL) public class NormalOrderPricingStrategy implements OrderPricingStrategy { // ... } Component(MEMBER) public class MemberOrderPricingStrategy implements OrderPricingStrategy { // ... }然后在需要用到策略集合的地方直接注入 MapService public class OrderSettlementService { private final MapString, OrderPricingStrategy strategyMap; public OrderSettlementService(MapString, OrderPricingStrategy strategyMap) { this.strategyMap strategyMap; } public double settle(Order order) { OrderPricingStrategy strategy strategyMap.get(order.getType()); if (strategy null) { throw new IllegalArgumentException(不支持的订单类型: order.getType()); } return strategy.calculate(order); } }Spring 在启动的时候会自动扫描到所有OrderPricingStrategy接口的实现类并注入到strategyMap这个 Map 里key 是 Bean 的名字默认类名首字母小写这里用Component(NORMAL)指定了名字。新增一种策略只需要加一个类加一个注解其它任何代码都不用改。这就是策略模式和 Spring 容器结合后的最佳状态。需要注意一个小细节多个实现类注入到 Map 时如果 Bean 名字冲突Spring 会报错所以Component里指定的名字要保证唯一。策略类型字段如果用字符串最好定义成常量类统一管理避免魔法值散落各处。4.3 基于自定义注解的注册式策略Map 自动注入已经很好用了但它的 key 依赖 Bean 名或者手动写死有时不够直观。如果策略类型枚举是写在类里的、靠一个getType()方法声明的我更推荐用自定义注解来注册策略代码能灵活很多。定义策略注解Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) public interface PricingStrategyType { OrderType value(); }在策略实现类上标注注解PricingStrategyType(OrderType.MEMBER) Component public class MemberOrderPricingStrategy implements OrderPricingStrategy { // ... }然后在策略注册器里把 Spring 注入的 Map 转成“按枚举映射”的 MapComponent public class PricingStrategyRegistry { private final MapOrderType, OrderPricingStrategy strategyMap new EnumMap(OrderType.class); public PricingStrategyRegistry(MapString, OrderPricingStrategy strategyBeans) { strategyBeans.forEach((beanName, strategy) - { PricingStrategyType annotation strategy.getClass().getAnnotation(PricingStrategyType.class); if (annotation ! null) { strategyMap.put(annotation.value(), strategy); } }); } public OrderPricingStrategy getStrategy(OrderType orderType) { OrderPricingStrategy strategy strategyMap.get(orderType); if (strategy null) { throw new IllegalArgumentException(Unsupported order type: orderType); } return strategy; } }这里用到了EnumMap它是专门为枚举设计的 Map 实现内部用数组存储遍历效率比普通 HashMap 高并且天然按枚举顺序排列。正因为 key 是枚举不是字符串整体代码的类型安全立刻上了一个台阶——传一个不存在的字符串在编译期发现不了传一个不存在的枚举在编译期就报错了。4.4 高阶场景策略里再套策略有些时候业务规则会更复杂一点比如“满减策略”有可能还会叠加“会员折扣”这就是策略的嵌套组合。策略模式本身并不排斥这种组合你可以让策略接口的实现类里面再持有另一个策略接口的引用public class MemberAndFullReductionStrategy implements OrderPricingStrategy { private final MemberOrderPricingStrategy memberStrategy; private final FullReductionOrderPricingStrategy reductionStrategy; public MemberAndFullReductionStrategy(MemberOrderPricingStrategy memberStrategy, FullReductionOrderPricingStrategy reductionStrategy) { this.memberStrategy memberStrategy; this.reductionStrategy reductionStrategy; } Override public double calculate(Order order) { double memberPrice memberStrategy.calculate(order); Order memberOrder order.cloneWithAmount(memberPrice); return reductionStrategy.calculate(memberOrder); } }坦白说策略套策略容易写乱如果不是强需求我建议把组合逻辑放到一个独立的编排服务里不要塞进策略类内部。策略类保持职责单一组合交给更上层的代码来做这样出了问题也好排查。5. 常见问题与排查技巧实录5.1 新手最容易踩的坑坑一策略类直接依赖另一个策略类的内部数据。策略之间应该相互独立即便要共用数据也通过上下文对象传递不要直接访问对方公开的字段或 getter。否则改一个策略类可能导致另一个策略类测试挂掉耦合就产生了。坑二Map 自动注入时的策略找不到问题。我见过不少人在 Spring 项目中使用 Map 注入时漏加Component注解导致启动之后注入的 Map 是空的。排查方向很简单启动日志里看有没有对应的 Bean 创建记录或者直接写个启动时检查逻辑扫描接口实现类数量是否符合预期。坑三策略类型使用魔法字符串。项目里如果订单类型用字符串表示一不小心写错一个字符比如MEMBER写成memeber运行期就会报“找不到策略”。这类问题很让人抓狂。我的建议是要么用枚举要么把字符串常量集中到OrderType常量类里坚决杜绝魔法字符串出现在业务代码里。5.2 排查思路速查表下面这张表是我在实际项目里积累的常见问题速查遇到类似情况可以直接对号入座现象可能原因排查方法调用策略时抛出空指针策略 Map 中没有对应 key断点或日志打印 Map 的 key 集合对比订单类型Spring 启动报 Bean 名字冲突多个Component指定了同一个名字检查所有策略类的Component注解值策略执行结果不符合预期策略实现中访问了错误依赖或参数单测每个策略类隔离验证输入输出新增策略后原功能受影响策略类相互间存在隐式依赖检查各策略类是否引用了其他策略类的实例策略 Map 注入为空接口实现类未加Component或扫描路径不对确认 Spring 扫描包路径覆盖策略包5.3 我的实战心得最后分享几点我在真实项目里攒下来的经验未必写进哪本教科书但很管用。第一策略模式和单元测试是天作之合。以前用 if-else 写折扣逻辑测试一个方法要覆盖所有分支构造数据非常痛苦。拆分策略类以后每个类只需针对自己的逻辑测一遍断言结果测试代码简洁明了覆盖率还提上去了。我强烈建议在重构到策略模式的同时给每个策略类补上对应的单元测试这叫“重构和生产同步改进”。第二策略类的数量要控制。策略模式虽好但策略类超过几十个以后管理成本也会上升。建议多按业务域分模块不要让所有策略类堆在一个包里可以再按子模块分目录。如果策略类是纯无状态的逻辑处理可以考虑把策略注册逻辑抽成一个独立的配置类集中管理避免到处散落。第三不要为了设计模式而设计模式。如果一个项目里就两三处 if-else 分支而且变动的可能性极低你非要拆成五六个类反而增加了阅读负担。我见过一些团队盲目追求代码“优雅”把所有 if-else 都改成策略模式加一大堆抽象类最后新同事看代码时一脸懵维护成本比原来还高。设计模式是工具不是信仰用得合适才是真的好。我个人在实际操作中最舒服的状态是策略接口定义得干净利落具体策略类各管各的上下文和注册逻辑尽量薄。这样无论是排 bug、加功能、做测试都能精准定位不拖泥带水。刚开始用策略模式时可能觉得“多写了几个类”但等项目迭代半年、一年以后回头看你会发现省下的时间精力远超当初多写的那几行代码。如果你手头正好有一个分支逻辑爆炸的类拿它当第一个策略模式练手对象是最合适的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑