news 2026/10/1 23:38:39

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
策略模式实战:从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 里的策略都不用自己 new:

DiscountStrategy 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 的 key:

@Component("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 Map<String, DiscountStrategy> strategyMap; @Autowired public DiscountStrategyDispatcher(Map<String, 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 的日子了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 23:37:39

JVM垃圾收集器解密:三色标记算法与CMS/G1并发标记实战

在Java这行混得越久&#xff0c;越觉得“JVM垃圾收集器”这几个字是个照妖镜。简历上写“熟悉JVM调优”的人很多&#xff0c;可真到线上被concurrent mode failure打脸&#xff0c;或者排查一个诡异的Full GC时&#xff0c;是背过面试题还是真懂原理&#xff0c;立刻就现出原形…

作者头像 李华
网站建设 2026/10/1 23:36:27

SUMO仿真第三阶段实战:从能跑到跑得对的核心配置与排错

前几天有个做交通咨询的朋友来问我&#xff1a;SUMO 仿真环境搭到第三步&#xff0c;为什么总觉得"跑是跑起来了&#xff0c;但结果不对劲"——窗口里车在动&#xff0c;输出文件也生成了&#xff0c;可一旦有人追问"这个交叉口的平均延误到底是怎么算出来的&qu…

作者头像 李华
网站建设 2026/10/1 23:35:09

从Wine到FEX-Emu与DXMT:iOS上运行x86-64 Windows程序的跨架构兼容链路拆解

1. 从“Madeira”这个名字说起&#xff1a;一个跨平台兼容层的真实需求第一次看到“Madeira”这个标题&#xff0c;加上关键词里那一串 Wine、FEX-Emu、DXMT、iOS、x86-64&#xff0c;我脑子里第一反应是&#xff1a;这又是一个在“让不同架构、不同系统的程序互相跑起来”这件…

作者头像 李华
网站建设 2026/10/1 23:34:25

WorkBuddy+腾讯云Lighthouse轻量AI部署实战指南

1. 这不是广告&#xff0c;是实打实的轻量云上手指南&#xff1a;WorkBuddy 腾讯云 Lighthouse 联动实测全记录你搜“WorkBuddy”时&#xff0c;页面里十有八九蹦出的是“怎么装”“国际版打不开”“缓存目录改不了”“技能不生效”&#xff0c;再往下翻&#xff0c;突然冒出来…

作者头像 李华
网站建设 2026/10/1 23:33:09

GraalVM实战指南:从Windows安装到Spring Boot原生镜像打包

1. 为什么突然都在聊GraalVM先说个现象&#xff1a;最近后台留言和社群里被问得最多的几个问题&#xff0c;一个是“怎么把Java项目打成exe”&#xff0c;另一个是“Spring Boot能不能换掉默认的打包方式”。这俩问题指向同一个答案——GraalVM。GraalVM不是新鲜东西&#xff0…

作者头像 李华
网站建设 2026/10/1 23:32:52

马德拉岛深度徒步指南:从Levada水渠到丰沙尔旅行的完整攻略

从里斯本飞马德拉的航班降落时&#xff0c;我盯着窗外那条贴着悬崖伸进大西洋的跑道&#xff0c;心里反复只有一个念头&#xff1a;这飞机到底是怎么刹住的。机舱里没人说话&#xff0c;前排的葡萄牙大叔在胸前画了个十字。飞机停稳后&#xff0c;他转头冲我笑了一下——那个笑…

作者头像 李华