策略模式实战:从if-else重构到Spring优雅落地

发布时间:2026/10/1 23:38:41
策略模式实战:从if-else重构到Spring优雅落地 先问你一个问题一个上线三个月、迭代了十几版的会员系统核心结算方法里挤满了 if-else加一个新会员等级得小心翼翼地上线你怕不怕我不只遇到过还曾亲自把这种代码交上去后来被测试同学一记“新增等级后旧会员折扣全错”的 Bug 拍在脸上。也就是从那时候开始设计模式里的策略模式Strategy Pattern被我当成后端代码里最重要的行为型模式之一。这篇东西我就围绕它展开从它到底解决什么问题、三个核心角色怎么理解到 Java 手写实现、和简单工厂模式怎么区分搭配最后再分享一些 Spring 项目里的落地姿势和踩坑记录。不管你是正在补设计模式期末作业还是被公司里乱七八糟的分支逻辑折磨得想重写项目这篇都能给你一套可以直接抄作业的思路。1. 策略模式到底是在解决什么问题1.1 几乎所有项目都会遇到的价格计算场景举个最常见的例子电商系统里做订单折扣需求刚上线时特别简单普通会员不打折黄金会员打 95 折钻石会员打 8 折。产品经理说得很轻松但也暗示了之后会不断加新规则。于是第一版核心代码只需要一个方法返回最终价格就行。当所有人都在庆祝上线的时候没人意识到这个“以后加规则很轻松”的话会成为三个月后全体开发人员的噩梦。最直接的实现是什么if-else 或者 switch。这个方法体初期看起来干净利落public double finalPrice(String userType, double amount) { if (NORMAL.equals(userType)) { return amount; } if (GOLD.equals(userType)) { return amount * 0.95; } if (DIAMOND.equals(userType)) { return amount * 0.8; } throw new IllegalArgumentException(未知会员类型); }这种写法真的有问题吗如果“会员类型只有三种且永远不变”那它完全没问题反而还简单直观。但真实业务不是这样的。运营会告诉你“下个月上超级会员打 7 折老会员要参与满减活动”最终还是会在同一个方法里叠加 an extraelse if。再过两个迭代这个方法就开始膨胀折扣、满减、运费、优惠券……几百行的 if-else 堆在一起没有一个人敢动它仿佛这代码是自己长出来的谁碰谁出事。这种情况就是典型的“面向分支编程”代码越写越累。1.2 分支代码的“坏味道”到底出在哪我看过不少新手写到这里就开始背概念说 if-else 拆开就是策略模式。这种理解方向是对的但更重要的是你得明白 if-else 到底哪里让人难受否则你就算套上策略模式的壳写出来的代码照样是一坨。分支代码的坏味道我总结成四点违背开闭原则。每来一个新规则必须打开既有核心方法去改改一个测试良好的方法稍不留神就把旧逻辑改挂了。分支和方法职责严重耦合。本来的核心逻辑是“算价格”结果里面塞满了类型判断和不同规则逻辑越混越多。不好测试。测试同学想测“钻石会员打 8 折”必须把所有分支都跑一遍场景构造不同条件的组合测试成本极高。代码可读性随分支数量指数下降。超过三层嵌套的 if-else读代码的人基本要靠猜。所以策略模式干的事说穿了也很朴素把“每一套算法或规则”从大方法里抽出来封装成独立的对象让它们能互相替换并且由调用方在运行时决定用哪一套。算法本身不再和固定的 if-else 写死在一起。2. 策略模式的核心思想与三个角色2.1 一句话理解策略模式策略模式的英文是 Strategy在 GoF 的《设计模式》里它属于行为型模式。官方定义说得很文绉绉定义一系列算法把它们一个个封装起来并且使它们可以相互替换。这定义本身没错但太抽象我换个生活化的例子。假设你每天要上班目的地是公司但具体怎么去有多种方案坐地铁、搭公交、打车、骑共享单车。这几种方式都是“到达公司”这个流程里的不同算法它们做的事情一样但实现完全不同。对你来说今天下雨就打车早高峰就坐地铁完全是看情况选一种。这和策略模式的思想一模一样你调用方根据需要选择某个具体策略交通工具让策略去执行具体的到达逻辑。在代码里那辆“车”就是策略对象“到达公司”这个动作就是策略接口里的统一方法“你”就是调用方或者上下文。2.2 策略模式的三个角色策略模式通常由三个角色组成这个面试也常问我给你拆开揉碎讲角色名称核心职责之我见Strategy抽象策略定义算法统一的入口方法让外部不关心具体实现ConcreteStrategy具体策略真正干活的类每个类封装一种规则或算法Context上下文持有 Strategy 引用负责调用算法但不关心具体是哪个策略这里最容易忽略的是 Context 到底该做什么。它并不是策略的粉丝它更像是一把“枪架”真正决定用什么策略的是外部调用方。如果你在 Context 里面写死“如果是黄金会员用 A 策略如果是钻石会员用 B 策略”那 Context 就又变成了一个 if-else 收容所策略模式相当于白搭。从依赖关系的角度看上层调用方依赖的是 Strategy 接口而不是某个具体的 DiscountStrategy 类Context 依赖的也是 Strategy 接口。这就是典型的依赖倒置原则也是策略模式“让代码面向接口编程”的底层原因。3. Java 手写一个策略模式从重构到落地3.1 第一步定义策略接口理解了理论我们直接上代码。还是回到折扣计算的场景先把策略接口抽出来/** * 折扣策略接口 */ public interface DiscountStrategy { /** * 根据原始价格计算最终价格 * * param originalPrice 原始价格 * return 最终价格 */ double calculate(double originalPrice); }这个接口设计越窄越好很多新手会把各种业务字段塞进方法参数里比如传一个完整订单对象结果所有策略类都依赖订单里的十几个字段策略之间也互相耦合。接口方法设计时参数和返回值最好足够稳定具体策略内部自己再去做额外逻辑。有一点要提醒真实项目里算金额和折扣千万别用 double业务金额计算一定要用 BigDecimal否则你可能赔上公司业绩和测试妹子的头发。我在这里用 double 纯粹为了让代码看起来不啰嗦方便理解模式本身。3.2 第二步实现具体策略类接下来把每一种会员折扣规则封装成独立类/** * 普通会员不打折 */ public class NormalDiscountStrategy implements DiscountStrategy { Override public double calculate(double originalPrice) { return originalPrice; } }/** * 黄金会员95折 */ public class GoldDiscountStrategy implements DiscountStrategy { Override public double calculate(double originalPrice) { return originalPrice * 0.95; } }/** * 钻石会员8折 */ public class DiamondDiscountStrategy implements DiscountStrategy { Override public double calculate(double originalPrice) { return originalPrice * 0.8; } }每个策略类只负责一件事黄金会员的折扣逻辑变了改 GoldDiscountStrategy 就行完全不会碰到钻石会员的代码。如果你做设计模式大作业能把这一段讲清楚老师基本就明白你没有背答案。3.3 第三步创建Context上下文Context 是策略和业务之间的桥梁。在很多项目里Context 可以是一个 service也可以是某个纯 Java 类核心职责就是持有策略对象在需要时调用它的方法/** * 订单价格计算上下文 */ public class OrderContext { private DiscountStrategy strategy; public OrderContext(DiscountStrategy strategy) { this.strategy strategy; } public void setStrategy(DiscountStrategy strategy) { this.strategy strategy; } public double calculateFinalPrice(double originalPrice) { return strategy.calculate(originalPrice); } }有人会问这个 Context 好像有点多余直接在外面调用strategy.calculate(price)不就行了吗不一定不行但 Context 的意义在于屏蔽策略调用细节并给代码留出扩展空间。比如你后面需要在计算前做日志、给最终结果做四舍五入、统一校验入参都有地方放不用散落在调用方代码里。3.4 调用方使用与新增策略的体验调用方使用起来非常舒服public class Application { public static void main(String[] args) { double originalPrice 1000.0; DiscountStrategy gold new GoldDiscountStrategy(); OrderContext context new OrderContext(gold); double finalPrice context.calculateFinalPrice(originalPrice); System.out.println(黄金会员最终价格 finalPrice); // 用户可以随时切换策略 context.setStrategy(new DiamondDiscountStrategy()); System.out.println(钻石会员最终价格 context.calculateFinalPrice(originalPrice)); } }现在如果产品经理说“下个月要加一个超级会员打 7 折”你要做什么只需要新建一个SuperDiscountStrategy类实现DiscountStrategy接口然后在调用方或工厂里传入新策略就行。之前的策略类、Context 类全部不用动甚至完全不影响线上已经跑着的逻辑。这个体验和我当初改那个巨型 if-else 相比一个天上一个地下。4. 策略模式 vs 简单工厂模式别再傻傻分不清4.1 创建型与行为型的本质区别很多人在学设计模式时看到策略模式和简单工厂模式代码长得差不多一下子就懵了。其实它们解决的完全是两类问题类图相似纯属表象。简单工厂模式是“创建型模式”它把“创建哪个对象”的决策逻辑封装起来让调用方不需要知道对象是如何 new 出来的。而策略模式是“行为型模式”它把“做哪件事”的算法封装起来让算法可以互相替换。一个是解决对象怎么来的一个是解决行为怎么变的。我做个对比表格你品一下对比维度简单工厂模式策略模式类型创建型行为型关注点对象创建算法封装与替换核心角色工厂、产品抽象、具体产品策略接口、具体策略、上下文解决的问题调用方与具体实现类的耦合调用方与具体算法的耦合典型流程根据条件返回一个对象持有策略对象调用策略方法如果你在写代码时主要纠结的是“该 new 哪个类这个类怎么初始化”优先考虑简单工厂如果你纠结的是“这个流程有几种做法不同情况下执行不同算法”那就用策略模式。4.2 项目里的“工厂策略”黄金组合实际开发里两者很少单独出现经常是“简单工厂 策略模式”组合使用这几乎是后端业务系统中最高频的组合之一。策略负责定义算法并实现算法细节工厂负责根据入参决定到底返回哪个策略对象。还是拿会员说事/** * 折扣策略工厂 */ public class DiscountStrategyFactory { public static DiscountStrategy getStrategy(String memberType) { if (GOLD.equals(memberType)) { return new GoldDiscountStrategy(); } if (DIAMOND.equals(memberType)) { return new DiamondDiscountStrategy(); } // 默认普通会员 return new NormalDiscountStrategy(); } }调用方改成这样连 Context 里的策略都不用自己 newDiscountStrategy strategy DiscountStrategyFactory.getStrategy(userType); OrderContext context new OrderContext(strategy); double finalPrice context.calculateFinalPrice(amount);这套组合的好处是新增会员等级时你通常只需要新增一个具体策略类然后去工厂里注册一条对应关系核心业务逻辑完全不用动。如果你觉得工厂里的 if-else 会越加越多也可以把工厂内部替换成枚举或 Map 注册表来减分支这部分我在第 5 节里细讲。5. 让代码更优雅枚举策略与Spring环境下的落地5.1 枚举实现策略小而美看到这里你应该发现了策略模式有一个“副作用”策略类多了以后类文件数量会膨胀。如果只是几个简单规则再拆那么多类反而显得过度设计。这时候“枚举策略”是一种不错的瘦身方案。枚举里可以定义抽象方法让每个枚举项实现自己的算法代码紧凑且内聚public enum MemberDiscountEnum { NORMAL { Override public double calculate(double originalPrice) { return originalPrice; } }, GOLD { Override public double calculate(double originalPrice) { return originalPrice * 0.95; } }, DIAMOND { Override public double calculate(double originalPrice) { return originalPrice * 0.8; } }; public abstract double calculate(double originalPrice); }调用方特别省事double price MemberDiscountEnum.valueOf(GOLD).calculate(1000);它的缺点也显而易见如果某个策略逻辑有三五百行那这个枚举会膨胀到让人不想打开。枚举策略适合“策略数量少、逻辑简单、类型固定”的场景比如状态机路由、常见编码规则映射。真要每个策略都很复杂老老实实用实现类拆开。5.2 在Spring项目中用依赖注入组织策略在 Spring 生态里写策略模式比手写 new 优雅得多。最经典的写法是让 Spring 容器自动收集所有策略 Bean然后注入成一个 Map。首先给策略实现类加上组件注解并指定好 Map 的 keyComponent(normal) public class NormalDiscountStrategy implements DiscountStrategy { Override public double calculate(double originalPrice) { return originalPrice; } }Component(gold) public class GoldDiscountStrategy implements DiscountStrategy { Override public double calculate(double originalPrice) { return originalPrice * 0.95; } }Component(diamond) public class DiamondDiscountStrategy implements DiscountStrategy { Override public double calculate(double originalPrice) { return originalPrice * 0.8; } }然后写一个调度器Spring 启动后会自动把容器里所有DiscountStrategy类型的 Bean 注入到 Map 中key 就是注解里指定的字符串Service public class DiscountStrategyDispatcher { private final MapString, DiscountStrategy strategyMap; Autowired public DiscountStrategyDispatcher(MapString, DiscountStrategy strategyMap) { this.strategyMap strategyMap; } public double apply(String memberType, double originalPrice) { DiscountStrategy strategy strategyMap.get(memberType.toLowerCase()); if (strategy null) { strategy new NormalDiscountStrategy(); } return strategy.calculate(originalPrice); } }调用方核心逻辑就一句话double price dispatcher.apply(GOLD, 1000);这段代码最大的好处是以后新增策略类只需要加一个Component(super)的类Dispatcher 完全不用改。Spring 的 DI 能力直接帮我们把“策略注册表”建好了省去了手写 Map 注册的麻烦。我项目里做消息推送渠道、第三方支付、优惠券玩法都是这么玩的。唯一要注意的是Map 的 key 必须全局唯一别不小心定义了两个Component(gold)否则启动时会直接报冲突。6. 常见问题与排查技巧实录6.1 策略类多了管理成本变高怎么办策略本身也有代价一个规则一个类时间久了策略类会非常多光看着包名都能得密集恐惧症。我常说这是“幸福的烦恼”——至少比巨型 if-else 容易改但管理成本确实存在。我的做法是先分业务包比如promotion.discount、promotion.freight、promotion.coupon策略按业务域归位其次简单固定的规则用枚举策略只有复杂策略才单独拆类再次用函数式接口做小型策略。比如 Java 8 之后如果策略只是比如“乘以 0.8”这样一句话就不必非得写一个类可以用 Lambda 或者方法引用作为策略传进去DiscountStrategy diamond price - price * 0.8;这种方式在局部业务里极度轻量但在大型项目里可读性不如具名类得看团队习惯了。6.2 上下文和策略职责容易出现错位最常见的错误写法是在 Context 里又写 if-else 判断用户类型然后再决定调谁// 这是个反面例子千万不要学 public double calculateFinalPrice(String userType, double originalPrice) { if (GOLD.equals(userType)) { return new GoldDiscountStrategy().calculate(originalPrice); } // 分分钟变成老古董 }这样做的结果是把“策略选择逻辑”又塞回了上下文。Context 的正确姿势应该是“只调不选”它接收一个已经创建好的策略对象然后执行。真正决定用哪个策略的人应该是调用方或者一个负责路由的工厂/分发器。6.3 策略接口设计太宽参数越传越多经验不足的开发很容易把策略接口设计成一个大杂烩比如一开始用calculate(Order order)后来发现有的策略需要会员信息有的需要优惠券于是又改成calculate(Order order, Member member, Coupon coupon)再将来加一个参数就崩了。我的经验是策略接口的参数尽量稳定且抽象甚至可以定义一个DiscountContext参数对象里面放所有策略可能要用的数据。策略之间按需取值而不是各自声明不同的参数列表。6.4 忘了处理空策略NPE直接送上门用 Map 或工厂获取策略时如果传入类型不在注册表里获取的结果就是 null直接调用calculate必然空指针。这类问题特别隐蔽通常只有脏数据才能触发。我在项目里统一采用“默认策略兜底”也就是找不到时给一个不做任何处理的默认实现比如普通会员策略。下面是这段时间我整理的策略模式常见问题速查表文字描述不直观表格更适合让团队小伙伴放在文档里当排查手册用现象可能原因排查思路与解决调用策略时空指针策略 Map 或工厂没有对应类型打印策略 Map 的 keySet检查注册名是否写错入参统一做默认策略兜底新增策略后没生效策略类没有加Component或在包扫描路径外检查类注解与包扫描配置启动日志里确认 Bean 是否注册Spring 启动时 Bean 冲突两个策略类的 Map key 相同检查Component注解的 value 是否唯一策略类直接互相调用业务混乱策略拆分粒度不合理把公共逻辑下沉到抽象类或独立 Service策略类之间不要互相依赖上下文里出现大量 if-elseContext 负担了策略选择逻辑将策略选择职责交给工厂/分发器Context 只做策略调用策略对象是单例但内部维护了可变状态Spring 默认单例策略类被多线程共享策略类设计成无状态不要把中间计算结果放在成员变量里这份速查表不光是理论很多是我自己踩过的坑尤其“策略类内部搞出状态”这条真出过线上事故。当时有个优惠策略把计算结果暂存到成员变量导致并发请求互相覆盖排查到凌晨才定位。最后顺手聊两句我的真实体会策略模式学起来不难难的是“什么时候该用”。它适合处理那些“规则在持续增加、算法会经常变化”的场景但如果你面对的是一段稳定且简单的小逻辑别硬套策略模式否则类爆炸比 if-else 爆炸还烦人。我个人的判断标准很简单当代码里出现第二个相似的分支时就开始考虑怎么把分支抽象成策略当上下文的 if-else 超过三四个时就必须动手重构。写代码不是去跟谁比拼用了多少设计模式而是让接你代码的人少掉点头发。策略模式是我用过之后回购率最高的模式之一只要你在真实项目里体会过“加新功能不用改旧代码”的爽感就很难再回到那个堆 if-else 的日子了。