支付方式不断增加时,真正难维护的不是 if-else 本身,而是业务规则、依赖获取、异常处理和扩展入口被挤在同一个方法里。本文从一段常见代码出发,使用策略模式与注册表重构支付路由,并讨论如何避免过度设计。

支付方式只有两三种时,if-else 往往是最直接、也最容易读懂的方案。问题通常不是它一开始写得不好,而是业务增长之后,新的判断不断被追加进去:银行卡要校验限额,钱包支付要检查实名状态,积分支付要判断余额,某些渠道还需要查询外部服务。

最后,一个原本几十行的方法开始同时负责参数校验、支付方式选择、渠道调用、异常转换和日志记录。任何新增支付方式,都必须修改这个“大方法”,测试范围也随之扩大。

这篇文章选择一个具体问题:如何把不断膨胀的支付方式分支,重构成容易扩展、容易测试的策略结构。重点不在于套用一个设计模式,而在于重新划分职责,让变化集中在合适的位置。

一段正在变复杂的代码

下面是一段简化后的支付服务:

public class PaymentService {

    private final BankClient bankClient;
    private final WalletClient walletClient;
    private final PointClient pointClient;

    public PaymentService(BankClient bankClient,
                          WalletClient walletClient,
                          PointClient pointClient) {
        this.bankClient = bankClient;
        this.walletClient = walletClient;
        this.pointClient = pointClient;
    }

    public PaymentResult pay(PaymentRequest request) {
        if (request == null || request.amount() == null
                || request.amount().signum() <= 0) {
            throw new IllegalArgumentException("支付金额必须大于 0");
        }

        if ("BANK".equals(request.method())) {
            if (request.accountId() == null) {
                throw new IllegalArgumentException("银行卡账户不能为空");
            }
            return bankClient.pay(request.accountId(), request.amount());
        } else if ("WALLET".equals(request.method())) {
            if (request.accountId() == null) {
                throw new IllegalArgumentException("钱包账户不能为空");
            }
            return walletClient.pay(request.accountId(), request.amount());
        } else if ("POINT".equals(request.method())) {
            return pointClient.pay(request.accountId(), request.amount());
        }

        throw new UnsupportedOperationException("不支持的支付方式");
    }
}

这段代码并非立刻需要重构。它的问题在于,变化方向已经很清楚:支付方式增加时,PaymentService 会持续增加条件分支,并且每个分支都包含不同的校验和调用细节。

如果继续扩展,主流程会逐渐失去稳定性。一个新增的支付渠道,可能因为修改了共享校验、异常处理或日志逻辑,影响原来已经工作的渠道。这里真正需要隔离的,不只是“选择哪个分支”,而是每种支付方式自身的业务规则。

先确定稳定的抽象

支付方式虽然实现不同,但通常都有一个共同动作:接收支付请求,返回支付结果。因此可以先定义一个策略接口:

public interface PaymentStrategy {

    String method();

    PaymentResult pay(PaymentRequest request);
}

method() 是策略的唯一标识,pay() 承载该支付方式自己的处理流程。接口不应该暴露银行卡客户端、钱包客户端等具体依赖,否则调用方仍然需要知道策略内部的细节。

请求和结果可以使用简单的不可变对象:

import java.math.BigDecimal;

public record PaymentRequest(
        String method,
        String accountId,
        BigDecimal amount) {
}

public record PaymentResult(
        boolean success,
        String transactionId,
        String message) {

    public static PaymentResult success(String transactionId) {
        return new PaymentResult(true, transactionId, "支付成功");
    }

    public static PaymentResult failure(String message) {
        return new PaymentResult(false, null, message);
    }
}

这里使用了 record 来表达只读数据对象。若项目尚未使用支持 record 的 Java 版本,也可以用 final 字段、构造方法和访问器实现同样的不可变语义;设计重点并不依赖某个语法特性。

让每种策略只处理自己的规则

下面实现银行卡和钱包两种策略。外部客户端是接口,便于替换真实实现和编写单元测试。

import java.math.BigDecimal;

public interface BankClient {
    String pay(String accountId, BigDecimal amount);
}

public interface WalletClient {
    String pay(String accountId, BigDecimal amount);
}

public final class BankPaymentStrategy implements PaymentStrategy {

    private final BankClient bankClient;

    public BankPaymentStrategy(BankClient bankClient) {
        this.bankClient = bankClient;
    }

    @Override
    public String method() {
        return "BANK";
    }

    @Override
    public PaymentResult pay(PaymentRequest request) {
        requireAccount(request);
        String transactionId = bankClient.pay(
                request.accountId(), request.amount());
        return PaymentResult.success(transactionId);
    }

    private void requireAccount(PaymentRequest request) {
        if (request.accountId() == null
                || request.accountId().isBlank()) {
            throw new IllegalArgumentException("银行卡账户不能为空");
        }
    }
}

public final class WalletPaymentStrategy implements PaymentStrategy {

    private final WalletClient walletClient;

    public WalletPaymentStrategy(WalletClient walletClient) {
        this.walletClient = walletClient;
    }

    @Override
    public String method() {
        return "WALLET";
    }

    @Override
    public PaymentResult pay(PaymentRequest request) {
        if (request.accountId() == null
                || request.accountId().isBlank()) {
            throw new IllegalArgumentException("钱包账户不能为空");
        }
        String transactionId = walletClient.pay(
                request.accountId(), request.amount());
        return PaymentResult.success(transactionId);
    }
}

现在,银行卡规则的修改不会触碰钱包策略。每个策略的测试也可以围绕自己的输入、依赖调用和异常行为展开,而不是通过一个庞大的服务方法间接验证。

用注册表代替新的分支工厂

策略模式解决了“每种支付方式怎么做”,但还需要解决“根据标识找到哪个策略”。如果再写一个包含大量 if-else 的工厂,分支只是换了位置,并没有真正降低变化成本。

可以在组装阶段建立注册表:

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public final class PaymentService {

    private final Map<String, PaymentStrategy> strategies;

    public PaymentService(List<PaymentStrategy> strategyList) {
        Map<String, PaymentStrategy> index = new HashMap<>();
        for (PaymentStrategy strategy : strategyList) {
            String method = normalize(strategy.method());
            if (index.put(method, strategy) != null) {
                throw new IllegalArgumentException(
                        "重复的支付方式: " + method);
            }
        }
        this.strategies = Map.copyOf(index);
    }

    public PaymentResult pay(PaymentRequest request) {
        validateCommonRequest(request);

        String method = normalize(request.method());
        PaymentStrategy strategy = strategies.get(method);
        if (strategy == null) {
            throw new UnsupportedOperationException(
                    "不支持的支付方式: " + request.method());
        }
        return strategy.pay(request);
    }

    private void validateCommonRequest(PaymentRequest request) {
        if (request == null || request.amount() == null
                || request.amount().signum() <= 0) {
            throw new IllegalArgumentException("支付金额必须大于 0");
        }
    }

    private String normalize(String method) {
        if (method == null || method.isBlank()) {
            throw new IllegalArgumentException("支付方式不能为空");
        }
        return method.trim().toUpperCase();
    }
}

注册表的关键不是 Map,而是依赖方向发生了变化:PaymentService 只依赖 PaymentStrategy,不再直接依赖每个具体支付客户端。新增积分支付时,只需实现 PointPaymentStrategy,并在组装处加入实例,主流程不需要修改。

在 Spring 项目中,可以让各个策略成为组件,再通过构造器注入 List<PaymentStrategy>。不过要注意,自动扫描只是完成对象组装,不能替代业务设计。策略标识必须唯一,重复标识应在启动或构造阶段尽早失败,而不是等到线上请求才发现。

这次重构真正改善了什么

第一,变化被隔离了。支付方式自己的校验、客户端调用和结果转换,集中在自己的策略中。

第二,主流程更稳定。公共校验、策略查找和调用顺序清晰可见,不会随着渠道增加而不断嵌套。

第三,测试边界更自然。可以单独测试银行卡策略是否校验账户、是否正确调用客户端,也可以单独测试注册表是否拒绝重复标识。

第四,依赖更容易替换。策略依赖的是 BankClientWalletClient 这样的接口,真实 HTTP 客户端、测试替身和故障模拟实现可以分别提供。

常见坑:不是所有 if-else 都值得改

1. 为了使用模式而使用模式

如果系统永远只有两种支付方式,且规则非常简单,引入多个接口、注册表和类文件,可能只会增加阅读成本。重构的依据应该是变化频率、分支复杂度和测试困难程度,而不是看到 if-else 就机械替换。

2. 把所有公共逻辑都塞进接口默认方法

金额校验可以放在服务层,但“银行卡必须绑定某类账户”属于银行卡策略自己的规则。过度抽取公共方法,容易得到一个表面统一、内部充满特殊判断的基类。

3. 用字符串作为到处传递的协议

示例中的 BANKWALLET 便于展示,但实际项目应统一定义来源和规范。可以使用枚举作为内部模型,或至少将标识集中管理,避免控制器、数据库和策略类各自写一套字符串。

4. 忽略异常和幂等语义

策略只负责选择和调用渠道,不代表支付系统天然可靠。外部调用超时后是否可以重试、重复请求如何识别、渠道成功但本地落库失败怎么办,仍然需要在更高层设计幂等键、状态机和补偿流程。不能因为代码结构变清晰,就误以为业务一致性问题已经解决。

5. 在策略中偷偷承担事务边界

如果一次支付包含本地订单状态更新和外部渠道调用,事务边界应由应用服务根据业务流程决定。不要让每个策略自行开启不同事务,否则调用链会变得难以推断,也容易产生“本地事务回滚但外部支付已经成功”的误解。

实践中的重构顺序

比较稳妥的步骤是:

  1. 先为原有支付分支补充行为测试,记录成功、参数错误、未知方式和客户端异常等情况。
  2. 抽取 PaymentStrategy 接口,但先保持原有行为不变。
  3. 一次迁移一个支付分支,避免大规模重写后无法定位差异。
  4. 引入注册表,并在构造阶段检查空标识和重复标识。
  5. 最后删除旧分支,观察日志、指标和异常类型是否保持兼容。

重构不是把代码拆成更多类,而是让每个类面对更单一、更稳定的问题。一个好的结果应该让新增支付方式变成“增加一个实现并完成组装”,而不是“修改核心服务,再祈祷所有旧分支都没有被影响”。

总结

策略模式适合处理一组可替换的业务算法,但它的价值来自职责边界,而不是模式名称。面对不断增长的支付方式分支,可以把问题拆成三层:服务负责公共校验和流程,注册表负责根据标识查找策略,具体策略负责自己的业务规则与外部依赖。

同时也要保持克制:分支少、变化小的代码不必强行模式化;分支多并不自动意味着需要继承体系;真正值得重构的信号,是核心方法频繁修改、测试难以隔离、不同规则互相污染,以及新增功能总要触碰稳定代码。

代码质量最终体现在变化发生时的成本。不能保证需求永远不变,但可以让变化尽量只影响它应该影响的地方。

最后修改:2026 年 08 月 23 日
如果觉得我的文章对你有用,请随意赞赏