Skip to content

策略模式 (Strategy) —— 条条大路通罗马

前言

在深入探讨策略模式之前,想向大家推荐一个非常棒的开源项目:design-patterns-23。这个项目用最现代的技术栈重写了 23 种设计模式,非常适合实战学习,还配套了在线交互演示站,可以边读文章边动手玩。本文的实战案例灵感也来源于此。

1. 策略模式是什么?

策略模式(Strategy Pattern)定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。

简单来说,策略模式就是准备一组算法,在运行时选择其中一个来使用

1.1 核心概念

策略模式主要由三个角色组成:

  • Context(环境类):它持有一个 Strategy 类的引用,负责将客户端的请求委托给具体的策略对象。它是客户端和具体策略之间的桥梁。
  • Strategy(抽象策略):定义了所有支持的算法的公共接口。Context 只依赖这个接口,而不依赖具体的实现。
  • ConcreteStrategy(具体策略):实现了 Strategy 接口,提供具体的算法实现。比如“满减算法”、“打折算法”等。

1.2 为什么我们需要它?

场景: 在电商系统中,支付促销是两个典型的策略模式应用场景。 比如用户结账时,可以选择支付宝微信支付或者银联云闪付; 又或者在双十一大促期间,商品可能有多种优惠方式:满 100 减 20全场 8 折、或者VIP 会员专享价

痛点: 如果不使用策略模式,你的代码里往往会充斥着大量的 if-elseswitch 语句。

java
public double calculatePrice(String type, double price) {
    if (type.equals("MAN_JIAN")) {
        return price - 20;
    } else if (type.equals("DISCOUNT")) {
        return price * 0.8;
    } else if (type.equals("VIP")) {
        // VIP 逻辑...
    }
    return price;
}

这种写法有两个致命缺点: 第一,违反开闭原则。每次运营想搞个新活动(比如“第二件半价”),你都得去修改 calculatePrice 的源码,重新编译部署,风险极大。 第二,代码臃肿。如果促销逻辑很复杂,这个方法会变得巨长无比,难以维护。

解决方案: 把每种计算逻辑(算法)封装成一个独立的类,客户端想用哪个就传哪个进去。这样新增一种策略只需要新增一个类,不需要修改原有代码。

2. Java 实战:电商促销引擎

我们来实现一个灵活的打折引擎。

2.1 标准实现

Step 1: 定义策略接口

java
public interface DiscountStrategy {
    // 计算打折后的价格
    double calculate(double originalPrice);
}

Step 2: 实现具体策略

满减策略

java
public class ManJianStrategy implements DiscountStrategy {
    @Override
    public double calculate(double originalPrice) {
        System.out.println("执行满减策略:满100减20");
        return originalPrice >= 100 ? originalPrice - 20 : originalPrice;
    }
}

折扣策略

java
public class DiscountStrategyImpl implements DiscountStrategy {
    @Override
    public double calculate(double originalPrice) {
        System.out.println("执行折扣策略:全场8折");
        return originalPrice * 0.8;
    }
}

无优惠策略

java
public class NoDiscountStrategy implements DiscountStrategy {
    @Override
    public double calculate(double originalPrice) {
        System.out.println("无优惠");
        return originalPrice;
    }
}

Step 3: 上下文 (Context)

java
public class PriceCalculator {
    private DiscountStrategy strategy;

    // 构造注入,或者 setter 注入
    public PriceCalculator(DiscountStrategy strategy) {
        this.strategy = strategy;
    }

    public void setStrategy(DiscountStrategy strategy) {
        this.strategy = strategy;
    }

    public double getFinalPrice(double originalPrice) {
        return strategy.calculate(originalPrice);
    }
}

Step 4: 客户端

java
public class Client {
    public static void main(String[] args) {
        // 1. 周末活动,全场8折
        PriceCalculator calculator = new PriceCalculator(new DiscountStrategyImpl());
        System.out.println("最终价格:" + calculator.getFinalPrice(100));

        // 2. 平日活动,满减
        calculator.setStrategy(new ManJianStrategy());
        System.out.println("最终价格:" + calculator.getFinalPrice(100));
    }
}

3. 进阶技巧:如何消除客户端的 if-else?

虽然我们把算法封装了,但在 Client 端(或者 Controller 层),通常还是需要根据前端传来的 type 参数来决定 new 哪个策略类。

java
// Controller 层可能还是这样:
if (type.equals("VIP")) {
    calculator.setStrategy(new VipStrategy());
} else if ...

这就又回到了起点!怎么彻底消灭 if-else

解决方案:策略工厂 + Map 注册表

java
import java.util.HashMap;
import java.util.Map;

public class StrategyFactory {
    private static final Map<String, DiscountStrategy> strategyMap = new HashMap<>();

    static {
        // 实际项目中,这里可以使用 Spring 的 @Autowired 自动注入所有实现类,放入 Map
        strategyMap.put("MAN_JIAN", new ManJianStrategy());
        strategyMap.put("DISCOUNT", new DiscountStrategyImpl());
        strategyMap.put("NO_DISCOUNT", new NoDiscountStrategy());
    }

    public static DiscountStrategy getStrategy(String type) {
        return strategyMap.getOrDefault(type, new NoDiscountStrategy());
    }
}

现在的客户端代码

java
public void pay(String type, double price) {
    // 这一行代码彻底消灭了 if-else
    DiscountStrategy strategy = StrategyFactory.getStrategy(type);
    
    PriceCalculator calculator = new PriceCalculator(strategy);
    double finalPrice = calculator.getFinalPrice(price);
    System.out.println("最终价格:" + finalPrice);
}

结合 Spring 框架,我们可以利用 ApplicationContext 在启动时自动扫描所有实现了 DiscountStrategy 的 Bean,并根据 Bean Name 注册到 Map 中,实现真正的零修改扩展

4. Java 8 函数式编程实现

如果策略逻辑很简单,专门写一个类有点重。Java 8 的 Lambda 表达式让策略模式变得极其轻量。

java
public class Java8Strategy {
    public static void main(String[] args) {
        // 直接传入 Lambda 作为策略
        PriceCalculator calculator = new PriceCalculator(price -> price * 0.5);
        System.out.println("半价:" + calculator.getFinalPrice(100));

        // 另一种策略
        calculator.setStrategy(price -> price - 10);
        System.out.println("减10块:" + calculator.getFinalPrice(100));
    }
}

这里利用了 DiscountStrategy 是一个函数式接口(只有一个抽象方法)的特性。

5. 源码中的策略模式

5.1 JDK java.util.Comparator

这是最著名的策略模式。 Collections.sort(list, comparator)

  • list 是数据。
  • comparator 是策略。 你想怎么排,传不同的 Comparator 进去就行,sort 方法本身不用变。

5.2 JDK 线程池 ThreadPoolExecutor

线程池的拒绝策略 RejectedExecutionHandler。 当任务满了,是直接抛异常?还是丢弃?还是由调用线程执行?

  • AbortPolicy
  • CallerRunsPolicy
  • DiscardOldestPolicy ...这些都是策略。

5.3 Spring Resource

Spring 加载资源时,根据路径的不同前缀(classpath:, file:, http:),使用不同的 Resource 实现类来读取文件。这也是策略模式的一种应用。

6. 策略模式 vs 模板方法模式 vs 状态模式

模式核心区别举例
策略模式组合 (Composition)。Context 持有算法接口,运行时可以换。侧重于算法的选择排序算法(冒泡、快排)
模板方法继承 (Inheritance)。子类重写父类的特定步骤。结构固定,细节改变。也就是去银行办业务(取号、排队、办具体业务、评价)
状态模式状态流转。侧重于对象状态改变导致行为改变,通常状态会自动流转。订单状态(待支付 -> 已支付)

如何选择?

  • 如果你想重用一套算法骨架,只是改某个步骤 -> 模板方法
  • 如果你有一堆平等的算法,想在运行时随便切 -> 策略模式
  • 如果你的对象有状态,且不同状态下行为狂变 -> 状态模式

7. 总结

策略模式是开闭原则(OCP)的忠实实践者。

  • 它把变化的代码(算法)从不变的代码(客户端调用逻辑)中剥离出来。
  • 配合工厂模式Spring 容器,可以做到新增算法完全不改动旧代码。

8. 实验实操

design-patterns-web 项目中,我们通过一个支付方式选择的 Demo 来演示策略模式。

  • 策略切换:当你选择 "Credit Card"、"PayPal" 或 "Bank Transfer" 时,你实际上是在切换不同的支付策略 (PaymentStrategy)。
  • 表单渲染:注意观察,随着策略的切换,下方的表单内容会发生完全不同的变化(信用卡需要输入卡号,PayPal 提示跳转,银行转账显示账户信息)。这是因为不同的策略对象实现了不同的 render 逻辑。
  • 统一执行:点击支付按钮时,Context(页面组件)只需要调用当前策略的 pay() 方法,而不需要关心具体是哪种支付方式。 截图建议:选择 "Credit Card" 支付方式,截取包含上方选择器和下方信用卡输入表单的界面,展示策略切换带来的 UI 变化。