策略模式实战:从硬编码到灵活算法替换的设计模式指南
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具体的策略对象。但在实际项目中,策略对象的创建本身可能很复杂(需要读取配置、依赖其他服务等),并且我们可能希望集中管理这些策略的创建逻辑。这时,策略模式常常与工厂模式(特别是简单工厂或工厂方法模式)结合使用。
我们可以创建一个DiscountStrategyFactory:
public class DiscountStrategyFactory { // 这里可以使用Map缓存策略实例,避免重复创建(如果是无状态的策略类) private static final Map<String, 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, Map<String, 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, attack) |
4.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 Map<String, RewardStrategy> strategyMap = new HashMap<>(); // 或者,更优雅的方式:在构造方法或 @PostConstruct 中初始化一个以 getType() 为key的Map private Map<String, RewardStrategy> typeStrategyMap; @Autowired public RewardStrategyContext(List<RewardStrategy> 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 Map<String, RewardStrategy> strategyMap; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; initStrategyMap(); } private void initStrategyMap() { strategyMap = new HashMap<>(); // 获取所有带有RewardStrategyType注解的Bean Map<String, 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) { ... }
策略模式是提升代码弹性、应对变化的利器。它通过“组合优于继承”的原则,将易变的算法部分解耦出来。当你发现代码中出现了多个并列的、可能变化的逻辑分支时,就应该考虑是否可以用策略模式来重构了。记住,设计模式的目标不是让代码变得更复杂,而是让它在面对未来变化时,能够更从容、更稳定。