Strategy Pattern:用接口替代 if-else

1. 概述

冗长的 if-else 或 switch 语句用于根据类型选择行为是代码即将难以维护的信号。每次添加新的类型,你都必须修改同一个函数,违反了开闭原则(Open/Closed Principle)。这篇文章将向你展示如何使用 Strategy Pattern 将每种行为提取到单独的类中,实现相同的接口,然后由 Spring 自动选择合适的 strategy 来替代你写的 if-else 语句。读完之后,你将能够自己重构一个混乱的费用/折扣计算 service。

2. 问题:if-else 随时间膨胀

假设你有一个 PaymentService 根据支付方式计算交易费用:

public class PaymentService {

    public double calculateFee(String paymentType, double amount) {
        if (paymentType.equals("CREDIT_CARD")) {
            return amount * 0.03;
        } else if (paymentType.equals("BANK_TRANSFER")) {
            return amount * 0.01;
        } else if (paymentType.equals("E_WALLET")) {
            return amount * 0.015;
        } else {
            throw new IllegalArgumentException("Unsupported type: " + paymentType);
        }
    }
}

问题:

  • 新增支付方式(如 CRYPTO)必须直接修改此方法。
  • 每种类型的费用计算逻辑混在一个函数中,难以独立测试。
  • 使用 String 比较容易导致拼写错误,没有编译时检查。

3. 解决方案:定义一个 Strategy 接口

核心思想:将“根据类型变化的行为”提取成一个接口,每种类型对应一个独立的实现。

public interface FeeStrategy {
    double calculateFee(double amount);
}

现在每个 strategy 都是一个独立的类,易于阅读和测试:

public class CreditCardFeeStrategy implements FeeStrategy {
    @Override
    public double calculateFee(double amount) {
        return amount * 0.03;
    }
}
public class BankTransferFeeStrategy implements FeeStrategy {
    @Override
    public double calculateFee(double amount) {
        return amount * 0.01;
    }
}
public class EWalletFeeStrategy implements FeeStrategy {
    @Override
    public double calculateFee(double amount) {
        return amount * 0.015;
    }
}

PaymentService 现在只需接收一个 FeeStrategy 并调用它,无需了解内部逻辑:

public class PaymentService {

    private final FeeStrategy feeStrategy;

    public PaymentService(FeeStrategy feeStrategy) {
        this.feeStrategy = feeStrategy;
    }

    public double calculateFee(double amount) {
        return feeStrategy.calculateFee(amount);
    }
}
  • feeStrategy 通过构造函数注入,PaymentService 不需要知道它是 CreditCardFeeStrategy 还是其他类型。
  • 新增支付方式 = 新增一个实现 FeeStrategy 的类,无需修改 PaymentService。

4. 正确选择 strategy:避免 if-else 出现在其他地方

常见问题:你从 PaymentService 中删除了 if-else,但却在调用 service 的地方重建了它。为了避免这种情况,使用 Map<String, FeeStrategy> 作为注册表,在初始化时按 key 注册 strategy(工厂模式),避免在代码中到处都是 if-else。

public class FeeStrategyFactory {

    private final Map<String, FeeStrategy> strategies;

    public FeeStrategyFactory() {
        this.strategies = Map.of(
            "CREDIT_CARD", new CreditCardFeeStrategy(),
            "BANK_TRANSFER", new BankTransferFeeStrategy(),
            "E_WALLET", new EWalletFeeStrategy()
        );
    }

    public FeeStrategy resolve(String paymentType) {
        FeeStrategy strategy = strategies.get(paymentType);
        if (strategy == null) {
            throw new IllegalArgumentException("Unsupported type: " + paymentType);
        }
        return strategy;
    }
}
  • strategies 是一个不可变的 map,在工厂初始化时构建一次。
  • resolve() 是仅有的一个“条件分支”点 — 但它只是做 map 查找,不包含业务逻辑。真正的逻辑在每个 strategy 中。

试试看:

FeeStrategyFactory factory = new FeeStrategyFactory();
FeeStrategy strategy = factory.resolve("E_WALLET");
PaymentService service = new PaymentService(strategy);

double fee = service.calculateFee(1_000_000);
System.out.println(fee);
15000.0

注意: 如果 strategy 列表是固定的且很少变化(2-3 种),简单的 if-else 仍然可以。Strategy Pattern 在 strategy 数量逐渐增加或每个 strategy 的逻辑很复杂时才真正发挥价值。

5. 利用 Spring:让容器自动注入 strategy 列表

在 Spring Boot 中,你不需要手动写 Map。将每个 strategy 标记为 @Component,Spring 会自动收集所有实现相同接口的 bean 到一个 List 中。

public interface FeeStrategy {
    String getPaymentType();
    double calculateFee(double amount);
}

添加 getPaymentType() 以便每个 strategy 声明它服务的类型:

@Component
public class CreditCardFeeStrategy implements FeeStrategy {

    @Override
    public String getPaymentType() {
        return "CREDIT_CARD";
    }

    @Override
    public double calculateFee(double amount) {
        return amount * 0.03;
    }
}

工厂现在接收 Spring 注入的 List<FeeStrategy>,通过构造函数构建 map:

@Component
public class FeeStrategyFactory {

    private final Map<String, FeeStrategy> strategies = new HashMap<>();

    public FeeStrategyFactory(List<FeeStrategy> strategyList) {
        for (FeeStrategy strategy : strategyList) {
            strategies.put(strategy.getPaymentType(), strategy);
        }
    }

    public FeeStrategy resolve(String paymentType) {
        FeeStrategy strategy = strategies.get(paymentType);
        if (strategy == null) {
            throw new IllegalArgumentException("Unsupported type: " + paymentType);
        }
        return strategy;
    }
}
  • Spring 扫描所有实现 FeeStrategy 的 bean,按初始化顺序注入到 strategyList。
  • 构造函数在 FeeStrategyFactory 创建时构建一次 map — 整个 strategy 选择过程中没有任何 if-else。
  • 新增 CryptoFeeStrategy:只需创建该类并添加 @Component 注解,Spring 会自动识别。FeeStrategyFactory 无需重建。

PaymentService 通过 @Service 使用这个工厂:

@Service
public class PaymentService {

    private final FeeStrategyFactory feeStrategyFactory;

    public PaymentService(FeeStrategyFactory feeStrategyFactory) {
        this.feeStrategyFactory = feeStrategyFactory;
    }

    public double calculateFee(String paymentType, double amount) {
        FeeStrategy strategy = feeStrategyFactory.resolve(paymentType);
        return strategy.calculateFee(amount);
    }
}

6. 为每个 strategy 和 factory 编写测试

因为每个 strategy 都是独立的类,你可以不需要任何 mock 就能单独测试它:

class CreditCardFeeStrategyTest {

    private final CreditCardFeeStrategy strategy = new CreditCardFeeStrategy();

    @Test
    void shouldCalculate3PercentFee() {
        double fee = strategy.calculateFee(1_000_000);

        assertThat(fee).isEqualTo(30_000.0);
    }
}

工厂的测试确保“选择 strategy”的逻辑正常工作,不会出错:

class FeeStrategyFactoryTest {

    private final FeeStrategyFactory factory = new FeeStrategyFactory(
        List.of(new CreditCardFeeStrategy(), new EWalletFeeStrategy())
    );

    @Test
    void shouldResolveCorrectStrategy() {
        FeeStrategy strategy = factory.resolve("E_WALLET");

        assertThat(strategy).isInstanceOf(EWalletFeeStrategy.class);
    }

    @Test
    void shouldThrowWhenTypeNotSupported() {
        assertThatThrownBy(() -> factory.resolve("CRYPTO"))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("CRYPTO");
    }
}
  • shouldResolveCorrectStrategy 确认工厂根据 key 返回正确的 strategy 类型。
  • shouldThrowWhenTypeNotSupported 确保在没有找到 strategy 时仍会抛出清晰的异常,与原来的 else 行为相同 — 但现在这个逻辑集中在一个地方。

7. 使用 Enum 替代 String key(更安全的选择)

如果你担心 String key 的拼写错误,可以使用 enum 作为 map 的 key,让编译器及早发现错误:

public enum PaymentType {
    CREDIT_CARD, BANK_TRANSFER, E_WALLET
}
public interface FeeStrategy {
    PaymentType getPaymentType();
    double calculateFee(double amount);
}

这种方法保持上述所有架构不变,只是将 map 的 key 类型改为 Map<PaymentType, FeeStrategy>。IDE 会提供完整的 enum 值提示,避免输入错误的字符串。

8. 何时不应使用 Strategy Pattern

  • 只有 2 个分支逻辑,扩展可能性几乎为零 — 一个简单的 if-else 比创建 2-3 个类更易读。
  • 各分支的逻辑几乎相同,只是参数不同 — 使用 strategy 会创建大量几乎重复的类。在这种情况下,使用配置参数的 enum 通常更简洁。
  • 行为的选择不是基于固定的“类型”,而是依赖许多复杂的运行时条件(多个条件组合) — Strategy Pattern 可能不够用,你应该考虑 Chain of Responsibility 或 rule engine。

提示: 不要仅因为某个 pattern“看起来更专业”就应用它。只在你看到分支数量会随时间增加或每个分支的逻辑足够复杂需要独立测试时才使用。

9. 结论

Strategy Pattern 完美解决了冗长 if-else 的问题:将根据类型变化的行为提取到单独的类中,都实现相同的接口。在 Spring Boot 中,你可以利用注入 List<Bean> 的机制自动收集 strategy,无需手动管理注册表。结果是:添加新的类型无需修改旧代码,每个 strategy 都能轻松独立测试,而“选择 strategy”的逻辑只在一个地方 — 一个 map 查找,不会有 if-else 分散在整个代码库中。

design-patternjavaspring-bootclean-code