C#-interface使用

1. 接口的本质认知
1.1 接口不只是"隔离变化"
很多开发者认为接口的作用就是"隔离变化",这个理解是对的,但不完整。
更准确地说,接口是:
| 维度 | 说明 |
|---|---|
| 契约 | 调用方只关心"你能做什么" |
| 角色 | 一个类可以扮演多个角色 |
| 边界 | 上层依赖抽象,下层提供实现 |
| 多态入口 | 同一段代码可以替换不同实现 |
“隔离变化"是接口带来的结果,而不是接口的全部意义。
1.2 接口的核心价值
接口 = 抽象"角色/能力"的工具
隔离变化 = 使用接口之后自然带来的收益2. 隐式接口实现
2.1 定义
隐式实现是最常见的接口实现方式。接口成员直接作为类的公共成员暴露,既可以通过类实例调用,也可以通过接口引用调用。
2.2 写法
public interface ILogger
{
void Log(string message);
}
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}2.3 特点
必须是 public
// 正确
public void Log(string message) { }
// 错误:隐式实现不能是 private/protected
private void Log(string message) { }既可以通过类调用,也可以通过接口调用
// 通过类实例调用
var logger = new ConsoleLogger();
logger.Log("hello");
// 通过接口引用调用
ILogger iLogger = new ConsoleLogger();
iLogger.Log("world");2.4 适合的场景
- 接口方法本来就是类的自然能力(主要职责)
- 你希望调用方直接从类实例上看到它
- 接口就是这个类的主要身份之一
典型例子:
// UserRepository 实现 IUserRepository
public class UserRepository : IUserRepository { }
// EmailSender 实现 IMessageSender
public class EmailSender : IMessageSender { }
// FileLogger 实现 ILogger
public class FileLogger : ILogger { }3. 显式接口实现
3.1 定义
显式实现不将接口成员暴露为类的公共 API,只有在将对象转型为对应接口时,该成员才可见。
3.2 写法
public interface ILogger
{
void Log(string message);
}
public class ConsoleLogger : ILogger
{
// 显式实现:不写访问修饰符,加上接口名前缀
void ILogger.Log(string message)
{
Console.WriteLine(message);
}
}注意: 显式实现不能加访问修饰符(
public、private等),写了会编译报错。
// 错误写法
public void ILogger.Log(string message) { } // 编译错误3.3 特点
不能直接通过类实例调用
var logger = new ConsoleLogger();
logger.Log("hello"); // 编译错误,不可见
// 必须转型为接口后才能调用
ILogger iLogger = logger;
iLogger.Log("hello"); // 正确
((ILogger)logger).Log("hello"); // 也正确成员只在"接口视角"下可见
显式实现不是说这个方法不存在,而是说:这个方法只在"你把对象看作某个接口"时才出现。
3.4 适合的场景
场景一:避免污染类的公共 API
public interface IInternalAudit
{
void Audit();
}
public class OrderService : IInternalAudit
{
// 主职责:公开暴露
public void CreateOrder()
{
Console.WriteLine("Create order");
}
// 辅助契约:隐藏在类 API 之外
void IInternalAudit.Audit()
{
Console.WriteLine("Audit order service");
}
}var service = new OrderService();
service.CreateOrder(); // 可见
// service.Audit(); // 不可见
((IInternalAudit)service).Audit(); // 可见场景二:多个接口成员同名但语义不同
public interface IReader
{
void Open();
}
public interface IWriter
{
void Open();
}
public class FileDevice : IReader, IWriter
{
void IReader.Open()
{
Console.WriteLine("Open for reading");
}
void IWriter.Open()
{
Console.WriteLine("Open for writing");
}
}var device = new FileDevice();
((IReader)device).Open(); // Open for reading
((IWriter)device).Open(); // Open for writing如果用隐式实现,只能有一个
public Open(),两个接口共享同一个实现,当语义不同时这是错误的。
场景三:兼容旧接口 / 辅助接口
.NET BCL 中大量使用这种手法:
public class MyCollection<T> : IEnumerable<T>, IEnumerable
{
// 主 API:泛型版本,公开暴露
public IEnumerator<T> GetEnumerator()
{
throw new NotImplementedException();
}
// 兼容旧接口:显式实现,不污染现代 API
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}场景四:IDisposable 的显式实现
当 IDisposable 不是类的主要能力,而只是辅助资源管理时:
public class DataExporter : IDisposable
{
private readonly FileStream _stream;
private bool _disposed;
public DataExporter(string filePath)
{
_stream = new FileStream(filePath, FileMode.Create);
}
// 主 API,清晰暴露
public async Task ExportAsync(IEnumerable<Order> orders)
{
if (_disposed)
throw new ObjectDisposedException(nameof(DataExporter));
foreach (var order in orders)
{
var bytes = Encoding.UTF8.GetBytes(order.ToString() + "\n");
await _stream.WriteAsync(bytes);
}
}
// 显式实现,不污染主 API
void IDisposable.Dispose()
{
if (_disposed) return;
_stream.Dispose();
_disposed = true;
}
}// using 语句能正确触发显式实现的 Dispose()
using var exporter = new DataExporter("output.txt");
await exporter.ExportAsync(orders);
// 块结束时自动调用 IDisposable.Dispose()3.5 一个高级用法(慎用)
同一个接口,同时存在隐式和显式实现:
public interface IMessage
{
string GetText();
}
public class Notice : IMessage
{
// 隐式实现
public string GetText() => "public text";
// 显式实现
string IMessage.GetText() => "interface text";
}var notice = new Notice();
Console.WriteLine(notice.GetText()); // public text
IMessage msg = notice;
Console.WriteLine(msg.GetText()); // interface text建议:能不用就别用。 这会让行为分裂,增加理解成本。除非你非常明确地要区分"类视角"和"接口视角"的行为,否则尽量避免。
4. 隐式 vs 显式:如何选择
4.1 核心区分
隐式实现:"这是我的公开能力。"
显式实现:"这是我在扮演某个接口角色时才有的能力。"4.2 选择标准
优先使用隐式实现,如果:
- 这个方法本来就是类的自然能力(主职责)
- 你希望调用方直接从类实例上看到它
- 接口就是这个类的主要身份之一
使用显式实现,如果:
- 不想将某个接口成员暴露到类的公共 API
- 多个接口成员重名,但语义不同(强信号)
- 这是兼容性接口、框架辅助接口、低优先级接口
- 想明确区分"类本身职责"和"接口角色职责”
- 想缩小类对外暴露的表面积,减少误用
4.3 对比速查表
| 特性 | 隐式实现 | 显式实现 |
|---|---|---|
| 访问修饰符 | 必须 public |
不能有访问修饰符 |
| 通过类实例调用 | 可以 | 不可以 |
| 通过接口引用调用 | 可以 | 可以 |
| 暴露在类公共 API | 是 | 否 |
| 适合场景 | 主职责、主能力 | 辅助契约、冲突解决 |
5. 接口 vs 抽象类
5.1 核心区别
接口:描述"能做什么" → 角色 / 能力 / 契约
抽象类:描述"是什么,并且已经有一部分共性实现" → 基类 / 模板 / 骨架5.2 多角色 vs 单继承
C# 类只能继承一个基类,但可以实现多个接口:
// 一个类扮演多个角色
public class FileCache : ICache, IDisposable, IAsyncDisposable
{
}接口的天然优势:一个类可以扮演多个角色,而抽象类做不到。
5.3 什么时候用接口
只需要约束,不需要共享实现:
public interface ISerializer
{
string Serialize<T>(T obj);
}
// 三种实现完全不同,接口非常合适
public class JsonSerializer : ISerializer { }
public class XmlSerializer : ISerializer { }
public class YamlSerializer : ISerializer { }5.4 什么时候用抽象类
需要共享实现、共享状态、模板流程:
public abstract class FileImporter
{
// 共享流程(模板方法)
public void Import(string path)
{
ValidatePath(path);
var content = File.ReadAllText(path);
Parse(content);
Save();
}
// 共性实现
protected virtual void ValidatePath(string path)
{
if (string.IsNullOrWhiteSpace(path))
throw new ArgumentException(nameof(path));
}
// 子类各自实现
protected abstract void Parse(string content);
protected abstract void Save();
}需要共享状态:
public abstract class WorkerBase
{
// 共享字段和构造函数
protected readonly ILogger _logger;
protected WorkerBase(ILogger logger)
{
_logger = logger;
}
}5.5 接口 + 抽象类组合使用(成熟系统的常见做法)
// 对外:依赖接口(稳定契约)
public interface IMessageSender
{
Task SendAsync(string message);
}
// 对内:抽象类沉淀共性(模板流程 + 共享状态)
public abstract class MessageSenderBase : IMessageSender
{
protected readonly ILogger _logger;
protected MessageSenderBase(ILogger logger)
{
_logger = logger;
}
public async Task SendAsync(string message)
{
Validate(message);
await SendCoreAsync(message);
_logger.LogInformation("message sent");
}
protected virtual void Validate(string message)
{
if (string.IsNullOrWhiteSpace(message))
throw new ArgumentException(nameof(message));
}
protected abstract Task SendCoreAsync(string message);
}
// 具体实现:只填差异部分
public class EmailSender : MessageSenderBase
{
public EmailSender(ILogger<EmailSender> logger) : base(logger) { }
protected override Task SendCoreAsync(string message)
{
Console.WriteLine($"Email: {message}");
return Task.CompletedTask;
}
}设计原则:
- 接口负责抽象边界
- 抽象类负责沉淀共性
5.6 选择口诀
优先用接口,如果你在表达:
- 某种能力 / 某种边界 / 某种可替换实现
- 某种策略 / 某种外部依赖
优先用抽象类,如果你在表达:
- 一组对象的共同父类 / 有明确继承关系
- 有共享实现 / 有共享状态 / 有模板流程
5.7 对比速查表
| 维度 | 接口 | 抽象类 |
|---|---|---|
| 多继承 | 支持多个 | 只能单继承 |
| 共享实现 | 不擅长(default 实现慎用) | 擅长 |
| 共享状态(字段) | 不支持 | 支持 |
| 构造函数 | 没有 | 有 |
| 适合表达 | 角色 / 能力 / 契约 | 家族 / 骨架 / 模板 |
| 典型例子 | ILogger ICache |
Stream ControllerBase |
6. 什么时候不要定义接口
这部分比"什么时候定义接口"更重要。很多项目的问题,不是接口太少,而是接口太多、太空、太假。
6.1 没有替换价值时,不要急着定义接口
// 如果 TaxCalculator 没有多种实现、不是边界、不需要多态
// 直接用具体类即可
public class TaxCalculator
{
public decimal Calculate(Order order) { ... }
}
// 不必要的接口定义
public interface ITaxCalculator
{
decimal Calculate(Order order);
}6.2 纯数据对象不要抽象接口
// 没有意义
public interface IUserDto
{
string Name { get; set; }
}
// 直接用数据类
public class UserDto
{
public string Name { get; set; }
}以下类型通常不需要接口:
DTO/VO/ViewModel- 领域实体(
Order、User、Product) Record类型Value Object
6.3 稳定的工具类,不需要强行接口化
// 如果没有多种哈希策略切换、没有测试替身需求
// 直接用具体类
public class Md5Hasher
{
public string Hash(string input) { ... }
}6.4 应用内部的实现细节类不需要接口
// 只在程序集内部使用,变化可能性低
internal class OrderNumberGenerator
{
public string Generate() { ... }
}6.5 不要为了"方便 Mock"而滥造接口
错误顺序:为了 mock → 建接口
正确顺序:设计需要抽象 → 建接口 → 测试顺便受益
天然应该抽象的依赖(测试只是附带收益):
- 数据库访问
- 第三方 HTTP 调用
- 消息队列
- 缓存
- 文件系统
- 时钟、随机数
不需要为测试而强行接口化的类:
- 纯业务计算类
- 无外部依赖的领域逻辑
6.6 不要为了"看起来规范"而机械配对接口
// 典型反模式:一比一机械配对,没有抽象价值
public interface IOrderService { ... }
public class OrderService : IOrderService { ... }
public interface IUserManager { ... }
public class UserManager : IUserManager { ... }
public interface IProductHelper { ... }
public class ProductHelper : IProductHelper { ... }这类代码会导致:
- 文件数量翻倍
- 导航成本增加
- 抽象失真
- 重构更痛苦
6.7 常见接口滥用模式
| 反模式 | 描述 | 问题 |
|---|---|---|
| 一比一空壳接口 | 每个类机械配一个 IXXX |
无替换价值,纯噪音 |
| 胖接口 | 一个接口包含大量不相关方法 | 违反接口隔离原则 |
| 以类为中心命名 | ICommonHelper、IManager、IBaseService |
抽象不清晰 |
| 为 DI 容器而造接口 | 只是为了注册到容器 | 本末倒置 |
7. 什么时候应该定义接口
7.1 五个判断问题
问题一:调用方关心的是"能力"还是"具体实现"?
能不能发消息?能不能写日志?能不能取缓存?
→ 适合接口问题二:这里是否是系统边界?
仓储 / 外部 API / 支付网关 / 消息队列 / 文件存储 / 缓存 / 时钟
→ 适合接口问题三:是否存在两种以上"有意义"的实现?
// 有实际多实现价值的接口
public interface ICacheProvider { }
// RedisCacheProvider
// MemoryCacheProvider
public interface INotificationSender { }
// SmsNotificationSender
// EmailNotificationSender
// WechatNotificationSender问题四:是否需要多态扩展?
// 策略模式:运行时选择不同策略
public interface IDiscountStrategy
{
decimal Calculate(Order order);
}问题五:如果没有接口,这段代码是否明显更难演进?
现在不用接口也没问题?没有外部依赖边界?不太可能扩展?
→ 先用具体类,等需求出现再提炼(渐进式抽象)7.2 应该定义接口的典型场景
// 仓储层(系统边界)
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(Guid id);
Task SaveAsync(Order order);
}
// 外部服务(可替换实现)
public interface IPaymentGateway
{
Task PayAsync(decimal amount);
}
// 策略(多态扩展)
public interface IPricingStrategy
{
decimal Calculate(Order order);
}
// 小而稳定的环境依赖
public interface IClock
{
DateTime UtcNow { get; }
}8. DI 中的接口注册
8.1 核心原则
接口定义在内层,实现放在外层,注册在入口
接口定义在哪个层 → 由哪个层负责,或由组合根统一管理8.2 典型分层模型
┌───────────────────────────────┐
│ API / Web 层 │ ← 入口、Controller、Middleware
├───────────────────────────────┤
│ Application 层 │ ← 用例、Service、DTO
├───────────────────────────────┤
│ Domain 层 │ ← 实体、领域服务、接口定义
├───────────────────────────────┤
│ Infrastructure 层 │ ← 仓储实现、第三方服务实现
└───────────────────────────────┘依赖方向:
Domain 层定义:
IOrderRepository ← 契约,属于内层
Infrastructure 层实现:
EfOrderRepository : IOrderRepository ← 实现,属于外层
Domain层不知道EfOrderRepository的存在,也不应该知道。
8.3 推荐注册方式:各层通过扩展方法自我注册
Infrastructure 层
// Infrastructure/DependencyInjection.cs
public static class InfrastructureServiceCollectionExtensions
{
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration configuration)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("Default")));
services.AddScoped<IOrderRepository, EfOrderRepository>();
services.AddScoped<IUserRepository, EfUserRepository>();
services.AddScoped<IEmailSender, SmtpEmailSender>();
services.AddScoped<IPaymentGateway, AlipayGateway>();
return services;
}
}Application 层
// Application/DependencyInjection.cs
public static class ApplicationServiceCollectionExtensions
{
public static IServiceCollection AddApplication(
this IServiceCollection services)
{
services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));
services.AddAutoMapper(Assembly.GetExecutingAssembly());
return services;
}
}Program.cs(组合根,保持简洁)
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddApplication()
.AddInfrastructure(builder.Configuration);
builder.Services.AddControllers();
var app = builder.Build();
app.Run();8.4 各层注册职责划分
| 层 | 注册内容 |
|---|---|
| Web 层 | Controller、Middleware、Swagger、跨层协调配置 |
| Application 层 | MediatR、AutoMapper、FluentValidation、应用级 Service |
| Infrastructure 层 | DbContext、仓储实现、第三方客户端、缓存实现、文件存储 |
8.5 生命周期选择
Singleton 整个进程生命周期内使用同一个实例
Scoped 同一次请求内使用同一个实例
Transient 每次注入都创建一个新实例| 类型 | 推荐生命周期 | 原因 |
|---|---|---|
DbContext |
Scoped | 不能跨请求共享连接和事务 |
| Repository | Scoped | 依赖 DbContext |
| Application Service | Scoped | 通常依赖仓储 |
HttpClient |
用 AddHttpClient |
有特殊管理机制 |
| 配置类 | Singleton | 不变的 |
| 内存缓存 | Singleton | 需要全局共享 |
| 无状态工具类 | Transient | 轻量级,无状态 |
8.6 重要注意事项:Singleton 不能直接依赖 Scoped
// 危险:Scoped 服务被 Singleton 捕获,变成事实上的 Singleton
public class MySingletonService
{
private readonly IOrderRepository _repo; // Scoped,危险!
public MySingletonService(IOrderRepository repo)
{
_repo = repo;
}
}正确做法:使用 IServiceScopeFactory
// 正确:手动创建 Scope
public class MySingletonService
{
private readonly IServiceScopeFactory _scopeFactory;
public MySingletonService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public async Task DoWorkAsync()
{
using var scope = _scopeFactory.CreateScope();
var repo = scope.ServiceProvider
.GetRequiredService<IOrderRepository>();
await repo.GetByIdAsync(Guid.NewGuid());
}
}9. 真实业务场景判断
场景一:仓储层
判断结论:
| 项目 | 结论 |
|---|---|
| 要不要接口 | 要,系统边界 |
| 隐式/显式 | 隐式,仓储方法是主能力 |
| 生命周期 | Scoped,依赖 DbContext |
// Domain 层 - 定义接口
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(Guid id);
Task<IReadOnlyList<Order>> GetByUserIdAsync(Guid userId);
Task SaveAsync(Order order);
Task DeleteAsync(Guid id);
}
// Infrastructure 层 - 实现
public class EfOrderRepository : IOrderRepository
{
private readonly AppDbContext _context;
public EfOrderRepository(AppDbContext context)
{
_context = context;
}
public async Task<Order?> GetByIdAsync(Guid id)
=> await _context.Orders.FindAsync(id);
public async Task<IReadOnlyList<Order>> GetByUserIdAsync(Guid userId)
=> await _context.Orders
.Where(o => o.UserId == userId)
.ToListAsync();
public async Task SaveAsync(Order order)
{
_context.Orders.Update(order);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(Guid id)
{
var order = await GetByIdAsync(id);
if (order is not null)
{
_context.Orders.Remove(order);
await _context.SaveChangesAsync();
}
}
}
// 注册
services.AddScoped<IOrderRepository, EfOrderRepository>();场景二:多种实现的通知发送器
判断结论:
| 项目 | 结论 |
|---|---|
| 要不要接口 | 要,明确多实现 + 多态切换 |
| 隐式/显式 | 隐式,Send 是主职责 |
| 生命周期 | Scoped 或 Transient,视实现决定 |
// 接口定义
public interface INotificationSender
{
Task SendAsync(string recipient, string message);
NotificationChannel Channel { get; }
}
public enum NotificationChannel { Email, Sms, Wechat }
// 多种实现
public class EmailNotificationSender : INotificationSender
{
public NotificationChannel Channel => NotificationChannel.Email;
public async Task SendAsync(string recipient, string message)
{
Console.WriteLine($"[Email] To: {recipient}, Message: {message}");
await Task.CompletedTask;
}
}
public class SmsNotificationSender : INotificationSender
{
public NotificationChannel Channel => NotificationChannel.Sms;
public async Task SendAsync(string recipient, string message)
{
Console.WriteLine($"[SMS] To: {recipient}, Message: {message}");
await Task.CompletedTask;
}
}工厂模式封装选择逻辑(推荐):
public interface INotificationSenderFactory
{
INotificationSender GetSender(NotificationChannel channel);
}
public class NotificationSenderFactory : INotificationSenderFactory
{
private readonly IEnumerable<INotificationSender> _senders;
public NotificationSenderFactory(IEnumerable<INotificationSender> senders)
{
_senders = senders;
}
public INotificationSender GetSender(NotificationChannel channel)
{
return _senders.FirstOrDefault(s => s.Channel == channel)
?? throw new InvalidOperationException(
$"No sender registered for channel: {channel}");
}
}
// 注册
services.AddScoped<INotificationSender, EmailNotificationSender>();
services.AddScoped<INotificationSender, SmsNotificationSender>();
services.AddScoped<INotificationSender, WechatNotificationSender>();
services.AddScoped<INotificationSenderFactory, NotificationSenderFactory>();扩展性: 新增
WechatNotificationSender只需注册,工厂和上层代码完全不变。这就是"对扩展开放,对修改关闭"。
场景三:内部计算器(先不要接口)
判断结论:
| 项目 | 结论 |
|---|---|
| 要不要接口 | 先不要,纯内部业务计算 |
| 隐式/显式 | 不适用 |
| 生命周期 | Transient 或直接 new |
// 直接用具体类,不套接口
public class OrderPriceCalculator
{
public decimal Calculate(Order order)
{
var basePrice = order.Items.Sum(i => i.Price * i.Quantity);
var discount = GetDiscount(order);
var tax = GetTax(basePrice - discount);
return basePrice - discount + tax;
}
private decimal GetDiscount(Order order)
=> order.IsVip
? order.Items.Sum(i => i.Price * i.Quantity) * 0.1m
: 0;
private decimal GetTax(decimal amount)
=> amount * 0.13m;
}何时才提炼接口(渐进式抽象):
// 当出现以下情况时再提炼:
// 1. 需要支持多种计算策略(普通/VIP/活动价)
// 2. 需要在测试中 Mock 价格结果
// 3. 被跨模块共享,需要稳定契约
public interface IPriceCalculator
{
decimal Calculate(Order order);
}场景四:同时实现两个接口,名称冲突
判断结论:
| 项目 | 结论 |
|---|---|
| 要不要接口 | 要 |
| 隐式/显式 | 必须显式,两个 Open() 语义不同 |
public interface IReadable
{
void Open();
string Read();
}
public interface IWritable
{
void Open();
void Write(string data);
}
public class DataService : IReadable, IWritable
{
private bool _readMode;
private bool _writeMode;
// 显式实现,区分两个 Open 的不同语义
void IReadable.Open()
{
_readMode = true;
Console.WriteLine("Opened for reading");
}
void IWritable.Open()
{
_writeMode = true;
Console.WriteLine("Opened for writing");
}
public string Read()
{
if (!_readMode)
throw new InvalidOperationException("Not opened for reading");
return "data";
}
public void Write(string data)
{
if (!_writeMode)
throw new InvalidOperationException("Not opened for writing");
Console.WriteLine($"Writing: {data}");
}
}var service = new DataService();
((IReadable)service).Open(); // Opened for reading
((IWritable)service).Open(); // Opened for writing场景五:接口分层设计综合示例
// Domain 层 - 定义接口契约
public interface IPaymentGateway
{
Task<PaymentResult> PayAsync(PaymentRequest request);
}
// Infrastructure 层 - 多个实现
public class AlipayGateway : IPaymentGateway
{
public async Task<PaymentResult> PayAsync(PaymentRequest request)
{
// 调用支付宝 API
return new PaymentResult { Success = true };
}
}
public class WechatPayGateway : IPaymentGateway
{
public async Task<PaymentResult> PayAsync(PaymentRequest request)
{
// 调用微信支付 API
return new PaymentResult { Success = true };
}
}
// Application 层 - 只依赖接口,不关心实现
public class OrderService
{
private readonly IOrderRepository _orderRepo;
private readonly IPaymentGateway _paymentGateway;
private readonly INotificationSenderFactory _senderFactory;
public OrderService(
IOrderRepository orderRepo,
IPaymentGateway paymentGateway,
INotificationSenderFactory senderFactory)
{
_orderRepo = orderRepo;
_paymentGateway = paymentGateway;
_senderFactory = senderFactory;
}
public async Task CreateOrderAsync(CreateOrderRequest request)
{
var order = new Order(request.UserId, request.Items);
var paymentResult = await _paymentGateway.PayAsync(
new PaymentRequest { Amount = order.TotalAmount });
if (!paymentResult.Success)
throw new PaymentFailedException();
await _orderRepo.SaveAsync(order);
var sender = _senderFactory.GetSender(NotificationChannel.Email);
await sender.SendAsync(request.UserEmail, "订单创建成功");
}
}10. 接口设计原则
10.1 接口要小,要聚焦(接口隔离原则)
// 胖接口:违反接口隔离原则
public interface IUserService
{
void Create();
void Delete();
void Update();
void Login();
void Logout();
void SendEmail();
void ExportExcel();
}
// 按角色拆分
public interface IUserRepository { void Create(); void Delete(); void Update(); }
public interface IAuthService { void Login(); void Logout(); }
public interface IEmailSender { void SendEmail(); }
public interface IReportExporter { void ExportExcel(); }10.2 按"角色"命名,不要按"类"命名
// 好的命名(角色/能力)
public interface IMessageSender { }
public interface IOrderRepository { }
public interface ICacheProvider { }
public interface IClock { }
// 一般的命名(不够清晰)
public interface IUserService { }
public interface ICommonHelper { }
public interface IManager { }10.3 接口一旦公开,修改成本很高
改一个接口成员会影响所有实现类。
- 不要轻易把接口做得很大
- 不要过早承诺过多成员
- 尽量保持稳定、精简
10.4 依赖倒置 ≠ 到处接口化
正确理解:高层模块依赖稳定抽象,而不是依赖易变细节
错误理解:项目里每个类都必须有一个 IXXX10.5 渐进式抽象
先用具体类,等到变化出现,或者边界清晰了,再提炼接口。
这叫 YAGNI(You Aren’t Gonna Need It)。
11. 完整决策地图
11.1 要不要定义接口?
需要定义接口吗?
│
├── 是系统边界?(数据库、外部 API、文件系统、消息队列)
│ └── 是 → 定义接口
│
├── 有多个实现,或可预见替换?
│ └── 是 → 定义接口
│
├── 需要多态 / 策略 / 插件机制?
│ └── 是 → 定义接口
│
├── 纯内部实现,无边界,无替换,无多态?
│ └── 先用具体类,等需求出现再提炼(渐进式抽象)
│
└── 只是为了看起来规范 / 方便 Mock?
└── 不要11.2 用隐式还是显式实现?
用隐式实现还是显式实现?
│
├── 这个方法是类的主要能力 / 主职责?
│ └── 隐式实现
│
├── 只是为了履行辅助契约,不想污染主 API?
│ └── 显式实现
│
├── 多个接口同名但语义不同?
│ └── 必须显式实现
│
└── 兼容旧接口 / 辅助接口(如非泛型 IEnumerable)?
└── 显式实现11.3 注册在哪里?
注册在哪里?
│
├── 仓储、数据库、外部服务实现
│ └── Infrastructure 层注册
│
├── 应用层协调类(MediatR、AutoMapper 等)
│ └── Application 层注册
│
├── Web 相关(Controller、Middleware)
│ └── Web 层注册
│
└── Program.cs 只做组合
└── 调用各层的注册扩展方法,保持简洁11.4 接口 vs 抽象类?
接口 vs 抽象类?
│
├── 表达"角色/能力",可能多个角色叠加?
│ └── 接口
│
├── 有共享实现、共享状态、模板流程,是"对象家族"?
│ └── 抽象类
│
└── 两者都需要?
└── 接口 + 抽象类组合使用
└── 接口对外暴露契约
抽象类对内沉淀共性11.5 生命周期选择?
生命周期?
│
├── 依赖 DbContext,或需要在同一请求内共享?
│ └── Scoped
│
├── 全局共享状态,整个进程只需一个?
│ └── Singleton
│ └── 注意:不能直接依赖 Scoped,用 IServiceScopeFactory
│
└── 无状态,轻量级,每次用新的?
└── Transient附录:核心结论速查
接口适合
- 边界 / 契约 / 策略 / 多态 / 角色 / 可替换依赖
抽象类适合
- 共性代码 / 模板流程 / 共享状态 / 继承骨架 / 对象家族
不要定义接口的典型情况
- 没有变化点
- 没有边界意义
- 没有多实现需求
- 只是为了"规范"或"方便 Mock"
- 只是为了给类配一个
IXXX
隐式实现
- 这是类的主要能力
- 希望从类实例直接访问
- 接口是类的主要身份
显式实现
- 辅助契约,不想污染主 API
- 多接口成员重名,语义不同
- 兼容旧接口、辅助接口
- 控制暴露面,减少误用