↓ 跳过正文

C#-interface使用

·14 分钟
作者
Hexin
在确定的逻辑与不确定的现实之间,做一名耐心的构建者。
目录

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 依赖倒置 ≠ 到处接口化
#

正确理解:高层模块依赖稳定抽象,而不是依赖易变细节
错误理解:项目里每个类都必须有一个 IXXX

10.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
  • 多接口成员重名,语义不同
  • 兼容旧接口、辅助接口
  • 控制暴露面,减少误用