PAYATHON 2026

类设计应选择 interface 还是 abstract class?

支付阿杰

结论

建议优先使用“接口 + 组合 + 依赖注入”:

  • 用 IPaymentSystem 定义支付系统的能力。
  • 将数据库访问封装到独立的 Repository 或 Data Provider 中。
  • 通过构造函数将数据访问依赖注入 PS1、PS2 等实现。
  • 只有当多个支付系统共享稳定的支付流程,而不只是复用数据库代码时,才需要引入 abstract class。

不建议用 PaymentSystemBase 专门处理数据库连接和查询。支付业务和数据库访问是两种职责,通过继承复用数据库代码会增加不必要的耦合。

为什么不应为数据库操作建立基类

继承表达的是“is-a”关系。PS1 是一种 payment system,但不是一种 database helper。

如果所有支付系统都继承 PaymentSystemBase,可能出现以下问题:

  • 支付业务会与数据库实现绑定。以后改用 API、缓存或消息队列时,调整起来更麻烦。
  • 单元测试需要设法绕开真实的数据库连接。
  • 查询、连接和事务等功能会不断进入基类,使它越来越臃肿。
  • C# 只支持单继承。把基类位置用于数据库复用,可能妨碍以后的业务继承。
  • 子类可能得到一些并不需要的 protected 数据库方法。

接口和抽象类可以同时使用。接口用于定义能力,抽象类用于复用同类对象之间稳定的业务流程。数据库访问则更适合通过组合提供。

推荐设计

先定义支付系统的业务契约:

public interface IPaymentSystem
{
    Task<PaymentResult> ProcessPaymentAsync(
        PaymentRequest request,
        CancellationToken cancellationToken = default);
}

然后将数据访问定义为独立依赖:

public interface IPaymentDataRepository
{
    Task<PaymentData?> GetPaymentDataAsync(
        string paymentId,
        CancellationToken cancellationToken = default);
}

public sealed record PaymentRequest(string PaymentId, decimal Amount);

public sealed record PaymentData(string PaymentId, string AccountNumber);

public sealed record PaymentResult(bool Success, string Message);

具体的支付系统通过构造函数接收 Repository:

public sealed class PS1 : IPaymentSystem
{
    private readonly IPaymentDataRepository _repository;

    public PS1(IPaymentDataRepository repository)
    {
        _repository = repository;
    }

    public async Task<PaymentResult> ProcessPaymentAsync(
        PaymentRequest request,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(request);

        PaymentData? data = await _repository.GetPaymentDataAsync(
            request.PaymentId,
            cancellationToken);

        if (data is null)
        {
            return new PaymentResult(false, "Payment data was not found.");
        }

        // PS1-specific payment processing
        return new PaymentResult(true, "Payment processed by PS1.");
    }
}

PS2 可以使用同一个数据访问契约,同时保留自己的处理逻辑:

public sealed class PS2 : IPaymentSystem
{
    private readonly IPaymentDataRepository _repository;

    public PS2(IPaymentDataRepository repository)
    {
        _repository = repository;
    }

    public async Task<PaymentResult> ProcessPaymentAsync(
        PaymentRequest request,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(request);

        PaymentData? data = await _repository.GetPaymentDataAsync(
            request.PaymentId,
            cancellationToken);

        if (data is null)
        {
            return new PaymentResult(false, "Payment data was not found.");
        }

        // PS2-specific payment processing
        return new PaymentResult(true, "Payment processed by PS2.");
    }
}

数据库实现留在基础设施层:

public sealed class SqlPaymentDataRepository : IPaymentDataRepository
{
    private readonly string _connectionString;

    public SqlPaymentDataRepository(string connectionString)
    {
        _connectionString = connectionString;
    }

    public async Task<PaymentData?> GetPaymentDataAsync(
        string paymentId,
        CancellationToken cancellationToken = default)
    {
        // Open the database connection here.
        // Execute a parameterized query.
        // Map the result to PaymentData.
        await Task.CompletedTask;

        return new PaymentData(paymentId, "example-account");
    }
}

测试时可以注入内存实现或 Mock,无需连接真实数据库。

什么时候适合使用 abstract class

如果 PS1、PS2 的整体处理顺序相同,只有某个支付步骤存在差异,可以使用 Template Method 模式。抽象类处理固定的业务流程,具体支付系统只实现不同的部分:

public abstract class PaymentSystemBase : IPaymentSystem
{
    private readonly IPaymentDataRepository _repository;

    protected PaymentSystemBase(IPaymentDataRepository repository)
    {
        _repository = repository;
    }

    public async Task<PaymentResult> ProcessPaymentAsync(
        PaymentRequest request,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(request);

        Validate(request);

        PaymentData? data = await _repository.GetPaymentDataAsync(
            request.PaymentId,
            cancellationToken);

        if (data is null)
        {
            return new PaymentResult(false, "Payment data was not found.");
        }

        return await ExecutePaymentAsync(request, data, cancellationToken);
    }

    protected virtual void Validate(PaymentRequest request)
    {
        if (string.IsNullOrWhiteSpace(request.PaymentId))
        {
            throw new ArgumentException(
                "Payment ID is required.",
                nameof(request));
        }

        if (request.Amount <= 0)
        {
            throw new ArgumentOutOfRangeException(
                nameof(request),
                "Amount must be greater than zero.");
        }
    }

    protected abstract Task<PaymentResult> ExecutePaymentAsync(
        PaymentRequest request,
        PaymentData data,
        CancellationToken cancellationToken);
}

具体实现只处理渠道差异:

public sealed class PS1 : PaymentSystemBase
{
    public PS1(IPaymentDataRepository repository)
        : base(repository)
    {
    }

    protected override Task<PaymentResult> ExecutePaymentAsync(
        PaymentRequest request,
        PaymentData data,
        CancellationToken cancellationToken)
    {
        // PS1-specific logic
        return Task.FromResult(
            new PaymentResult(true, "Payment processed by PS1."));
    }
}

基类仍然不直接管理数据库连接,而是依赖 IPaymentDataRepository 抽象。这样,支付流程不会与具体的存储技术绑定。

多个支付系统如何选择

如果需要在运行时根据渠道选择支付系统,可以增加 Resolver 或 Factory:

public interface IPaymentSystemResolver
{
    IPaymentSystem Resolve(string systemName);
}

public sealed class PaymentSystemResolver : IPaymentSystemResolver
{
    private readonly IReadOnlyDictionary<string, IPaymentSystem> _systems;

    public PaymentSystemResolver(
        PS1 ps1,
        PS2 ps2)
    {
        _systems = new Dictionary<string, IPaymentSystem>(
            StringComparer.OrdinalIgnoreCase)
        {
            ["PS1"] = ps1,
            ["PS2"] = ps2
        };
    }

    public IPaymentSystem Resolve(string systemName)
    {
        if (_systems.TryGetValue(systemName, out IPaymentSystem? system))
        {
            return system;
        }

        throw new NotSupportedException(
            $"Payment system '{systemName}' is not supported.");
    }
}

调用方只需要选择并调用对应的支付系统,不必了解数据库或渠道内部的实现细节:

IPaymentSystem paymentSystem = resolver.Resolve("PS1");

PaymentResult result = await paymentSystem.ProcessPaymentAsync(
    new PaymentRequest("PAY-1001", 99.50m),
    cancellationToken);

注意事项

不要在构造函数中调用 CreateDatabaseConnection()。构造函数应尽量只做依赖赋值和简单的状态检查。数据库连接可能失败、阻塞,还涉及资源释放,因此应在实际方法中创建,并配合 async、await 和正确的资源释放机制使用。

输入检查也需要按职责划分:

  • 所有渠道通用的业务规则,可以放在应用服务或抽象基类中。
  • 某个渠道特有的限制,由对应的支付系统实现处理。
  • 数据完整性和持久化规则,由数据访问层和数据库约束负责。

真实支付还需要考虑幂等性、事务边界、超时、重试、日志和敏感信息保护。不能简单地为所有支付请求统一添加重试,否则可能导致重复扣款。是否允许重试,应根据支付渠道提供的幂等机制决定。

IPaymentSystem 负责表达支付能力,Repository 负责隔离数据访问。只有存在稳定的公共支付流程时,才需要使用 abstract class,不应仅为共享数据库代码而引入它。

备注:内容仅供参考。