策略模式:告别 if-else,让算法自由切换
一个电商系统的支付模块往往有很多支付方式。需求很简单:支持支付宝、微信、银行卡三种支付方式。你打开ide,写下了这段代码:
publicStringpay(Stringtype,BigDecimalamount){if("alipay".equals(type)){// 调用支付宝 SDKreturn"支付宝支付成功:"+amount;}elseif("wechat".equals(type)){// 调用微信 SDKreturn"微信支付成功:"+amount;}elseif("bank".equals(type)){// 调用银行 SDKreturn"银行卡支付成功:"+amount;}else{return"不支持该支付方式";}}功能是实现了,但产品经理又提了需求:要加 Apple Pay。你改了一次代码。过两天又说要加数字人民币。你又改了一次。再过两天说要根据用户等级给不同支付方式打不同的折扣逻辑,你看着这个越长越胖的if-else,陷入了沉思。
每次新增一种支付方式,都要回来改这个方法。改多了不仅容易出错,还会和同事产生代码冲突。有没有一种方式,能让每种支付方式自己管自己,新增的时候只加代码不改老代码?策略模式可以完美解决你的问题。
目录
- 策略模式是什么
- 为什么需要策略模式
- 策略模式的结构
- 用策略模式重构上述if-else结构
- 策略模式与 if-else 的对比
- 策略模式的进阶用法
- 小结
策略模式是什么
策略模式是一种行为型设计模式,它把一组算法各自封装成独立的对象,让它们可以互相替换。
好比你去餐厅吃饭,菜单上有清蒸、红烧、水煮三种做法,你选了红烧鱼。你不需要知道红烧的具体步骤,你只需要告诉服务员"我要红烧",厨房就按照红烧的流程来做。下次你想换口味,说"改成清蒸",厨房就切换到清蒸的流程。做法(算法)是可替换的,点菜的流程(调用方)不需要变。
策略模式做的就是这件事:把每种"做法"封装成一个策略对象,调用方只需要选策略,不用关心具体怎么执行。
为什么需要策略模式
回到开头那个支付场景。用if-else写的代码有三个明显的问题:
1. 违反开闭原则。每新增一种支付方式,都要修改pay方法。开放了修改,而不是开放了扩展。
2. 职责不清晰。所有的支付逻辑堆砌在一个方法里,支付宝的 、微信的、银行的调用逻辑全混在一起,一点也不“优雅”。
3. 难以复用。如果另一个模块也需要"根据支付类型执行不同逻辑",你得把这段if-else再抄一遍,或者抽成一个更庞大的方法。
策略模式的解法是:把每个分支变成一个独立的类,通过一个统一的接口调用。新增支付方式就新增一个类,已有的代码一行也不用动。
策略模式的结构
先看整体结构,只有三个角色:
Context(上下文) │ │ 持有 ▼ Strategy(策略接口) │ ├── ConcreteStrategyA(具体策略 A) ├── ConcreteStrategyB(具体策略 B) └── ConcreteStrategyC(具体策略 C)| 角色 | 职责 |
|---|---|
| Strategy(策略接口) | 定义所有策略的共同行为规范 |
| ConcreteStrategy(具体策略) | 实现具体的算法逻辑 |
| Context(上下文) | 持有策略引用,委托策略执行 |
为什么中间还要加一个 Context?直接让调用方new AlipayStrategy().pay(amount)可不可以?
确实是可以的,但 Context 的存在有两个意义。第一,它封装了策略的切换逻辑,调用方不需要每次都手动创建策略实例,只需要context.setStrategy(...)一个方法搞定。第二,它可以持有公共状态。比如支付场景中,Context 可以保存用户的订单号、支付金额等信息,策略执行时从 Context 中获取,而不是每个策略都传一遍这些参数。
策略接口定义了"能做什么",Context 决定了"让谁来做",具体策略负责"怎么做"。三者各司其职。
用策略模式重构上述 if - else结构
第一步:定义策略接口
所有支付方式都要实现同一个接口,这是策略模式的契约:
// 支付策略接口publicinterfacePayStrategy{Stringpay(BigDecimalamount);}接口只定义一个方法pay,所有的支付方式都去实现它。调用方只需要认这个接口,不需要知道具体是哪种支付。
这里有一个设计上的取舍:接口方法越少越好。一个方法的接口意味着每个策略只需要做一件事,职责单一,理解和实现的成本都很低。如果你的接口有五六个方法,那大概率是拆得不够细,或者把不该属于策略的东西塞进来了。
第二步:实现具体策略
每种支付方式是一个独立的类,各自管各自的逻辑:
// 支付宝支付策略publicclassAlipayStrategyimplementsPayStrategy{@OverridepublicStringpay(BigDecimalamount){// 调用支付宝 SDK 的逻辑return"支付宝支付成功:"+amount;}}// 微信支付策略publicclassWechatPayStrategyimplementsPayStrategy{@OverridepublicStringpay(BigDecimalamount){// 调用微信 SDK 的逻辑return"微信支付成功:"+amount;}}// 银行卡支付策略publicclassBankPayStrategyimplementsPayStrategy{@OverridepublicStringpay(BigDecimalamount){// 调用银行 SDK 的逻辑return"银行卡支付成功:"+amount;}}每个类只关心自己的支付逻辑。支付宝的类并不知道微信的存在,微信的类也不知道银行的存在。它们之间完全解耦。
注意一个细节:每个策略类内部的实现可以完全不同。支付宝可能需要处理 RSA 签名,微信可能需要处理 HMAC 签名,银行可能需要调用银联的 SOAP 接口。这些复杂的差异被封装在各自的类里,对外只暴露一个统一的pay方法。调用方永远不需要知道"支付宝支付和微信支付到底有什么区别",它只知道:传入金额,拿到结果。
第三步:实现上下文
Context 是调用方和策略之间的桥梁。它持有策略引用,把具体的支付操作委托给策略执行:
// 支付上下文publicclassPayContext{privatePayStrategystrategy;// 设置支付策略publicvoidsetStrategy(PayStrategystrategy){this.strategy=strategy;}// 执行支付publicStringexecutePay(BigDecimalamount){if(strategy==null){thrownewRuntimeException("请先选择支付方式");}returnstrategy.pay(amount);}}setStrategy让你可以运行时切换策略,executePay委托给具体的策略执行。Context 不知道也不关心具体用的是哪个策略。
你可以把 Context 理解成一个"遥控器"。遥控器上只有一个"播放"按钮(executePay),但你插不同的光盘(策略),按播放就会播放不同的内容。遥控器本身不需要改,换光盘就行。
第四步:使用
publicclassMain{publicstaticvoidmain(String[]args){PayContextcontext=newPayContext();// 用户选择支付宝context.setStrategy(newAlipayStrategy());System.out.println(context.executePay(newBigDecimal("99.9")));// 输出: 支付宝支付成功:99.9// 用户切换成微信context.setStrategy(newWechatPayStrategy());System.out.println(context.executePay(newBigDecimal("199.0")));// 输出: 微信支付成功:199.0// 再切换成银行卡context.setStrategy(newBankPayStrategy());System.out.println(context.executePay(newBigDecimal("50.0")));// 输出: 银行卡支付成功:50.0}}调用方只需要做两件事:选策略、执行。至于每种支付方式内部怎么实现的,调用方完全不关心。
这就是策略模式最直观的好处:调用方的代码不会因为策略的增加而膨胀。不管是 3 种支付方式还是 30 种,Main里的代码结构都是一样的,只是setStrategy传入的对象不同。
现在要新增 Apple Pay,怎么做?只需要加一个类:
publicclassApplePayStrategyimplementsPayStrategy{@OverridepublicStringpay(BigDecimalamount){return"Apple Pay支付成功:"+amount;}}然后在使用的地方context.setStrategy(new ApplePayStrategy())就行了。已有代码一行没改。这就是开闭原则的体现:对扩展开放,对修改关闭。
整个实现的核心机制可以总结为三步:定义接口、封装策略、运行时切换。策略接口定义了"做什么",具体策略各自实现"怎么做",上下文负责"选谁来做"。
理解上述策略模式模板的难点在于setStrategy(PayStrategy strategy)这个方法。它的意义不只是赋值,还可以让策略可以在运行时动态切换。这是策略模式和if-else的本质区别:if-else的分支在代码写死的那一刻就确定了,而策略模式的切换发生在运行时。
策略模式与 if-else 的对比
| 对比维度 | if-else | 策略模式 |
|---|---|---|
| 新增策略 | 改已有方法,加 else if | 新建一个类,实现接口 |
| 删除策略 | 删 else if,可能影响后续逻辑 | 删掉对应的类,不影响其他 |
| 切换方式 | 编译时写死 | 运行时动态切换 |
| 职责边界 | 全堆在一个方法里 | 每个策略独立,各管各的 |
| 可测试性 | 难以单独测试某个分支 | 每个策略可以独立单元测试 |
| 代码冲突 | 改同一个方法,容易冲突 | 各写各的类,互不干扰 |
if-else 是编译时写死的逻辑分支,策略模式是运行时可替换的对象。
再举一个实际的差异。假设你的系统需要支持 A/B 测试:新用户看到新版支付流程,老用户看到旧版。用if-else,你得在pay方法里再套一层判断,逻辑越来越复杂。用策略模式,你只需要在运行时根据用户类型选择不同的策略:
if(user.isNewUser()){context.setStrategy(newNewVersionPayStrategy());}else{context.setStrategy(newOldVersionPayStrategy());}策略的选择逻辑和策略的实现逻辑是分开的,各自独立演化,互不干扰。
还有一个容易忽略的点:可测试性。用if-else写的支付逻辑,你想单独测试支付宝的分支,得构造一个type = "alipay"的条件,还得确保不会误入其他分支。而策略模式下,每个策略类就是一个独立的单元,直接new AlipayStrategy().pay(amount)就能测试,不需要任何前置条件。单元测试写起来简单,跑起来也快。
策略模式的进阶用法
用工厂封装策略选择
虽然PayContext里的if-else消除了,但在使用的地方还是要手动new具体策略。如果策略很多,调用方还是得知道所有策略类的名字。这时候可以配合工厂模式,把策略的创建也封装起来:
importjava.util.HashMap;importjava.util.Map;// 策略工厂publicclassPayStrategyFactory{privatestaticfinalMap<String,PayStrategy>STRATEGIES=newHashMap<>();// 注册所有策略static{STRATEGIES.put("alipay",newAlipayStrategy());STRATEGIES.put("wechat",newWechatPayStrategy());STRATEGIES.put("bank",newBankPayStrategy());}// 根据类型获取策略publicstaticPayStrategygetStrategy(Stringtype){PayStrategystrategy=STRATEGIES.get(type);if(strategy==null){thrownewRuntimeException("不支持的支付方式:"+type);}returnstrategy;}}使用的时候就更简洁了:
PayContextcontext=newPayContext();context.setStrategy(PayStrategyFactory.getStrategy("alipay"));context.executePay(newBigDecimal("99.9"));调用方只需要传一个字符串"alipay",不需要知道AlipayStrategy这个类的存在。新增支付方式只需要在工厂里注册一行,和之前用TOOL_REGISTRY注册工具是一个思路。
这个工厂本质上就是一个注册表:字符串到策略对象的映射。和我们之前在 Agent 工具注册那篇文章里讲的TOOL_REGISTRY是同一个模式。你会发现设计模式之间是相通的,策略模式 + 工厂模式的组合在实际项目中非常常见,几乎是标配。
用 Spring 自动注入策略
如果你用 Spring 框架,策略模式可以用的更方便更优雅。Spring 的@Autowired支持按名称注入,可以自动把所有策略实现收集到一个 Map 里:
importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Component;importjava.util.Map;@ComponentpublicclassPayStrategyFactory{privatefinalMap<String,PayStrategy>strategyMap;// Spring 会自动把所有 PayStrategy 实现注入进来// key 是 Bean 的名称,value 是对应的实例@AutowiredpublicPayStrategyFactory(Map<String,PayStrategy>strategyMap){this.strategyMap=strategyMap;}publicPayStrategygetStrategy(Stringtype){returnstrategyMap.get(type);}}具体策略只需要加上@Component注解并指定名称:
@Component("alipay")publicclassAlipayStrategyimplementsPayStrategy{@OverridepublicStringpay(BigDecimalamount){return"支付宝支付成功:"+amount;}}@Component("wechat")publicclassWechatPayStrategyimplementsPayStrategy{@OverridepublicStringpay(BigDecimalamount){return"微信支付成功:"+amount;}}新增支付方式只需要加一个@Component类,连工厂的注册代码都不用改。Spring 帮你完成了策略的注册和查找,这才是策略模式在实际项目中最常见的用法。
为什么 Spring 的方式更优雅?因为手动注册工厂有个隐患:你加了新的策略类,但忘了在工厂里注册,编译不会报错,只有运行时才会发现找不到策略。而 Spring 的自动注入消除了这个问题,只要你的类实现了接口并加了@Component,就一定会被注入进来,不会遗漏。让框架帮你兜底,比靠人脑记忆可靠得多。
小结
策略模式本质上在做一件事:把"选择哪个算法"和"算法怎么执行"分离开来。算法之间各自封装、互不干扰,选择发生在运行时而不是编译时。
如果代码里出现了越来越多的if-else分支,每个分支对应一种算法或行为,而且这些分支还在不断新增,不妨试一试策略模式。先抽出策略接口,再把每个分支变成一个具体策略类,最后用上下文类来管理和切换策略,这样就成功用策略模式优化了你的if-else代码块。