策略模式实战:从硬编码到灵活算法替换的设计模式指南

策略模式实战:从硬编码到灵活算法替换的设计模式指南 1. 从“硬编码”到“灵活切换”为什么我们需要策略模式如果你写过一段处理不同支付方式的代码或者遇到过根据用户等级计算不同折扣的逻辑那你大概率已经踩过“硬编码”的坑了。我刚开始做项目时就写过这样的代码一个巨大的if-else或者switch-case块里面塞满了各种业务逻辑。比如一个订单处理类里面既有支付宝支付的逻辑又有微信支付的逻辑还有银联支付的逻辑。当时觉得挺清晰改一个支付方式就在一个文件里改嘛。直到后来产品经理说我们要加一个“数字货币支付”我打开那个已经膨胀到几百行的类看着里面交织在一起的支付、风控、日志逻辑头都大了。更别提测试同学每次都要把整个支付流程重新测一遍因为任何改动都可能影响到其他看似无关的支付方式。这就是策略模式要解决的核心痛点将算法或行为从使用它的上下文Context中分离出来使得它们可以独立变化和替换。简单说就是把那些会变的部分比如不同的支付算法、不同的折扣计算规则抽出来封装成一个个独立的“策略”类。而那个使用策略的类上下文只需要知道有一个统一的策略接口可以调用就行了至于具体是哪个策略在干活它不关心。这种做法的好处是显而易见的。首先它符合“开闭原则”—— 对扩展开放对修改关闭。要加一个新的支付方式你只需要新增一个实现了支付策略接口的类然后在需要的地方注入这个新类的实例即可完全不用去修改原有的、已经稳定运行的订单处理类。其次它让代码的职责更清晰每个策略类只负责一件事符合“单一职责原则”。最后它极大地提升了代码的可测试性你可以单独测试每一个策略类而不用每次都启动一个庞大的、耦合度高的业务流程。在 Java 的日常开发中从集合排序的Comparator到 Spring 框架中各种Handler、Resolver策略模式的身影无处不在。它不是什么高深莫测的“银弹”而是一个朴实无华却极其实用的工具能帮你把那些容易变动的、复杂的业务逻辑管理得井井有条。接下来我们就从一个最经典的场景出发用 UML 类图理清它的骨架再用代码填满它的血肉。2. 策略模式的骨架一张图看懂所有角色理解一个设计模式最直观的方式就是看它的 UML 类图。它能清晰地展示各个参与者之间的关系比干巴巴的文字描述强得多。策略模式的类图非常简洁但清晰地定义了三种核心角色。2.1 核心角色拆解我们先来看一张标准的策略模式 UML 类图然后逐一解释注此处为文字描述类图结构实际应用中可使用 StarUML 等工具绘制------------------- ---------------------- | Context | | interface | |-------------------| | Strategy | | -strategy:Strategy|----|----------------------| |-------------------| | execute(): void | | setStrategy() | ---------------------- | executeStrategy()| ^ ------------------- | | ----------------------------------- | | --------------------- --------------------- | ConcreteStrategyA | | ConcreteStrategyB | |---------------------| |---------------------| | | | | |---------------------| |---------------------| | execute(): void | | execute(): void | --------------------- ---------------------1. 策略接口这是整个模式的基石通常命名为Strategy。它定义了一个所有具体策略都必须实现的方法比如execute()、calculate()或pay()。这个方法就是上下文Context与具体策略交互的唯一契约。接口的存在使得上下文可以面向接口编程而不依赖任何具体的实现。在 Java 中这就是一个普通的接口Interface。2. 具体策略类这些是策略接口的具体实现者比如ConcreteStrategyA、ConcreteStrategyB。每个类都封装了一个独立的、完整的算法或行为。例如AlipayStrategy实现了PaymentStrategy接口的pay方法里面是调用支付宝 SDK 的完整逻辑WechatPayStrategy则实现了微信支付的逻辑。它们是可被替换的“零件”。3. 上下文类这是策略模式的使用者通常命名为Context。它持有一个对策略接口的引用组合关系。上下文类本身并不实现具体的算法而是将工作“委托”给它所持有的策略对象。它通常提供两种方法setStrategy(Strategy strategy): 用于在运行时动态地切换策略。executeStrategy(): 调用所持有策略对象的接口方法。上下文类就像一个“插座”而具体策略类就是不同的“插头”。只要插头符合插座的标准实现策略接口就可以即插即用。2.2 关系与协作流程从类图中我们可以看到几个关键关系组合关系Context拥有一个Strategy。用空心菱形和实线表示体现了“整体与部分”的关系但部分可以独立于整体存在。这是策略模式的核心上下文拥有一个策略而不是继承自它。实现关系ConcreteStrategyA/B实现Strategy接口。用带空心三角的虚线表示。依赖关系Context的executeStrategy()方法内部会调用strategy.execute()这是一种使用上的依赖。它们的典型协作流程是客户端Client创建或获取一个具体的策略对象如new ConcreteStrategyA()。客户端创建一个上下文对象并通过setStrategy方法将上一步的策略对象设置给它。客户端调用上下文对象的executeStrategy方法。上下文对象将调用委托给其持有的策略对象执行具体的算法。这个流程的关键在于“运行时动态绑定”。在步骤2中我们可以传入ConcreteStrategyA也可以传入ConcreteStrategyB上下文的行为会随之改变而上下文的代码却无需任何修改。3. 从理论到实践一个完整的电商折扣计算案例光看类图可能还有点抽象我们用一个电商平台中常见的“折扣计算”场景来把代码写出来。假设我们有三种折扣策略无折扣、固定金额折扣、百分比折扣。3.1 定义策略接口与具体实现首先定义我们的策略接口。它只声明一个计算折扣后价格的方法。/** * 折扣策略接口 */ public interface DiscountStrategy { /** * 计算折扣后的价格 * param originalPrice 商品原价 * return 折扣后的价格 */ double calculateDiscount(double originalPrice); }接着实现三种具体的折扣策略。每个类都是一个独立的算法单元。/** * 无折扣策略 */ public class NoDiscountStrategy implements DiscountStrategy { Override public double calculateDiscount(double originalPrice) { // 直接返回原价 return originalPrice; } } /** * 固定金额折扣策略如满100减20 */ public class FixedAmountDiscountStrategy implements DiscountStrategy { private double discountAmount; public FixedAmountDiscountStrategy(double discountAmount) { if (discountAmount 0) { throw new IllegalArgumentException(折扣金额不能为负数); } this.discountAmount discountAmount; } Override public double calculateDiscount(double originalPrice) { double finalPrice originalPrice - discountAmount; // 防止折后价格为负数 return finalPrice 0 ? 0 : finalPrice; } } /** * 百分比折扣策略如打8折 */ public class PercentageDiscountStrategy implements DiscountStrategy { private double discountRate; // 折扣率如0.8代表8折 public PercentageDiscountStrategy(double discountRate) { if (discountRate 0 || discountRate 1) { throw new IllegalArgumentException(折扣率必须在(0, 1]区间内); } this.discountRate discountRate; } Override public double calculateDiscount(double originalPrice) { return originalPrice * discountRate; } }注意在具体策略类的构造方法或设置方法中进行参数校验如折扣金额非负、折扣率范围合理是非常好的实践。这能尽早暴露问题避免脏数据流入核心计算逻辑导致更隐蔽的错误。3.2 构建上下文与客户端调用现在创建我们的上下文类CheckoutContext结账上下文。它负责持有并使用折扣策略。/** * 结账上下文持有折扣策略 */ public class CheckoutContext { // 持有策略接口的引用 private DiscountStrategy discountStrategy; private double originalPrice; public CheckoutContext(double originalPrice) { this.originalPrice originalPrice; // 默认策略无折扣 this.discountStrategy new NoDiscountStrategy(); } /** * 动态设置折扣策略 */ public void setDiscountStrategy(DiscountStrategy discountStrategy) { if (discountStrategy null) { throw new IllegalArgumentException(折扣策略不能为空); } this.discountStrategy discountStrategy; } /** * 执行结账计算委托给具体的策略对象 */ public double checkout() { System.out.println(商品原价: originalPrice); double finalPrice discountStrategy.calculateDiscount(originalPrice); System.out.println(应用折扣后价格: finalPrice); return finalPrice; } // 也可以提供一个便捷方法一次性设置并计算 public double checkoutWithStrategy(DiscountStrategy strategy) { setDiscountStrategy(strategy); return checkout(); } }最后在客户端比如一个Main类或一个 Service 方法中我们可以看到策略模式的灵活性。public class StrategyPatternDemo { public static void main(String[] args) { // 场景1普通商品无折扣 CheckoutContext context1 new CheckoutContext(100.0); context1.checkout(); // 输出商品原价: 100.0应用折扣后价格: 100.0 System.out.println(---); // 场景2促销商品固定金额减20 CheckoutContext context2 new CheckoutContext(100.0); context2.setDiscountStrategy(new FixedAmountDiscountStrategy(20.0)); context2.checkout(); // 输出商品原价: 100.0应用折扣后价格: 80.0 System.out.println(---); // 场景3会员商品打8折 CheckoutContext context3 new CheckoutContext(100.0); // 使用便捷方法 double finalPrice context3.checkoutWithStrategy(new PercentageDiscountStrategy(0.8)); // 输出商品原价: 100.0应用折扣后价格: 80.0 System.out.println(---); // 场景4动态切换策略模拟根据用户等级实时改变折扣 CheckoutContext dynamicContext new CheckoutContext(200.0); // 初始为普通用户无折扣 dynamicContext.checkout(); // 用户升级为白银会员改为9折 dynamicContext.setDiscountStrategy(new PercentageDiscountStrategy(0.9)); dynamicContext.checkout(); // 遇到双十一活动额外满减50 dynamicContext.setDiscountStrategy(new FixedAmountDiscountStrategy(50.0)); dynamicContext.checkout(); // 这段代码展示了同一个上下文对象如何在不同时刻执行不同的算法而无需修改自身。 } }通过这个案例你可以清晰地看到隔离变化折扣计算逻辑的变化被封装在各自的*DiscountStrategy类中。易于扩展如果要新增一个“每满300减50”的阶梯折扣策略只需要新建一个StepDiscountStrategy类实现DiscountStrategy接口然后在客户端像使用其他策略一样使用它即可。CheckoutContext类一行代码都不用改。避免条件判断客户端代码里没有出现if (user.isVip()) { ... } else if (hasCoupon) { ... }这样的复杂分支。选择哪种策略的逻辑可以上移到更合适的层面如根据用户等级、活动类型在工厂或配置中决定。4. 策略模式的进阶应用与深度辨析掌握了基础用法后我们来看看策略模式在更复杂场景下的应用以及它和其他相似模式的区别。这能帮助你在实际架构设计中做出更合适的选择。4.1 与工厂模式结合策略的创建与管理在简单的 Demo 中我们在客户端main方法里直接new具体的策略对象。但在实际项目中策略对象的创建本身可能很复杂需要读取配置、依赖其他服务等并且我们可能希望集中管理这些策略的创建逻辑。这时策略模式常常与工厂模式特别是简单工厂或工厂方法模式结合使用。我们可以创建一个DiscountStrategyFactorypublic class DiscountStrategyFactory { // 这里可以使用Map缓存策略实例避免重复创建如果是无状态的策略类 private static final MapString, DiscountStrategy STRATEGY_MAP new HashMap(); static { // 初始化策略池key可以是策略类型编码如从数据库或配置中心读取 STRATEGY_MAP.put(NO_DISCOUNT, new NoDiscountStrategy()); STRATEGY_MAP.put(FIXED_10, new FixedAmountDiscountStrategy(10.0)); STRATEGY_MAP.put(PERCENTAGE_80, new PercentageDiscountStrategy(0.8)); // 可以很方便地在这里添加新的策略 STRATEGY_MAP.put(NEW_YEAR_50, new FixedAmountDiscountStrategy(50.0)); } /** * 根据策略标识获取策略实例 * param strategyKey 策略标识 * return 对应的策略对象 */ public static DiscountStrategy getStrategy(String strategyKey) { DiscountStrategy strategy STRATEGY_MAP.get(strategyKey); if (strategy null) { throw new IllegalArgumentException(未找到对应的折扣策略: strategyKey); // 或者返回一个默认策略如 NoDiscountStrategy } return strategy; } // 更动态的方式根据配置信息创建策略适合需要参数的策略 public static DiscountStrategy createStrategy(String type, MapString, Object params) { switch (type) { case FIXED: Double amount (Double) params.get(amount); return new FixedAmountDiscountStrategy(amount); case PERCENTAGE: Double rate (Double) params.get(rate); return new PercentageDiscountStrategy(rate); // ... 其他类型 default: return new NoDiscountStrategy(); } } }这样客户端代码就变得更简洁且策略的创建逻辑被隔离了public class OrderService { public void processOrder(Order order) { // 根据订单中的活动编码决定使用哪种策略 String discountCode order.getDiscountCode(); DiscountStrategy strategy DiscountStrategyFactory.getStrategy(discountCode); CheckoutContext context new CheckoutContext(order.getTotalAmount()); context.setDiscountStrategy(strategy); double finalAmount context.checkout(); order.setFinalAmount(finalAmount); // ... 后续持久化等操作 } }实操心得对于无状态的策略对象即不包含成员变量或成员变量是常量强烈建议在工厂中使用缓存如Map或ConcurrentHashMap返回单例实例。这能减少大量小对象的创建和 GC 压力。对于有状态的策略如折扣金额需要从外部传入则每次可能需要创建新实例或者使用原型模式。4.2 策略模式 vs. 状态模式意图决定结构策略模式和状态模式在 UML 类图上看起来几乎一模一样都是一个上下文持有某个接口的引用接口有多个具体实现。这让很多初学者感到困惑。它们的核心区别在于意图策略模式客户端主动选择并配置策略。策略之间是平行的、可互换的它们代表完成同一任务的不同算法。客户端很清楚自己为什么要从策略A切换到策略B例如从支付宝支付切换到微信支付。策略对象通常不知道其他策略的存在。状态模式状态之间的转换由状态对象自身或上下文驱动是自动的、基于内部条件的。状态代表对象所处的状况状态转换是状态机的一部分。一个状态对象通常知道它的下一个可能状态是什么。例如一个订单对象从“待支付”状态在收到支付成功通知后会自动转换到“已支付”状态这个转换逻辑可能封装在“待支付”这个状态类里。一个简单的对比表格特性策略模式状态模式核心目的封装可互换的算法让客户端灵活选择封装与对象状态相关的行为让状态转换显得自然谁控制切换客户端或外部配置状态对象自身或上下文基于内部事件关系认知策略之间通常相互独立不知晓彼此状态之间相互知晓共同构成一个状态机典型场景支付方式选择、排序算法、压缩算法、折扣计算订单状态流转、TCP连接状态、游戏角色状态 idle, run, attack4.3 策略模式 vs. 简单工厂模式创建与使用的分离另一个容易混淆的是简单工厂模式。简单工厂负责创建对象它根据传入的参数返回不同的产品实例。而策略模式关注的是使用对象它定义了一系列算法并使其可以互换。在实践中它们经常协同工作如上文所述简单工厂负责生产具体的策略对象策略模式则负责使用这些对象。工厂解决了“怎么来”的问题策略模式解决了“怎么用”的问题。将两者结合客户端代码只需要和工厂交互获取合适的策略然后交给上下文使用进一步降低了耦合。5. 策略模式在真实项目中的落地与避坑指南理论很美好但落地到真实的、复杂的业务系统中策略模式会遇到一些特有的挑战。下面分享几个我踩过的坑和总结的经验。5.1 策略的发现与注册告别硬编码的工厂在 4.1 节的工厂示例中我们是在静态代码块里手动将策略注册到Map中的。当策略数量很多比如有几十种营销活动或者策略需要动态增删比如由运营人员在后台配置新活动时这种硬编码的方式就变得难以维护。这时我们可以利用Spring Framework 的依赖注入和接口发现机制来实现更优雅的策略管理。方法一使用 Service 和 Autowired 注入 Map (Spring)// 1. 定义策略接口 public interface RewardStrategy { String getType(); void issueReward(User user); } // 2. 具体策略实现使用 Service 注解并通过 PostConstruct 注册自己 Service public class PointRewardStrategy implements RewardStrategy { Override public String getType() { return POINT; } Override public void issueReward(User user) { // 发放积分逻辑 System.out.println(向用户 user.getName() 发放积分); } } Service public class CouponRewardStrategy implements RewardStrategy { Override public String getType() { return COUPON; } Override public void issueReward(User user) { // 发放优惠券逻辑 System.out.println(向用户 user.getName() 发放优惠券); } } // 3. 策略上下文与工厂 Service public class RewardStrategyContext { // Spring会自动将RewardStrategy的所有实现类注入到这个Map中 // Key是Bean的名字Value是Bean的实例。我们可以改造一下让Key是我们的策略类型。 Autowired private final MapString, RewardStrategy strategyMap new HashMap(); // 或者更优雅的方式在构造方法或 PostConstruct 中初始化一个以 getType() 为key的Map private MapString, RewardStrategy typeStrategyMap; Autowired public RewardStrategyContext(ListRewardStrategy strategies) { typeStrategyMap strategies.stream() .collect(Collectors.toMap(RewardStrategy::getType, Function.identity())); } public RewardStrategy getStrategy(String rewardType) { RewardStrategy strategy typeStrategyMap.get(rewardType); if (strategy null) { throw new IllegalArgumentException(不支持的奖励类型: rewardType); } return strategy; } public void executeReward(String rewardType, User user) { RewardStrategy strategy getStrategy(rewardType); strategy.issueReward(user); } }这种方式的好处是新增一个策略时你只需要新建一个类实现RewardStrategy接口并加上Service注解它就会被自动扫描并注册到上下文的Map中。完全无需修改工厂类或任何其他现有代码完美符合开闭原则。方法二使用自定义注解与 ApplicationContextAware如果策略类型不是通过接口方法getType()获取而是想通过自定义注解来标记也可以实现。// 自定义注解 Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) Component // 继承Component使其能被扫描 public interface RewardStrategyType { String value(); } // 策略实现类使用注解 RewardStrategyType(POINT) Service public class PointRewardStrategy implements RewardStrategy { ... } RewardStrategyType(COUPON) Service public class CouponRewardStrategy implements RewardStrategy { ... } // 在工厂/上下文中通过ApplicationContext获取所有带有该注解的Bean Service public class AnnotatedRewardStrategyFactory implements ApplicationContextAware { private ApplicationContext applicationContext; private MapString, RewardStrategy strategyMap; Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext applicationContext; initStrategyMap(); } private void initStrategyMap() { strategyMap new HashMap(); // 获取所有带有RewardStrategyType注解的Bean MapString, Object beansWithAnnotation applicationContext.getBeansWithAnnotation(RewardStrategyType.class); for (Object bean : beansWithAnnotation.values()) { if (bean instanceof RewardStrategy) { RewardStrategyType annotation bean.getClass().getAnnotation(RewardStrategyType.class); strategyMap.put(annotation.value(), (RewardStrategy) bean); } } } // ... getStrategy 方法 }5.2 策略模式并非银弹识别其适用边界策略模式很好但不能滥用。以下情况可能不适合使用策略模式或者需要变通策略数量极少且稳定如果只有一两种算法且未来几乎不可能变化直接使用条件判断if-else可能更简单直接。引入策略模式会增加类的数量带来一定的复杂度。客户端必须知晓所有策略如果使用策略的客户端代码需要根据复杂的业务逻辑从大量策略中选出一个这个选择逻辑本身可能又变得复杂。此时可以考虑再引入一层“元策略”或“策略选择器”来封装这部分逻辑。策略对象需要共享大量数据如果所有策略都需要访问上下文中的大量数据那么策略接口的方法签名可能会变得很臃肿需要传入很多参数。这时可以考虑将上下文对象本身作为参数传递给策略方法或者重新审视职责划分看是否有些数据应该放在策略对象内部初始化。算法非常简单如果算法只是一两行简单的计算为其单独创建一个类可能显得“杀鸡用牛刀”。但在团队协作和长期维护的视角下即使简单的算法如果它有变化的可能用策略模式隔离它也是值得的这能提高代码的可测试性。5.3 性能考量与最佳实践策略对象的创建开销如果策略对象是无状态的务必将其设计为单例并通过工厂缓存复用避免频繁的 GC。Spring 中默认的ServiceBean 就是单例的。选择策略的开销如果策略选择逻辑如if-else链或switch被频繁调用例如在循环中或高并发接口中需要关注其性能。通常使用Map进行O(1)复杂度的查找是最优的。避免在热点代码中使用反射来动态创建策略。清晰的命名策略接口和具体实现类的命名要能清晰表达其意图。例如SortStrategy比Strategy好QuickSortStrategy比ConcreteStrategyA好得多。使用枚举管理策略类型在客户端或工厂中使用枚举来定义策略类型而不是字符串常量可以利用编译时检查避免拼写错误。public enum DiscountType { NO_DISCOUNT, FIXED_AMOUNT, PERCENTAGE } // 在工厂中 public DiscountStrategy getStrategy(DiscountType type) { ... }策略模式是提升代码弹性、应对变化的利器。它通过“组合优于继承”的原则将易变的算法部分解耦出来。当你发现代码中出现了多个并列的、可能变化的逻辑分支时就应该考虑是否可以用策略模式来重构了。记住设计模式的目标不是让代码变得更复杂而是让它在面对未来变化时能够更从容、更稳定。