类设计应选择 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,不应仅为共享数据库代码而引入它。
备注:内容仅供参考。
