1. 从一段“烂代码”说起
先说个我早期做项目时遇到的真实场景。某个同事负责一个后端接口,里面有一段“经典”的业务逻辑:根据用户类型发放不同力度的优惠券。代码长这样:
if ("new_user".equals(userType)) { // 新用户注册礼包,满99减30 } else if ("vip".equals(userType)) { // VIP会员专享,满199减80 } else if ("normal".equals(userType)) { // 普通用户,满299减20 } else if ("invited".equals(userType)) { // 被邀请用户,满99减10 } else { // 默认,无优惠 }你可能觉得这没啥问题,但实际情况是:业务方隔三差五加一种用户类型,每次都要往这个方法里塞一段if。过了两三个月,这个方法膨胀到一百多行,看着就头大。更要命的是,测试同事每次都要重新把整个方法的逻辑过一遍,生怕新增分支影响老逻辑。项目组里的每个人都知道这代码“有病”,但没人愿意动,因为太容易改坏了。
这就是典型的“面向过程式分支”写法,它的问题不在于if本身,而在于它把“判断”和“执行”绑死在了一起。每增加一种策略,就要修改旧代码,这违反了设计原则里非常重要的一条——开闭原则:对扩展开放,对修改关闭。
解药就是标题里说的这个老朋友:策略模式。这篇文章我会用最朴素的语言,从概念讲到实战,再附上我在项目里踩过的坑和总结的一些代码写法上的习惯,希望能帮你彻底把它用起来,而不是停留在背定义、画 UML 图的表面阶段。
这篇文章非常适合三类读者:
- 刚接触设计模式、对策略模式只停留在“好像听过这个名词”的初学者;
- 已经能看懂策略模式 Demo,但不知道在 Spring 项目里怎么落地的新人;
- 被一堆
if-else缠身、想重构又不敢动手的开发者。
2. 策略模式到底在说什么
2.1 一个生活化的类比
咱们先不聊代码,聊个生活场景。
想象你开了一家饺子店,店里的厨师分工明确:张三擅长擀皮,李四擅长调馅,王五擅长煮饺子。顾客下单后,店长不会把所有活儿都自己干,而是根据订单内容,把任务分配给对应的师傅。
这里每个师傅就是一种“策略”,店长只要做到“调度”,他自己不需要会擀皮,也不需要会调馅。以后如果来了个赵六,擅长煎饺子,店长只需要新增一个岗位,不用把全店的工作流程改一遍。
策略模式的核心思想就是这么简单:把一组可互相替换的算法(或业务规则)封装起来,客户端通过一个统一的入口去调用,至于具体用哪个实现,在运行时决定。
2.2 三个核心角色
陌生名词最容易劝退小白。咱们先记三个角色,一切代码都只是这三个角色的具象化。
| 角色 | 职责 | 类比 |
|---|---|---|
| 策略接口(Strategy) | 定义一组算法的统一规范,通常是一个接口 | 饺子店里的“厨师”岗位说明书 |
| 具体策略(Concrete Strategy) | 实现策略接口,各自完成一种具体算法 | 张三、李四、王五 |
| 上下文(Context) | 持有策略接口的引用,对外提供统一的调用入口 | 店长,负责调派人手 |
很多教材里还会提到“客户端”(Client),它负责创建具体策略对象并设置到上下文中。在 Spring 项目里,通常由容器帮我们完成这部分工作。
2.3 和状态模式的区别
很多初学者会把策略模式和状态模式搞混,因为它们的类图长得有点像。我在这里简单区分一下,后面不再赘述:
- 策略模式的“策略”是由客户端主动选择的,客户端清楚知道自己要什么规则;
- 状态模式的“状态”是对象内部自己流转的,外部并不关心对象当前处于什么状态,只关心它对外表现的行为。
打个比方:策略模式像你用导航软件选“最快路线”还是“少收费路线”,是你决定的;状态模式像手机根据你当前所处位置自动切换“响铃”“振动”“静音”,是设备自己判断的。
这个区别在实际项目中很重要,因为一旦选错模式,代码会变得非常别扭。这一点我会在本文最后的实操心得里再展开讲一讲。
3. 手写一个最最简单的 Demo
“Talk is cheap.” 咱们直接上手写代码。这个 Demo 不依赖任何框架,只用 JDK 自带的能力,保证你能独立跑通。
3.1 定义策略接口
目标是实现一个计算器,支持加法、减法、乘法。先定义一个统一的策略接口:
public interface CalculateStrategy { int calculate(int a, int b); }就这么简单。这个接口就是整个策略模式的核心契约:你输入两个数,我还你一个数。
3.2 写三个具体策略
加法策略:
public class AddStrategy implements CalculateStrategy { @Override public int calculate(int a, int b) { return a + b; } }减法策略:
public class SubtractStrategy implements CalculateStrategy { @Override public int calculate(int a, int b) { return a - b; } }乘法策略:
public class MultiplyStrategy implements CalculateStrategy { @Override public int calculate(int a, int b) { return a * b; } }每个策略类只做一件事,所以它们都非常短。这不是偷懒,这正是策略模式的设计目标:每个策略足够内聚,单一职责。将来要增加除法,直接新增一个 DivideStrategy 类,不影响任何旧代码。
3.3 写上下文
上下文是为了屏蔽客户端与具体策略实现类的耦合。没有上下文,调用方就得自己 new 一个策略类,然后直接调calculate,这也不是不行,但调用方就和具体实现绑定了,失去了策略“可替换”的意义。
public class CalculatorContext { private CalculateStrategy strategy; // 通过构造方法注入策略 public CalculatorContext(CalculateStrategy strategy) { this.strategy = strategy; } // 通过 setter 方法注入策略(可实现运行时动态切换) public void setStrategy(CalculateStrategy strategy) { this.strategy = strategy; } public int execute(int a, int b) { return strategy.calculate(a, b); } }注意我同时保留了构造器注入和 setter 注入两种方式,这在某些动态场景下非常有用。比如你的程序在运行过程中,根据用户的交互行为改变计算规则,setter 注入就是唯一的解。
3.4 客户端调用
public class Client { public static void main(String[] args) { CalculatorContext context = new CalculatorContext(new AddStrategy()); System.out.println("3 + 5 = " + context.execute(3, 5)); // 运行时切换策略 context.setStrategy(new MultiplyStrategy()); System.out.println("3 * 5 = " + context.execute(3, 5)); } }跑一下,输出如下:
3 + 5 = 8 3 * 5 = 15这个 Demo 虽然简单,但已经完整展示了策略模式的三个核心动作:定义统一接口、编写多个实现、由上下文对外提供服务。
如果你只是想入门,看到这里其实就够了。但现实项目里,我们几乎不会这么“裸用”策略模式,而是会结合 Spring 容器让它发挥更大的威力。下面进入正题。
4. 在 Spring 项目里落地的正确姿势
4.1 为什么不用 new
上面的 Demo 中,你看到了new AddStrategy()这种写法。在真实项目中,我们基本不会直接在业务代码里new策略对象,原因有两个:
- 对象生命周期管理混乱:策略类大多是无状态的(不持有成员变量,或只持有不可变配置),频繁创建和销毁没有意义,交给 Spring 管理成单例更合理;
- 调用方仍然依赖具体类:虽然
CalculatorContext解耦了,但如果客户端还要new,实际上策略的选择逻辑还是散落各处。
我们的目标是:让调用方根本感觉不到策略类的存在,传入一个类型标识,自动完成分发。
4.2 用 Spring 管理策略族
先定义策略接口,这次我们拿真实的业务场景来说——会员订单折扣计算:
public interface DiscountStrategy { // 返回这个策略对应的用户类型或场景标识 String getUserType(); // 根据原价计算折扣后价格 BigDecimal calculateDiscount(BigDecimal originalPrice); }接口里加了一个getUserType()方法,为什么要加?因为下面要用它和 Spring 的 Bean 名称/集合特性配合,实现自动路由。
实现三个策略类,并注册为 Spring Bean:
@Component public class NewUserDiscountStrategy implements DiscountStrategy { @Override public String getUserType() { return "new_user"; } @Override public BigDecimal calculateDiscount(BigDecimal originalPrice) { // 新用户,统一打 8.5 折 return originalPrice.multiply(new BigDecimal("0.85")); } }@Component public class VipDiscountStrategy implements DiscountStrategy { @Override public String getUserType() { return "vip"; } @Override public BigDecimal calculateDiscount(BigDecimal originalPrice) { // VIP 用户,满 199 减 80,不满则原价 if (originalPrice.compareTo(new BigDecimal("199")) >= 0) { return originalPrice.subtract(new BigDecimal("80")); } return originalPrice; } }@Component public class DefaultDiscountStrategy implements DiscountStrategy { @Override public String getUserType() { return "default"; } @Override public BigDecimal calculateDiscount(BigDecimal originalPrice) { // 默认用户,不打折 return originalPrice; } }一句话总结这三个类:getUserType()是策略的“门牌号”,calculateDiscount()是策略的“实际动作”。
4.3 核心:利用 Spring 自动注入策略 Map
这是整个实战中最关键、也最让人眼前一亮的地方。Spring 支持将某个接口的所有实现类自动收集到一个Map中,Map 的 key 就是 Bean 名称。
但我们不会直接用 Bean 名称作为路由,因为 Bean 默认名称是类名首字母小写,比如newUserDiscountStrategy,不够直观、也不够稳定。所以我们需要做一个“别名映射”。
先看代码:
@Service public class DiscountStrategyContext { private final Map<String, DiscountStrategy> strategyMap; // Spring 自动将策略接口的所有实现类注入到这个 Map 中 public DiscountStrategyContext(Map<String, DiscountStrategy> strategyMap) { this.strategyMap = strategyMap; } /** * 根据用户类型获取对应策略 */ public DiscountStrategy getStrategy(String userType) { // 重新构建映射关系:策略标识 -> 策略对象 return strategyMap.values().stream() .filter(strategy -> strategy.getUserType().equals(userType)) .findFirst() .orElse(strategyMap.get("defaultDiscountStrategy")); } public BigDecimal execute(String userType, BigDecimal originalPrice) { DiscountStrategy strategy = getStrategy(userType); return strategy.calculateDiscount(originalPrice); } }这段代码干了什么?我拆解一下:
Map<String, DiscountStrategy> strategyMap:Spring 看到构造器参数是一个 Map,并且泛型 key 是 String,会自动把容器中所有DiscountStrategy类型 Bean 收集进来,key 是 Bean 名称;getStrategy:遍历这个 Map,根据每个策略自己声明的getUserType()找到匹配项;orElse兜底:找不到对应策略时,回退到默认策略defaultDiscountStrategy,避免空指针。
这里有小伙伴会问:为什么不直接用 Bean 的名称作为 key?因为 Bean 名称是约定俗成的类名首字母小写,换个人重构类名就全乱了。而getUserType()是策略自己声明出来的标识,业务语义更强,也更稳定。
4.4 controller 层调用
@RestController @RequestMapping("/order") public class OrderController { private final DiscountStrategyContext strategyContext; public OrderController(DiscountStrategyContext strategyContext) { this.strategyContext = strategyContext; } @PostMapping("/price") public BigDecimal calculate(@RequestBody OrderRequest request) { return strategyContext.execute(request.getUserType(), request.getOriginalPrice()); } }调用方传入什么用户类型,内部就自动路由到对应策略。业务层不需要出现任何if。注意,这个版本用的是 Java 8 的 Stream 遍历,性能完全够用,因为策略数量通常是个位数到十几个,不是性能瓶颈。
如果你用的是更现代的写法(比如 Java 8 的Map.merge或者直接构造一个HashMap手动填充),也是可以的。但上面这种基于 Spring 自动注入的风格最干净,也最符合主流项目的习惯。
4.5 直接在 Map 上做逻辑的优化空间
有经验的开发可能已经发现,每次调用getStrategy时都要遍历整个 Map,不够优雅。虽然量小无所谓,但我们可以做一个“更聪明”的初始化:
@Service public class DiscountStrategyContext { private final Map<String, DiscountStrategy> strategyMap = new HashMap<>(); // Spring 会把所有实现注入到 list 中 public DiscountStrategyContext(List<DiscountStrategy> strategies) { // 在构造阶段就建立好路由表 for (DiscountStrategy strategy : strategies) { this.strategyMap.put(strategy.getUserType(), strategy); } } public DiscountStrategy getStrategy(String userType) { return strategyMap.getOrDefault(userType, strategyMap.get("default")); } public BigDecimal execute(String userType, BigDecimal originalPrice) { DiscountStrategy strategy = getStrategy(userType); return strategy.calculateDiscount(originalPrice); } }把“路由表的构建”放在构造函数里完成,之后每次查询都是 O(1) 的 HashMap 查找。这种写法还解决了一个隐患:如果有两个策略返回了相同的getUserType(),在构建阶段就会覆盖掉前面的值,你很快就能发现问题(或者也可以用检测重复的代码提前报错),而不是等到调用时才出现诡异行为。
我给个小建议:如果策略数量比较少,用 Stream 遍历即可,代码更直观;如果策略种类比较多,或者对性能有轻微要求,用第二种“构建路由表”的方式,差别其实也不大。但无论如何,别在设计上把策略标识写死成“约定”,一定要让每个策略自己报告身份,这样以后加策略才真正省事。
5. 策略模式 + 枚举与函数式接口的“现代玩法”
如果你觉得上面的 Spring 装配方案还不够过瘾,我再介绍一种近两年在项目里越来越常见的写法:策略模式结合枚举和Function接口。
这种写法特别适合那种“策略体量小、一个方法就搞定”的场景,能把代码量压缩到极致,同时不失可读性。
public enum DiscountStrategyEnum { NEW_USER("new_user", price -> price.multiply(new BigDecimal("0.85"))), VIP("vip", price -> price.compareTo(new BigDecimal("199")) >= 0 ? price.subtract(new BigDecimal("80")) : price), DEFAULT("default", price -> price); private final String userType; private final Function<BigDecimal, BigDecimal> handler; DiscountStrategyEnum(String userType, Function<BigDecimal, BigDecimal> handler) { this.userType = userType; this.handler = handler; } public static BigDecimal execute(String userType, BigDecimal originalPrice) { for (DiscountStrategyEnum strategy : values()) { if (strategy.userType.equals(userType)) { return strategy.handler.apply(originalPrice); } } return DEFAULT.handler.apply(originalPrice); } }调用:
BigDecimal price = DiscountStrategyEnum.execute("vip", new BigDecimal("230"));这种写法本质上是“策略模式 + 枚举 + lambda”的组合拳,三个角色的代码全部收拢到一个枚举里。它的最大优势是代码集中、一眼就能看出所有策略全貌;最大劣势是策略之间如果有大量共享逻辑,就不容易复用。
所以我的建议是:
- 策略方法体只有一两行的,优先用枚举 + lambda 就够了;
- 策略方法体超过十行、或需要依赖多个服务组件的,用 Spring Bean + 上下文的完整策略模式。
两者不冲突,甚至可以混合使用。好的设计从来不是“只用一种模式”,而是“在最合适的位置用最合适的方式”。
6. 常见问题与排查技巧实录
这块内容是我的“独家干货担当”。下面这些问题全部来自真实开发过程中的现身说法,有的问题不是看两篇博客就能知道的。
6.1 Spring 注入 Map 后 Bean 名称对不上的问题
新手容易踩的第一个坑:Map 注入后,key 是 Bean 名称没错,但当你用自定义的getUserType()去查找时,发现查不到。原因一般有两个:
- 类上没有加
@Component/@Service,Spring 容器里根本没有这个 Bean,Map 里自然是空的; getUserType()返回的字符串和传入的userType不一致,比如多了一个空格、大小写不同。
排查方法:可以在DiscountStrategyContext的构造函数中加一个断点,或者干脆在启动的时候打日志,把 Map 的 key 和每个策略上报的getUserType()打出来一览无遗。让“锅”浮出水面,别靠肉眼去读代码。
6.2 多个策略类导致的无用类数量膨胀
这是“过度设计”的典型反面例子。有些团队为了用设计模式而用设计模式,散弹枪式地创建七八个策略类,每个类就写了一行 return,抽象过度,反而让新人看不懂。
解决办法:评估策略的复用性和复杂度。如果一个策略只有一行 return,且没有多个地方复用,那这个策略就没有独立成类的必要,用枚举或直接在调用处做条件判断更合适。策略模式的意义在于“算法族的可替换性”,不是“消灭所有的 if”。有些 if 是业务状态的合法映射,不该被消灭。
我个人的判断标准比较主观:一个策略类的方法体低于三行,且策略总数不超过三个,我就会考虑是否真的要用完整策略模式。重模式的收益主要在长期维护和扩展上,但短期需要付出一定的阅读成本。
6.3 策略里塞了太多代码,变成“上帝策略”
还有另一种反面案例:策略类数量虽然不多,但某个策略类的方法体写了两百行,把所有特殊情况都堆在里头。这其实就不是策略的问题了,而是“策略内部分支过多”。
处理思路是:如果一个策略太复杂,应继续拆分成子策略,或者用模板方法模式固定主流程,把可变部分继续向下抽象。策略模式可以嵌套使用,这不是什么丢人的事。嵌套时注意保持每个子策略的命名清晰,别最后自己都找不到北。
6.4 运行时动态切换策略为什么不生效
有些同学在 controller 里先getStrategy拿到了策略对象,之后想换策略,发现换不掉。原因是策略上下文持有的是策略对象的副本,而你改的是另一个引用。
这种问题通常出在:
- 没有通过上下文更新策略,而是直接改了局部变量;
- 或者策略被 Spring 配置成了单例,但你在策略内部不小心写了可变的成员变量。
我的建议:策略类尽量设计成无状态的(不持有可变成员变量)。如果确实需要携带配置参数,可以用@Value注入只读配置,但不要在策略类里声明可变的HashMap之类的东西。多线程环境下,这会造成非常隐蔽的 bug。
6.5 策略内部需要用 Spring 管理的其他 Bean
策略类也是 Bean,所以可以直接通过构造器把需要的其他组件注入进来:
@Component public class VipDiscountStrategy implements DiscountStrategy { private final CouponService couponService; public VipDiscountStrategy(CouponService couponService) { this.couponService = couponService; } @Override public BigDecimal calculateDiscount(BigDecimal originalPrice) { BigDecimal vipDiscount = originalPrice.subtract(new BigDecimal("80")); // 还可以叠加优惠券 return couponService.applyCoupon(vipDiscount, "VIP_COUPON"); } }这正好演示了策略模式的“可组合性”:具体策略内部完全可以调用其它服务,它的职责只是“算折扣”,至于折扣计算过程中需要查表、校验、准备数据,是它自己的事。
7. 策略模式背后的设计思想
很多初学者困惑:策略模式就这么简单?为什么那么多资深前辈强调它的价值?
我理解,策略模式真正想解决的,不是“消除 if-else”这个问题本身,而是让代码的可变部分和不可变部分相互隔离:
- 不可变部分:调用的入口(
getStrategy)、路由方式(Map 映射)、默认兜底逻辑; - 可变部分:每个策略内部的业务规则。
这种隔离带来了三个长期收益:
- 可测试性高:每个策略可以独立写单元测试,不需要启动整个 Spring Boot 应用,也不需要 mock 一堆不相关的类;
- 变更成本低:新增一个策略,只需新增一个类,不改动任何现有类,代码评审时 diff 极小,回归测试的范围也极小;
- 新人友好:新同学接手时,只需要看
DiscountStrategyContext和策略接口,就能理解整个规则体系的运作方式,不需要在一个几百行的 if-else 里考古。
还有人会问:这和“面向接口编程”有什么区别?区别就是策略模式把“用接口来调用算法”和“算法的运行时选择”两者统一了起来。面向接口编程是基本功,而策略模式是基本功在具体业务场景中的一种经典编排方式。
8. 工具选型与代码评审时的关注点
这一节回答一个很多人关心的问题:策略模式在代码评审中出现时,评审者一般会看什么?也能帮你自查。
关注点一:策略标识是否稳定。getUserType()这类标识字段,不应该取自用户可修改的原始输入,而应该来自后端内部定义好的枚举或字典。否则用户传什么值,策略就随便命中,安全性带隐患。
关注点二:兜底策略是否存在。永远留一个 default 策略,因为路由查不到的情况一定会出现,与其让代码报空指针,不如让它走一个明确的默认行为。
关注点三:策略之间是否存在隐式耦合。有些策略可能依赖“前一个策略已经执行过”的上下文状态,这种隐式耦合一旦出现,策略就退化成了一种顺序执行的 paint 流程,不再是独立的算法族了。这时候需要重新梳理一下拆分粒度和调用时序。
关注点四:Spring 的注入写法是否干净。见过有人在策略 Map 的获取上又包了一层@Autowired字段注入,混合了字段注入和构造器注入风格,代码虽能运行,但一致性很差,后续维护会增加心理负担。建议全项目统一用构造器注入。
9. 策略模式在真实项目中的一个完整场景还原
我常喜欢把知识放在一个完整的场景里去验证。这里我构造一个尽量贴近实际业务需求的小项目,把它从头到脚走一遍,最终代码的结构你可以直接“抄”。
场景:某商城支持“多支付方式结算”。不同支付方式的手续费率、封顶金额、打折系数不同。支付方式包括:余额支付、微信支付、支付宝支付、银行卡支付。每种方式的计费规则如下:
| 支付方式 | 计费规则 |
|---|---|
| 余额支付 | 手续费率为 0 |
| 微信支付 | 手续费率为 0.6%,最高 20 元 |
| 支付宝支付 | 手续费率为 1%,最高 25 元 |
| 银行卡支付 | 手续费率为 1.2%,最高 50 元 |
这个场景非常适合策略模式,因为:
- 支付方式会不断增加(比如后续扩展“数字人民币支付”);
- 每种支付方式的费率算法互相独立、互不干扰;
- 调用的入口是统一的服务,外部只需要传入支付方式标识。
接口:
public interface PaymentFeeStrategy { String getPaymentType(); BigDecimal calculateFee(BigDecimal amount); }实现类之一:
@Component public class WechatPayFeeStrategy implements PaymentFeeStrategy { @Value("${pay.wechat.rate:0.006}") private BigDecimal rate; @Value("${pay.wechat.cap:20}") private BigDecimal cap; @Override public String getPaymentType() { return "wechat"; } @Override public BigDecimal calculateFee(BigDecimal amount) { BigDecimal fee = amount.multiply(rate); return fee.min(cap); } }其它支付方式类似。然后上下文:
@Service public class PaymentFeeContext { private final Map<String, PaymentFeeStrategy> strategyMap; public PaymentFeeContext(List<PaymentFeeStrategy> strategies) { strategyMap = new HashMap<>(); for (PaymentFeeStrategy strategy : strategies) { strategyMap.put(strategy.getPaymentType(), strategy); } } public BigDecimal calculateFee(String paymentType, BigDecimal amount) { PaymentFeeStrategy strategy = strategyMap.get(paymentType); if (strategy == null) { throw new IllegalArgumentException("Unsupported payment type: " + paymentType); } return strategy.calculateFee(amount); } }这里我故意没有加兜底策略,而是直接抛出异常。为什么?因为支付方式不支持时是硬错误,不应该是默认行为,这和用户折扣场景的“默认策略”语义不同。这个细节说明一个道理:模式是骨架,业务语义才是血肉,不能机械套用。
测试类可以这样写:
@SpringBootTest class PaymentFeeContextTest { @Autowired private PaymentFeeContext context; @Test void wechatFee() { BigDecimal fee = context.calculateFee("wechat", new BigDecimal("100")); // 100 * 0.006 = 0.6 assertEquals(new BigDecimal("0.6"), fee); } @Test void wechatFeeCap() { BigDecimal fee = context.calculateFee("wechat", new BigDecimal("5000")); // 5000 * 0.006 = 30 -> 封顶 20 assertEquals(new BigDecimal("20"), fee); } }这套结构放回真实项目里,无论是扩展新的支付方式,还是调整某一种支付方式的费率,都只需要动很小的一块环境,完全符合开闭原则。
10. 策略模式常见误用自查清单
| 现象 | 问题根源 | 该怎么做 |
|---|---|---|
| 策略类数量爆炸,每个策略就一行代码 | 过度抽象 | 考虑用枚举 + 函数式接口替代 |
| 上下文里出现大量 if-else | 路由逻辑没抽象好 | 用 Map 构建路由表替代 |
| 策略类里持有可变状态 | 线程安全风险 | 改成无状态或使用局部变量 |
| 调用方频繁新建策略对象 | 没有利用 Spring 容器 | 交给 Spring 管理成单例 |
| 策略标识写死在字符串里 | 维护困难易脏数据 | 定义枚举常量统一管理 |
| 不同策略间有顺序依赖 | 拆分粒度不当 | 将顺序逻辑放进编排层,而不是策略内部 |
这张表建议收藏,每次自测代码时对照一遍,可以帮你避开大部分设计层面的坑。
11. 我在团队里推行策略模式时的三个心得
代码说完了,最后聊点软性的东西。
第一,警惕“模式化改造强迫症”。我曾经在一个项目里,看到某个同学把一段三行的 if-else 硬生生套上策略模式,额外产生了四个类。这种改造除了让类图变得花哨之外,没有带来任何实际收益。先问自己三个问题:这段逻辑是否频繁变更?算法是否互相独立?调用方是否需要动态切换行为?三个问题答案只要不是“是”,就别动刀。
第二,策略模式的蔓延速度比想象中快。一旦你落地了一处,业务方尝到了“加功能不回归”的甜头,很快就会在更多场景复制这个模式。这本来是好事,但要注意控制策略粒度的统一。我会和一个团队的成员约定一个简单规范:策略接口的语义必须单一,比如“计算折扣”和“计算运费”不要混在同一个接口里。
第三,文档要跟着策略实现走。策略类一多,如果没有人维护一张“策略 - 标识 - 生效规则”的速查表,后续维护者很快就晕了。我把这张表放在代码仓库的 markdown 文件里,每次新增策略时要求同时更新速查表。这不是强制规范,但做了几次之后,大家都发现可以少问很多“这个策略是干嘛的”的重复问题。
最后再分享一个小技巧:在策略类比较多的时候,可以在类名上用业务前缀统一风格,比如OrderDiscountStrategy、OrderShippingStrategy,这样通过 IDE 的类名搜索,就能快速定位到指定领域中所有可用的策略,比直接改Map里的 key 要直观得多。
希望这篇从概念到实战、从代码到心法的长文,能让你彻底拿捏策略模式。下次再看到那种几十个分支的 if-else 时,你就有底气把它优雅地重构掉了。