1. 从一堆if-else说起:策略模式真正要解决的痛点
Java设计模式这个系列写到第三篇,终于要碰策略模式了。哪怕你没系统学过设计模式,也大概率在业务代码里和它狭路相逢过:一段看着人畜无害的价格计算逻辑,随着会员等级、促销活动、优惠券叠加逐渐变成几十层if-else;每次加需求都要小心翼翼地把新分支塞进某个神奇方法。真正拆过这种代码的人都明白,那一刻最需要的不是再补一个else if,而是换一种组织行为的方式。策略模式的核心就是把算法逐一封装成独立对象,让调用方说一声“按这个策略算”就能拿到想要的结果。
1.1 一个把项目拖垮的促销计价场景
假设某电商系统里,结算模块最初只有普通用户和VIP用户两档价格,代码写得很顺。一个if搞定,读起来又清晰又好维护。后来产品经理说要支持不同会员等级的折扣率,于是if-else从二选一变成了多选一。再后来,运营盯上了节日活动,价格计算变成了“会员折扣叠加节日满减,再叠加新客立减”的排列组合,方法开始膨胀。
以下是当时代码的简化版,省略了数据库查询等细节:
public BigDecimal calculatePrice(Order order, User user, LocalDate date) { BigDecimal price = order.getTotalPrice(); // 会员等级折扣 if ("NORMAL".equals(user.getLevel())) { // 无折扣 } else if ("SILVER".equals(user.getLevel())) { price = price.multiply(new BigDecimal("0.98")); } else if ("GOLD".equals(user.getLevel())) { price = price.multiply(new BigDecimal("0.95")); } else if ("PLATINUM".equals(user.getLevel())) { price = price.multiply(new BigDecimal("0.90")); } // 节日活动折扣 if (isHoliday(date)) { price = price.subtract(new BigDecimal("50")); } if (isNewUser(user)) { price = price.multiply(new BigDecimal("0.80")); } return price; }有经验的开发者看到这种代码会立刻头皮发麻:这个方法的变化点和稳定点被糅合在一起。折扣规则是变化最频繁的部分,但它被硬编码在“调用链”中间;促销活动是另一个变化维度,又和会员折扣通过无意义的顺序绑定。想加一个“周年庆限时秒杀”,得先回答一个问题:“到底应该插在会员折扣前面还是后面?”当你开始思考这种顺序问题时,实际上已经在用一个隐形的、没有文档的状态机来管理业务规则了。
1.2 开闭原则:把“变化”关进独立的箱子
面向对象设计里有一条和策略模式关系最密切的原则——开闭原则:对扩展开放,对修改关闭。上面这个例子的问题并不在于if-else本身,而在于它把规则“写死”在了使用方法里。下次要增加“企业客户专属折扣”时,开发者恨不得翻遍整个方法,把几层判断小心翼翼地拆开,生怕影响既有逻辑。每来一个需求,改动范围都在扩大。
策略模式解决这个问题的思路是:把不同计价方式抽象成一个接口,每种会员等级或每种活动提供独立实现,并把算法之间的差异封装在各自类中。当新需求出现时,新增策略类即可;调用方法和老策略类都不用动。这就是“对修改关闭”的含义。
当然,设计模式不是银弹,策略模式也有自己的代价。后面我会专门用一节来说哪些场景不该用,以及类数量膨胀以后该怎么办。
2. 策略接口、具体策略、上下文:三个角色各司其职
策略模式的结构非常单纯,只有三个角色:策略接口、具体策略类、上下文类。这三个角色的边界一乱,模式就会变味。我在代码评审时见过不少挂着“策略模式”名字的错误实现——把所有实现塞进同一个类,然后对外声称利用了多态。那不叫策略模式,那叫换个命名方式的switch。
2.1 策略接口:给一组算法立规矩
策略接口定义一个统一的算法入口,让上层代码不需要知道具体实现的存在。在Java里,它通常是一个只有单个抽象方法的接口,甚至可以直接声明成函数式接口:
public interface DiscountStrategy { BigDecimal apply(BigDecimal originalPrice, OrderContext context); }这个接口是不是只有一个方法是次要的,重要的是所有策略类都能通过同一个方法签名被调用,并且调用方不需要根据类型做instanceof判断。如果接口里加入了和计价无关的方法,或者策略内部需要外部传很多个性化参数,那就要警惕接口设计是否存在问题。
我见过一个失败的接口,它同时定义了两个方法:一个算折扣,一个判断是否支持某种渠道。最后实现类里要么两个方法都写上同样无用逻辑,要么干脆抛异常。接口的职责一旦不纯粹,策略之间的共性就消失了,策略模式也就名存实亡。
2.2 具体策略:算法族里的每位成员
每个具体策略类实现接口,承载真正可替换的算法。以订单计价为例,可以有NormalDiscountStrategy、SilverDiscountStrategy、GoldDiscountStrategy,也可以有HolidayDiscountStrategy。类的名字本身就是业务语义,阅读代码时甚至不需要打开类就能大概知道规则长什么样:
public class GoldDiscountStrategy implements DiscountStrategy { @Override public BigDecimal apply(BigDecimal originalPrice, OrderContext context) { return originalPrice.multiply(new BigDecimal("0.95")); } }具体策略之间互相不感知,各自持有自己的内部状态或依赖。这正是“算法族”的典型结构——它们解决同一类问题,但具体做法完全不同。策略模式并不规定这些类必须继承自某个抽象基类,接口已经足够,因为策略之间的公共逻辑往往少得可怜。
2.3 上下文类:策略的持有者与调用入口
上下文类在策略模式里承担“中转站”的角色,它不像工厂那样负责创建策略,而是负责把策略“接住”,然后在合适的时机调用。简单场景里,上下文可以直接持有策略引用:
public class OrderPriceCalculator { private final DiscountStrategy strategy; public OrderPriceCalculator(DiscountStrategy strategy) { this.strategy = strategy; } public BigDecimal calculate(Order order) { OrderContext context = new OrderContext(order.getUser(), order.getItems(), order.getDate()); return strategy.apply(order.getTotalPrice(), context); } }从这个代码可以看出一个微小但重要的边界:上下文不应该负责决定“选哪个策略”,否则上下文就会成为if-else的新温床。上下文只需要知道“我这里有一个策略可用”,至于这个策略是谁传进来的、为什么选它,它都不该关心。一旦上下文里出现了判断会员类型的逻辑,就说明选择策略的职责被放错了地方。
3. 一次完整重构实录:订单计价器从if-else瀑布走向策略模式
理论讲得再多,不如动手跑一遍。我在某电商项目里做过一次非常典型的计价模块重构,当时那个方法已经膨胀到一百多行,每次发版都不敢碰。我把重构过程拆成三个版本,带着当时的取舍记录在这里,比直接贴一个“标准答案”更有参考价值。
3.1 第一版:让所有会员逻辑挤在一个方法里
先梳理出当时的计价需求:
- 普通会员:原价,无任何折扣
- 银卡会员:98折
- 金卡会员:95折
- 钻石会员:88折
- 节假日全场促销:在上述基础上再减10元
- 新客立减:首次下单减20元
当时的第一版计价方法大致是下面这个样子:
public BigDecimal price(Order order, User user) { BigDecimal price = order.getTotalPrice(); String level = user.getLevel(); if ("SILVER".equals(level)) { price = price.multiply(BigDecimal.valueOf(0.98)); } else if ("GOLD".equals(level)) { price = price.multiply(BigDecimal.valueOf(0.95)); } else if ("DIAMOND".equals(level)) { price = price.multiply(BigDecimal.valueOf(0.88)); } if (isHoliday()) { price = price.subtract(BigDecimal.TEN); } if (user.isNew()) { price = price.subtract(BigDecimal.valueOf(20)); } return price; }这段代码的问题一眼就能看出来:随着需求增多,方法会同时承担“选策略”“执行策略”“叠加策略”三种逻辑。而且,如果产品经理突然决定“节假日不再和会员折扣叠加,改成二选一”,你就需要在这个方法里增加几行优先级判断。连锁影响的范围完全不可控。更麻烦的是,这段价格规则根本没法做精准的单测——想测试金卡加节假日的组合,必须构造一个完整的User对象,还要让isHoliday()返回true;如果业务逻辑里藏了静态方法,那测试会直接变成一场噩梦。
3.2 第二版:策略类上线,计价逻辑各回各家
重构第一步,定义策略接口,再把每个会员等级做成一个策略类。这个步骤非常机械,但好处立竿见影:所有规则都有自己的“家”了。
public interface DiscountStrategy { BigDecimal apply(BigDecimal originPrice, User user); } public class SilverDiscountStrategy implements DiscountStrategy { @Override public BigDecimal apply(BigDecimal originPrice, User user) { return originPrice.multiply(BigDecimal.valueOf(0.98)); } } public class GoldDiscountStrategy implements DiscountStrategy { @Override public BigDecimal apply(BigDecimal originPrice, User user) { return originPrice.multiply(BigDecimal.valueOf(0.95)); } }然后,在计价服务里通过一个Map把类型码映射到具体策略:
public class PriceCalculator { private final Map<String, DiscountStrategy> strategyMap = new HashMap<>(); public PriceCalculator() { strategyMap.put("SILVER", new SilverDiscountStrategy()); strategyMap.put("GOLD", new GoldDiscountStrategy()); strategyMap.put("DIAMOND", new DiamondDiscountStrategy()); } public BigDecimal calculate(Order order) { User user = order.getUser(); DiscountStrategy strategy = strategyMap.get(user.getLevel()); if (strategy == null) { strategy = new NormalDiscountStrategy(); } BigDecimal price = strategy.apply(order.getTotalPrice(), user); if (isHoliday()) { price = price.subtract(BigDecimal.TEN); } if (user.isNew()) { price = price.subtract(BigDecimal.valueOf(20)); } return price; } }写到这里,被重构过的同学可能已经发现问题:第二版虽然把每个等级抽成了策略类,但“选策略”的逻辑仍然存在,只是从if-else变成了map.get。如果产品经理决定调整策略的组合顺序,这逻辑还写在calculate里,问题的本质没有彻底解决。但至少有两大收获:第一,每个策略类可以单独测试;第二,增加新的会员等级,不需要再碰calculate方法的主体。这两项收获已经足以支撑一次commit的价值。
3.3 第三版:把组合与顺序交给策略编排器
第三版才是关键。在这个模拟项目中,我们把“一个会员等级可能叠加多个促销”的需求拆解成了“策略选择”和“策略执行”两层,并引入了策略编排器。
public interface DiscountStrategy { BigDecimal apply(BigDecimal originPrice, OrderContext context); } public class OrderDiscountComposer { private final List<DiscountStrategy> strategies; public OrderDiscountComposer(List<DiscountStrategy> strategies) { this.strategies = strategies; } public BigDecimal calculate(Order order) { BigDecimal price = order.getTotalPrice(); OrderContext context = new OrderContext(order); for (DiscountStrategy strategy : strategies) { price = strategy.apply(price, context); } return price; } }组合顺序从“编码写死”变成了“由调用方传入”,这样测试不同顺序就非常灵活。模拟项目里我们甚至在启动配置里定义了策略列表,让运营人员可以通过配置文件调整顺序,而不用重新编译。当然,这属于策略模式之上的另一层扩展,模式本身并不强制这样做。核心洞察在于:策略模式的目的不是“消灭判断”,而是把判断和执行分离,让变化点集中在某几处,而不是散落在业务调用的各个角落。
3.4 重构后的扩展性体检与单测落地
重构到第三版后,如果产品说“我们要支持企业客户,设置专属95折”,需要做的事就变成了:
- 新增一个企业客户策略类
- 在策略注册表或配置里增加对应的映射规则
原始方法、老策略类、上下文类都不需要改动。这就是开闭原则落地时的直观体感。而且,每个策略的单测可以只关注自己的输入输出,编写成本断崖式下降。
例如,金卡策略的测试可以写成这样:
public class GoldDiscountStrategyTest { @Test void should_apply_95_percent_discount() { DiscountStrategy strategy = new GoldDiscountStrategy(); OrderContext context = new OrderContext(user, items, date); BigDecimal result = strategy.apply(BigDecimal.valueOf(100), context); assertEquals(0, result.compareTo(BigDecimal.valueOf(95))); } }不再需要mock一整条服务链,不再需要造复杂的订单状态,只验证一个纯函数式的输入输出关系即可。我在项目里给团队演示过这套流程,一个新同学从需求到新增策略类再到补上单测,半小时出头就完成。这个速度在第一版代码体系下是不敢想象的。
4. 从手工new策略到注册表+工厂:策略选择的解耦进阶
很多教材讲到策略模式就在这里打住了,但真实项目里还有一个躲不开的问题:既然策略不负责选择自己,那谁来选?选错策略直接导致线上事故。我见过不少团队把“选策略”的逻辑留在业务代码里,结果每调用一次计价器,就得写一遍new,同一个字符串判断散落在五个不同服务里。
4.1 谁负责挑选策略:封装选择逻辑
我们把“选策略”的逻辑放进一个独立的工厂类,业务侧不需要知道策略怎么选,直接调用工厂即可。这里有两种常用做法:if-else工厂和注册表工厂。先说最简单的一种:
public class DiscountStrategyFactory { public static DiscountStrategy of(String level) { if ("SILVER".equals(level)) { return new SilverDiscountStrategy(); } if ("GOLD".equals(level)) { return new GoldDiscountStrategy(); } if ("DIAMOND".equals(level)) { return new DiamondDiscountStrategy(); } return new NormalDiscountStrategy(); } }简单工厂本质还是if-else,但选择逻辑被收敛到了单个类里。对于分支较少的场景完全够用。真正的进步在于:选择逻辑从业务调用方迁移到了工厂,业务方法里不再重复出现“new策略”的结构。后续即使要增加新的会员等级,也只需要改一个工厂类,不会出现全局搜索到处修改的情况。
4.2 枚举注册表与Map查表
分支稍多时,工厂里那串if同样让人难受。我更喜欢用枚举注册表来组织策略。在每个策略上加上一个枚举,把类型码和策略一一绑定:
public enum MemberLevel { SILVER("silver", new SilverDiscountStrategy()), GOLD("gold", new GoldDiscountStrategy()), DIAMOND("diamond", new DiamondDiscountStrategy()); private final String code; private final DiscountStrategy strategy; MemberLevel(String code, DiscountStrategy strategy) { this.code = code; this.strategy = strategy; } public static DiscountStrategy match(String code) { for (MemberLevel level : values()) { if (level.code.equals(code)) { return level.strategy; } } return new NormalDiscountStrategy(); } }这种写法有两个优势:策略和业务代码在枚举里被紧凑映射,新增等级只需在枚举里加一行;枚举的values()天然提供了遍历能力,方便做兜底和统计。但要注意,枚举常量在JVM里是单例的,如果策略类内部持有可变状态,就可能在并发场景下出问题。所以,注册表里的策略类最好设计成无状态,或者保证线程安全。
4.3 综合方案:工厂持有注册表
真正的生产环境里,我会把“枚举注册表”和“工厂”结合:工厂封装“查找策略”的逻辑,内部使用枚举或Map作为注册表;如果查不到,就返回一个默认策略。业务侧对工厂的依赖非常干净。
public class DiscountStrategyFactory { private static final Map<String, DiscountStrategy> REGISTRY = new HashMap<>(); static { REGISTRY.put("SILVER", new SilverDiscountStrategy()); REGISTRY.put("GOLD", new GoldDiscountStrategy()); REGISTRY.put("DIAMOND", new DiamondDiscountStrategy()); } public static DiscountStrategy get(String level) { return REGISTRY.getOrDefault(level, new NormalDiscountStrategy()); } }getOrDefault这一行是Java 8的便利,但真正的业务意义是:任何未知会员等级都不该直接报错,而应当退化到最基础策略。这个“缺省兜底”的思路非常关键,它让系统对新接入的第三方渠道、未知配置、脏数据产生极好的容忍度。如果不做兜底,一个未知等级的用户进来,就有可能计算出错误的订单价格,甚至抛异常影响整单下单。
有同学可能会问:为什么不直接抛出异常强制规范数据?我的经验是,线上环境里的脏数据往往来自老接口、历史缓存或外部系统,与其让用户看到报错,不如先兜底再记录日志,后续异步修复数据。这种降级思想在真实系统里比“严格校验”更实用。
5. 策略模式与工厂模式、状态模式的恩怨纠葛
设计模式之间经常被人认错,尤其是“策略模式”与“状态模式”,以及“策略模式”与“工厂模式”。我在做内部技术分享时特别喜欢拿这几对做对比,因为它们看起来太像了,但实际上关心的问题完全不同。
5.1 工厂模式负责“造对象”,策略模式负责“怎么跑”
工厂模式解决的问题是:客户端不想直接new具体类,于是把创建环节委托给工厂。它关心的是创建时机、参数校验、选择合适的实现类。策略模式解决的问题是:在运行时切换一组算法,上下文在这些算法之间自由选择。它关心的是执行时机和算法替换。
一个通俗的类比:工厂模式像便利店,顾客说“我要一瓶饮料”,店员从货架上拿一瓶出来递给他;策略模式更像你口袋里同时装着地铁卡和公交卡,走到闸机前掏哪张由你决定。前者决定“用什么对象”,后者决定“怎么执行动作”。两者经常配套出现:工厂负责根据配置创建或查找策略对象,策略负责后续的行为切换。这种组合在Java企业级项目里几乎无处不在。
5.2 状态模式:行为随内部状态流转,策略模式:行为由外部指定
状态模式在类图上长得几乎和策略模式一模一样,这也是很多人混淆的根源。但只要关注使用语义,就能发现根本区别。状态模式的重心是“状态机的流转”:对象内部维护一个状态,这个状态自己决定下一个状态是谁,状态变迁由内部事件驱动。策略模式的重心是“算法的替换”:上下文把某个外部策略塞进来,这套算法不关心下一个策略是谁,调用方随时可以抽换。
举个具体例子:订单状态机里,一个订单从“待支付”流转到“已支付”,再由“已支付”流转到“已发货”,这种迁移是内聚的、自驱动的,适合状态模式。而“计算会员折扣”这件事,用金卡策略还是钻石卡策略,完全由当前用户数据决定,策略之间不存在迁移关系,适合策略模式。如果非要把后者设计成状态模式,就会把算法选择状态机化,最后得到一套看起来高级、但没人维护得动的结构。
5.3 一张表看清三种模式的分工
| 对比维度 | 工厂模式 | 策略模式 | 状态模式 |
|---|---|---|---|
| 核心问题 | 对象该由谁创建 | 算法该如何替换 | 行为如何随状态迁移 |
| 关注时机 | 创建期 | 运行期 | 运行期 |
| 谁来选择 | 客户端传入参数 | 调用方设置策略 | 状态自身决定转移 |
| 典型代码位置 | 工厂类 | 上下文类 | 状态机/状态类 |
| 与if-else的关系 | 替代new判断 | 替代行为分支 | 替代状态迁移分支 |
这张表在我的团队里被反复引用。当有人把策略类塞进工厂里,或者用策略模式去实现状态机时,我就会让他看一眼这张表,大部分争议基本当场平息。
6. Java 8+之后,策略模式可以写得更轻:Lambda、方法引用与Map查表
关于策略模式,很多老教材的示例都是C++或Java 5时代的风格:每个策略一个类,每个类还要override方法。到了Java 8以后,函数式编程的引入把策略模式的编码成本大幅压缩。如果你还在为“为一个小算法写一个类”这件事觉得重,那这节内容值得仔细看。
6.1 用Lambda表达式替代具体策略类
策略接口如果只有一个抽象方法,它天然就是函数式接口,例如:
@FunctionalInterface public interface DiscountStrategy { BigDecimal apply(BigDecimal originPrice, OrderContext context); }那就可以直接用Lambda定义策略,而不需要新建类文件:
DiscountStrategy silverStrategy = (price, context) -> price.multiply(BigDecimal.valueOf(0.98)); DiscountStrategy goldStrategy = (price, context) -> price.multiply(BigDecimal.valueOf(0.95));把它们攒进Map,代码一下子轻巧了很多:
Map<String, DiscountStrategy> strategyMap = Map.of( "SILVER", (price, context) -> price.multiply(BigDecimal.valueOf(0.98)), "GOLD", (price, context) -> price.multiply(BigDecimal.valueOf(0.95)), "DIAMOND", (price, context) -> price.multiply(BigDecimal.valueOf(0.88)) );很多团队的代码里没有真正的策略类,但仍能享受策略模式的收益。关键是有那个抽象接口和选择入口,至于策略是类还是Lambda,本质上只是语法层面的差异。对入门者来说,我建议先把类写一遍,再用Lambda替换,体会一下“代码从膨胀到轻巧”的过程,比直接抄Lambda写法更有学习价值。
6.2 用方法引用直接绑定已有逻辑
如果系统里已经有一个工具类或者POJO里定义了现成的计算方法,方法引用能让策略代码更简洁,也更贴近业务命名:
public final class DiscountRules { public static BigDecimal silverRule(BigDecimal price, OrderContext ctx) { return price.multiply(BigDecimal.valueOf(0.98)); } public static BigDecimal goldRule(BigDecimal price, OrderContext ctx) { return price.multiply(BigDecimal.valueOf(0.95)); } } Map<String, DiscountStrategy> strategies = Map.of( "SILVER", DiscountRules::silverRule, "GOLD", DiscountRules::goldRule );这种写法最大的优点是,把策略和规则方法绑定在同一个注册位置,跳转起来非常快。如果后续某个规则从一行变成一百行,再把这个方法重构成类即可,调用方不会有任何感知。方法引用的出现,让“策略即方法”这句话在Java生态里第一次真正成立。
6.3 用Map代替switch分支的边界与代价
用Map查表替代switch和一堆if-else,确实能把可读性和可维护性提升一大截,但也要注意代价。Map的key如果是字符串,拼错时就会在运行时静默失败。如果整个Map的初始化逻辑集中在一个类里,它的规模就是所有策略的总和,代码会慢慢变成“策略大杂烩”。
我的经验是,当策略数量超过二十个时,手动一条条put的静态Map本身就会散发出坏味道。这时候应该考虑两种替代方案:第一,用反射扫描注解,让策略自动注册;第二,如果项目用了Spring,直接利用依赖注入把所有策略Bean注入到一个Map里,key就是Bean名称或注解值,业务侧按需取用。
@Component public class DiscountStrategyRegistry { private final Map<String, DiscountStrategy> strategyMap; public DiscountStrategyRegistry(Map<String, DiscountStrategy> strategyMap) { this.strategyMap = strategyMap; } public DiscountStrategy get(String name) { return strategyMap.getOrDefault(name, new NormalDiscountStrategy()); } }Spring会把容器里所有DiscountStrategy类型的Bean收集到这个Map中,key默认是Bean的name。新增一个策略只需要写一个类并打上Component注解,注册表代码一行都不用改。这才是策略模式在现代框架下的标准打开方式之一,这个系列虽然以纯Java为主,但这种真实项目里的写法已经无法回避。
7. 实践总结与避坑指南:什么时候该用,什么时候该收手
写到这里,策略模式的核心已经讲完了,但我知道所有人最关心的问题是这个模式在实践中到底容不容易翻车。设计模式不是越多越好,尤其是策略模式这种“看着就优雅”的东西,更容易被滥用。
7.1 策略类膨胀是它最真实的代价
每新增一种会员等级,就要新增一个策略类,如果系统里有几十种促销玩法,类数量就会翻倍甚至翻三倍,策略类、注册表、工厂各来一套。代码评审里经常看到“策略模式用多了导致类爆炸”的批评,这并非模式本身的错,而是没有选对粒度。如果两个策略之间的差异只是一行乘法的系数,那么把它们合并成一个带参数的策略类,并让注册表保存参数,可能比硬生生拆出十个类更合理。
public class FixedRateDiscountStrategy implements DiscountStrategy { private final BigDecimal rate; public FixedRateDiscountStrategy(BigDecimal rate) { this.rate = rate; } @Override public BigDecimal apply(BigDecimal originPrice, OrderContext context) { return originPrice.multiply(rate); } }银卡、金卡、钻石会员都可以统一看成“创建一个带rate参数的对象”,三个策略类在这里就应该合并成这一个类,通过构造参数区分。设计模式的运用永远要落脚到“消除重复、控制复杂度”上,而不是为了让代码显得很牛。
7.2 隐含的陷阱:策略内部持有状态与执行顺序
策略模式在使用过程中有两个隐含坑最容易被踩。第一个是策略对象的无状态假设。如果策略实例被多个线程或多次请求复用,而它内部有一个非线程安全的成员变量,就会出现“这次请求用了别人的折扣”这类神秘bug。我排查过一起线上计费异常,最后定位到就是一个策略实现里保存了上一次调用时设置的缓存值,多线程一并发就串了。解决办法是把策略设计成无状态;如果必须传值,就通过方法参数传入,而不是用字段保存。
第二个坑是策略链中替换顺序的隐式约定。当多个策略按顺序叠加时,不同的顺序会直接影响结果。第三节中的编排器就是为这个问题而设计的,但很多人第一次上手时会把顺序写死在服务方法里,等运营要调整顺序,又不得不重新动代码。所以,进入策略模式的同时,建议把“策略集合”建模成一级对象,通过配置或调用方传入,避免在上下文类里把顺序写死。
7.3 识别“过度设计”的三个信号
我能给出的判断标准是:如果没有可替换的多个算法,就不要用策略模式;如果策略之间没有共享接口行为,也不要用策略模式;如果“选策略”的逻辑比“执行策略”的逻辑还复杂,更要慎重。第三个信号最反直觉:策略模式会让“选谁”从原来的几个if变成大量注册表代码,要是这个选择逻辑的复杂度盖过了策略本身的实现,那说明你的业务更适合用规则引擎或状态机,而不是策略模式。
我在实际项目里最常见到的一种不当使用,是为了替换一个只有一个分支的if,把代码包装成三层结构。这样的重构,事后看复杂度并没有下降,反而让阅读者需要跨越更多类才能理解一行业务。设计模式要解决的是“扩展的正确姿势”,不是面试表演。
7.4 最后分享一点实操心得
就我个人经验来说,策略模式在Java业务系统里最大的价值,不是让代码量变少,而是让不同的人可以在同一个代码库里并行开发不同算法而不互相打扰。团队里一个人负责银卡逻辑,另一个人负责钻石逻辑,他们各自只改自己的策略类,最后在注册表里汇合,合并冲突少得惊人。这一点在多人协作项目里的体感特别明显。如果团队里还没有统一的设计模式语言,从策略模式开始建立代码评审的讨论基础,是一个不错的选择。
这个系列的策略模式篇就写到这里。下一篇文章我会聊聊观察者模式或者模板方法模式,后者和策略模式在结构上是很好的对比。理解了它们的差异,设计模式才算真正从看懂到会用。如果你在实际项目里用策略模式踩过什么坑,或者对“什么时候该拆策略类、什么时候该合并”有自己的判断,欢迎在评论区聊。能说出具体场景的那种讨论,永远比抽象地争论“设计模式到底好不好”更有价值。