netnr 2025-12-02

一、为什么需要依赖注入(DI)

1.1 解决的核心问题

    1. 【松耦合】
       类只依赖接口,不依赖具体实现
       → 更换实现时不需要修改业务代码

    2. 【可测试】
       可以注入 Mock 对象进行单元测试
       → 测试时不会真的发邮件、写数据库

    3. 【可维护】
       依赖关系清晰可见(看构造函数就知道)
       → 便于理解和修改代码

    4. 【可扩展】
       新增实现只需添加类 + 修改配置
       → 符合开闭原则(对扩展开放,对修改关闭)

    5. 【环境切换】
       开发/测试/生产环境使用不同实现
       → 只需修改配置,代码不变                                     

1.2 SOLID 原则关联

原则 全称 DI 如何支持
S 单一职责 类只负责业务逻辑,不负责创建依赖
O 开闭原则 新增实现无需修改现有代码
L 里氏替换 任何实现都可以替换接口
I 接口隔离 依赖小而专注的接口
D 依赖倒置 依赖抽象(接口),不依赖具体实现

二、问题演示:没有 DI 的代码

2.1 紧耦合代码示例

// ❌ 不好的设计:紧耦合

public class EmailService
{
    public void SendEmail(string to, string message)
    {
        Console.WriteLine($"发送邮件到 {to}: {message}");
        // 实际发送邮件的逻辑...
    }
}

public class UserService
{
    // 问题1:直接 new 具体类
    private EmailService _emailService = new EmailService();
    
    // 问题2:或者在方法内部调用静态方法
    public void RegisterUser(string username, string email)
    {
        // 保存用户...
        Console.WriteLine($"用户 {username} 注册成功");
        
        // 发送欢迎邮件
        _emailService.SendEmail(email, "欢迎注册!");
    }
}

// 使用
class Program
{
    static void Main()
    {
        var userService = new UserService();
        userService.RegisterUser("张三", "zhangsan@example.com");
    }
}

2.2 这种设计的问题

  1. 【无法替换实现】
     想换成短信通知?必须修改 UserService 代码
     if (type == "email") ... else if (type == "sms") ...
                                                                
  2. 【无法单元测试】
     测试 RegisterUser 会真的发邮件!
     无法 Mock EmailService
                                                                
  3. 【依赖关系隐藏】
     从外部看不出 UserService 依赖什么
     需要阅读内部代码才知道
                                                                
  4. 【违反开闭原则】
     每次新增通知方式都要修改 UserService
                                                                
  5. 【环境切换困难】
     开发环境也会真的发邮件
     需要到处写 if (isDevelopment) ...

2.3 静态方法的问题

// ❌ 静态方法方式 - 看似简单,实则问题更多

public static class EmailService
{
    public static void SendEmail(string to, string msg) 
        => Console.WriteLine($"发送邮件: {to}");
}

public static class SmsService
{
    public static void SendSms(string to, string msg) 
        => Console.WriteLine($"发送短信: {to}");
}

public class UserService
{
    public void RegisterUser(string name, string contact, string type)
    {
        Console.WriteLine($"用户 {name} 注册成功");
        
        // ❌ 问题:必须知道所有发送方式,每次新增都要改这里
        if (type == "email")
            EmailService.SendEmail(contact, "欢迎注册");
        else if (type == "sms")
            SmsService.SendSms(contact, "欢迎注册");
        else if (type == "wechat")  // 新增
            WeChatService.Send(contact, "欢迎注册");
        // ... 无穷无尽
    }
}

三、解决方案:使用依赖注入

3.1 第一步:定义接口(面向抽象)

// ✅ 定义抽象接口
public interface IMessageService
{
    void SendMessage(string to, string message);
}

3.2 第二步:创建多个实现

// ✅ 邮件实现
public class EmailService : IMessageService
{
    public void SendMessage(string to, string message)
    {
        Console.WriteLine($"📧 发送邮件到 {to}: {message}");
    }
}

// ✅ 短信实现
public class SmsService : IMessageService
{
    public void SendMessage(string to, string message)
    {
        Console.WriteLine($"📱 发送短信到 {to}: {message}");
    }
}

// ✅ 微信实现
public class WeChatService : IMessageService
{
    public void SendMessage(string to, string message)
    {
        Console.WriteLine($"💬 发送微信到 {to}: {message}");
    }
}

// ✅ 开发环境:假服务(不真正发送)
public class FakeMessageService : IMessageService
{
    public void SendMessage(string to, string message)
    {
        Console.WriteLine($"🔧 [开发模式] 模拟发送到 {to}: {message}");
    }
}

3.3 第三步:通过构造函数注入

// ✅ 好的设计:依赖接口,通过构造函数注入
public class UserService
{
    private readonly IMessageService _messageService;
    
    // 构造函数注入:依赖由外部传入
    public UserService(IMessageService messageService)
    {
        _messageService = messageService;
    }
    
    public void RegisterUser(string username, string contact)
    {
        Console.WriteLine($"用户 {username} 注册成功");
        
        // 不关心具体用什么方式发送,只管调用
        _messageService.SendMessage(contact, "欢迎注册!");
    }
}

3.4 第四步:在组合根配置依赖

// Program.cs - 组合根(Composition Root)
var services = new ServiceCollection();

// 根据环境配置不同实现
if (Environment.IsDevelopment())
{
    // 开发环境:使用假服务
    services.AddTransient<IMessageService, FakeMessageService>();
}
else
{
    // 生产环境:使用真实服务
    services.AddTransient<IMessageService, EmailService>();
}

services.AddTransient<UserService>();

var provider = services.BuildServiceProvider();
var userService = provider.GetRequiredService<UserService>();
userService.RegisterUser("张三", "zhangsan@example.com");

四、三种注入方式

4.1 构造函数注入(推荐)

public class OrderService
{
    private readonly IPaymentService _payment;
    private readonly IInventoryService _inventory;
    private readonly IEmailService _email;
    
    // ✅ 构造函数注入 - 最常用,依赖关系一目了然
    public OrderService(
        IPaymentService payment,
        IInventoryService inventory,
        IEmailService email)
    {
        _payment = payment;
        _inventory = inventory;
        _email = email;
    }
    
    public void CreateOrder(Order order)
    {
        _inventory.Reserve(order.ProductId);
        _payment.Process(order.Amount);
        _email.SendConfirmation(order);
    }
}

优点

  • 依赖关系明确可见
  • 对象创建后依赖不可变
  • 便于单元测试
  • 可以标记为 readonly

4.2 属性注入

public class ReportService
{
    // 属性注入 - 适用于可选依赖
    public ILogger? Logger { get; set; }
    
    public void GenerateReport()
    {
        Logger?.Log("开始生成报表...");
        // 生成报表逻辑...
        Logger?.Log("报表生成完成");
    }
}

// 使用
var service = new ReportService();
service.Logger = new ConsoleLogger();  // 可选设置
service.GenerateReport();

优点

  • 灵活,可以不设置
  • 适合可选依赖

缺点

  • 依赖可能为空,需要判空
  • 依赖关系不够明确

4.3 方法注入

public class DataProcessor
{
    // 方法注入 - 每次调用可使用不同实现
    public void Process(IDataReader reader)
    {
        var data = reader.ReadData();
        // 处理数据...
    }
}

// 使用 - 每次可传入不同实现
var processor = new DataProcessor();
processor.Process(new SqlDataReader());    // SQL 数据源
processor.Process(new FileDataReader());   // 文件数据源
processor.Process(new ApiDataReader());    // API 数据源

优点

  • 每次调用可以使用不同依赖
  • 适合策略模式

缺点

  • 每次调用都需要传递

4.4 对比总结

方式 适用场景 优点 缺点
构造函数注入 必需依赖 明确、不可变、易测试 参数可能较多
属性注入 可选依赖 灵活 可能为空、不够明确
方法注入 临时/变化依赖 每次可不同 每次都需传递

五、.NET 内置 DI 容器

5.1 基本使用

using Microsoft.Extensions.DependencyInjection;

// 1. 创建服务容器
var services = new ServiceCollection();

// 2. 注册服务(告诉容器:接口 → 实现)
services.AddTransient<IMessageService, EmailService>();
services.AddTransient<IUserRepository, UserRepository>();
services.AddTransient<UserService>();

// 3. 构建服务提供者
var serviceProvider = services.BuildServiceProvider();

// 4. 获取服务(容器自动注入所有依赖)
var userService = serviceProvider.GetRequiredService<UserService>();
userService.RegisterUser("张三", "test@example.com");

5.2 注册方式

var services = new ServiceCollection();

// 方式1:接口 → 实现
services.AddTransient<IEmailService, EmailService>();

// 方式2:直接注册实现类
services.AddTransient<UserService>();

// 方式3:工厂方法(复杂创建逻辑)
services.AddTransient<IPaymentService>(provider =>
{
    var config = provider.GetRequiredService<IConfiguration>();
    var paymentType = config["PaymentType"];
    
    return paymentType switch
    {
        "Alipay" => new AlipayService(),
        "WeChat" => new WeChatPayService(),
        _ => new DefaultPaymentService()
    };
});

// 方式4:注册实例
var logger = new FileLogger("app.log");
services.AddSingleton<ILogger>(logger);

// 方式5:尝试注册(如果已注册则跳过)
services.TryAddTransient<IEmailService, EmailService>();

5.3 获取服务

var provider = services.BuildServiceProvider();

// 方式1:获取必需服务(不存在则抛异常)
var userService = provider.GetRequiredService<UserService>();

// 方式2:获取可选服务(不存在返回 null)
var emailService = provider.GetService<IEmailService>();

// 方式3:获取所有实现(适用于一个接口多个实现)
var allHandlers = provider.GetServices<IEventHandler>();

六、服务生命周期

6.1 三种生命周期

var services = new ServiceCollection();

// 🔵 Transient(瞬时):每次请求都创建新实例
services.AddTransient<ITransientService, TransientService>();

// 🟢 Scoped(作用域):同一作用域内共享实例
services.AddScoped<IScopedService, ScopedService>();

// 🔴 Singleton(单例):整个应用程序共享一个实例
services.AddSingleton<ISingletonService, SingletonService>();

6.2 生命周期图解

  【Transient - 瞬时】
                                                                
   请求1 → new A()
   请求2 → new A()    ← 每次都是新的
   请求3 → new A()
                                                                
   适用:轻量级、无状态服务
                                                                
                                                                
  【Scoped - 作用域】
                                                                
   HTTP请求1 ┬─ 服务A ──┐
             └─ 服务B ──┼── 共享同一个实例
                        │
   HTTP请求2 ┬─ 服务A ──┼── 新实例
             └─ 服务B ──┘
                                                                
   适用:数据库上下文、工作单元
                                                                
                                                                
  【Singleton - 单例】
                                                                
   应用启动 ───────────────────────── 应用结束
        ↓                                ↓
      创建                              销毁
        └──── 所有请求共享这一个实例 ─────┘
                                                                
   适用:配置服务、缓存、日志

6.3 生命周期演示

public interface IOperationService
{
    Guid OperationId { get; }
}

public class OperationService : IOperationService
{
    // 每个实例有唯一 ID
    public Guid OperationId { get; } = Guid.NewGuid();
}

// 测试代码
var services = new ServiceCollection();
services.AddTransient<IOperationService, OperationService>();
var provider = services.BuildServiceProvider();

var service1 = provider.GetRequiredService<IOperationService>();
var service2 = provider.GetRequiredService<IOperationService>();

Console.WriteLine($"Service1: {service1.OperationId}");
Console.WriteLine($"Service2: {service2.OperationId}");
Console.WriteLine($"相同实例: {service1.OperationId == service2.OperationId}");

// 输出(Transient):
// Service1: 3f8a1b2c-xxxx-xxxx-xxxx-xxxxxxxxxxxx
// Service2: 7d4e5f6a-xxxx-xxxx-xxxx-xxxxxxxxxxxx
// 相同实例: False

// 如果改成 Singleton,输出会是:
// 相同实例: True

6.4 生命周期选择指南

生命周期 使用场景 示例
Transient 轻量级、无状态服务 验证器、工具类
Scoped 每个请求需要独立状态 DbContext、工作单元
Singleton 全局共享、线程安全 配置、缓存、日志

6.5 注意事项

  ❌ 不要在 Singleton 中注入 Scoped 或 Transient
                                                                
  // 错误!Singleton 会一直持有第一次注入的 Scoped 服务
  public class MySingleton
  {
      private readonly IScopedService _scoped;  // ❌ 危险!
  }
                                                                
  解决方案:注入 IServiceProvider,需要时再获取
  public class MySingleton
  {
      private readonly IServiceProvider _provider
                                                                
      public void DoWork()
      {
          using var scope = _provider.CreateScope();
          var scoped = scope.ServiceProvider
                            .GetRequiredService<IScopedService>()
      }
  }

七、完整项目示例

7.1 项目结构

UserRegistrationDemo/
├── Models/
│   └── User.cs
├── Services/
│   ├── IUserService.cs          # 接口和实现可以放一起
│   ├── UserService.cs
│   ├── IEmailService.cs
│   ├── EmailService.cs
│   └── FakeEmailService.cs
├── Repositories/
│   ├── IUserRepository.cs
│   └── InMemoryUserRepository.cs
├── Controllers/
│   └── UserController.cs
└── Program.cs

7.2 Models

// Models/User.cs
namespace Demo.Models;

public class User
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public string Email { get; set; } = "";
    public string PasswordHash { get; set; } = "";
    public DateTime CreatedAt { get; set; } = DateTime.Now;
}

public class RegisterRequest
{
    public string Name { get; set; } = "";
    public string Email { get; set; } = "";
    public string Password { get; set; } = "";
}

public class ServiceResult
{
    public bool Success { get; set; }
    public string Message { get; set; } = "";
    public object? Data { get; set; }

    public static ServiceResult Ok(string msg = "成功", object? data = null)
        => new() { Success = true, Message = msg, Data = data };

    public static ServiceResult Fail(string msg)
        => new() { Success = false, Message = msg };
}

7.3 Services

// Services/IUserService.cs & UserService.cs
namespace Demo.Services;

public interface IUserService
{
    ServiceResult Register(RegisterRequest request);
    ServiceResult Login(string email, string password);
    User? GetById(int id);
}

public class UserService : IUserService
{
    private readonly IUserRepository _repo;
    private readonly IEmailService _email;
    private readonly ILogger<UserService> _logger;

    // 构造函数注入所有依赖
    public UserService(
        IUserRepository repo,
        IEmailService email,
        ILogger<UserService> logger)
    {
        _repo = repo;
        _email = email;
        _logger = logger;
    }

    public ServiceResult Register(RegisterRequest request)
    {
        // 1. 验证
        if (string.IsNullOrWhiteSpace(request.Email))
            return ServiceResult.Fail("邮箱不能为空");

        if (request.Password.Length < 6)
            return ServiceResult.Fail("密码至少6位");

        // 2. 检查邮箱是否已存在
        if (_repo.GetByEmail(request.Email) != null)
            return ServiceResult.Fail("邮箱已注册");

        // 3. 创建用户
        var user = new User
        {
            Name = request.Name,
            Email = request.Email,
            PasswordHash = HashPassword(request.Password)
        };

        _repo.Add(user);
        _logger.LogInformation("用户注册成功: {Email}", user.Email);

        // 4. 发送欢迎邮件(不关心具体实现)
        _ = _email.SendAsync(user.Email, "欢迎", $"你好 {user.Name}!");

        return ServiceResult.Ok("注册成功", new { user.Id });
    }

    public ServiceResult Login(string email, string password)
    {
        var user = _repo.GetByEmail(email);

        if (user == null)
            return ServiceResult.Fail("用户不存在");

        if (user.PasswordHash != HashPassword(password))
            return ServiceResult.Fail("密码错误");

        _logger.LogInformation("用户登录: {Email}", email);
        return ServiceResult.Ok("登录成功", new { user.Id, user.Name });
    }

    public User? GetById(int id) => _repo.GetById(id);

    private static string HashPassword(string password)
    {
        using var sha = System.Security.Cryptography.SHA256.Create();
        var bytes = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
        return Convert.ToBase64String(bytes);
    }
}
// Services/IEmailService.cs & 实现
namespace Demo.Services;

public interface IEmailService
{
    Task SendAsync(string to, string subject, string body);
}

// 生产环境实现
public class EmailService : IEmailService
{
    private readonly ILogger<EmailService> _logger;

    public EmailService(ILogger<EmailService> logger) => _logger = logger;

    public async Task SendAsync(string to, string subject, string body)
    {
        _logger.LogInformation("📧 [生产] 发送邮件到 {To}: {Subject}", to, subject);
        await Task.Delay(100);  // 模拟发送
        // 实际项目:调用 SMTP 或第三方邮件服务
    }
}

// 开发环境实现(不真正发送)
public class FakeEmailService : IEmailService
{
    private readonly ILogger<FakeEmailService> _logger;

    public FakeEmailService(ILogger<FakeEmailService> logger) => _logger = logger;

    public Task SendAsync(string to, string subject, string body)
    {
        _logger.LogWarning("📧 [开发] 模拟发送邮件到 {To}: {Subject}", to, subject);
        return Task.CompletedTask;
    }
}

7.4 Repositories

// Repositories/IUserRepository.cs & 实现
namespace Demo.Repositories;

public interface IUserRepository
{
    User? GetById(int id);
    User? GetByEmail(string email);
    User Add(User user);
    IEnumerable<User> GetAll();
}

// 内存实现(开发/测试用)
public class InMemoryUserRepository : IUserRepository
{
    private readonly List<User> _users = new();
    private int _nextId = 1;

    public User? GetById(int id) 
        => _users.FirstOrDefault(u => u.Id == id);

    public User? GetByEmail(string email)
        => _users.FirstOrDefault(u => 
            u.Email.Equals(email, StringComparison.OrdinalIgnoreCase));

    public User Add(User user)
    {
        user.Id = _nextId++;
        _users.Add(user);
        return user;
    }

    public IEnumerable<User> GetAll() => _users.ToList();
}

7.5 Controller

// Controllers/UserController.cs
namespace Demo.Controllers;

[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
    private readonly IUserService _userService;

    // 只注入 IUserService,不关心具体实现
    public UserController(IUserService userService)
    {
        _userService = userService;
    }

    [HttpPost("register")]
    public IActionResult Register([FromBody] RegisterRequest request)
    {
        var result = _userService.Register(request);
        return result.Success ? Ok(result) : BadRequest(result);
    }

    [HttpPost("login")]
    public IActionResult Login([FromBody] LoginRequest request)
    {
        var result = _userService.Login(request.Email, request.Password);
        return result.Success ? Ok(result) : Unauthorized(result);
    }

    [HttpGet("{id}")]
    public IActionResult Get(int id)
    {
        var user = _userService.GetById(id);
        return user != null ? Ok(user) : NotFound();
    }
}

public record LoginRequest(string Email, string Password);

7.6 Program.cs(组合根 - 核心配置)

// Program.cs
using Demo.Services;
using Demo.Repositories;

var builder = WebApplication.CreateBuilder(args);

// 
// 依赖注入配置(核心!)
// 

// 1. 注册仓储
if (builder.Environment.IsDevelopment())
{
    // 开发环境:内存数据库
    builder.Services.AddSingleton<IUserRepository, InMemoryUserRepository>();
}
else
{
    // 生产环境:MySQL(示例)
    // builder.Services.AddScoped<IUserRepository, MySqlUserRepository>();
    builder.Services.AddSingleton<IUserRepository, InMemoryUserRepository>();
}

// 2. 注册邮件服务
if (builder.Environment.IsDevelopment())
{
    // 开发环境:假服务,不真正发送
    builder.Services.AddTransient<IEmailService, FakeEmailService>();
}
else
{
    // 生产环境:真实邮件服务
    builder.Services.AddTransient<IEmailService, EmailService>();
}

// 3. 注册业务服务
builder.Services.AddScoped<IUserService, UserService>();

// 4. 添加控制器
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// 

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.MapControllers();

Console.WriteLine($"🚀 环境: {app.Environment.EnvironmentName}");
Console.WriteLine("📍 访问: http://localhost:5000/swagger");

app.Run();

八、单元测试

8.1 使用 Mock 测试

using Moq;
using Xunit;
using Demo.Services;
using Demo.Repositories;
using Demo.Models;
using Microsoft.Extensions.Logging;

public class UserServiceTests
{
    private readonly Mock<IUserRepository> _mockRepo;
    private readonly Mock<IEmailService> _mockEmail;
    private readonly Mock<ILogger<UserService>> _mockLogger;
    private readonly UserService _userService;

    public UserServiceTests()
    {
        // 创建 Mock 对象
        _mockRepo = new Mock<IUserRepository>();
        _mockEmail = new Mock<IEmailService>();
        _mockLogger = new Mock<ILogger<UserService>>();

        // 注入 Mock 对象创建被测试服务
        _userService = new UserService(
            _mockRepo.Object,
            _mockEmail.Object,
            _mockLogger.Object);
    }

    [Fact]
    public void Register_ValidRequest_ShouldSucceed()
    {
        // Arrange(准备)
        var request = new RegisterRequest
        {
            Name = "张三",
            Email = "test@example.com",
            Password = "123456"
        };

        _mockRepo.Setup(r => r.GetByEmail(request.Email))
                 .Returns((User?)null);  // 模拟邮箱不存在
        _mockRepo.Setup(r => r.Add(It.IsAny<User>()))
                 .Returns((User u) => { u.Id = 1; return u; });

        // Act(执行)
        var result = _userService.Register(request);

        // Assert(断言)
        Assert.True(result.Success);
        Assert.Equal("注册成功", result.Message);
        
        // 验证邮件服务被调用
        _mockEmail.Verify(e => e.SendAsync(
            request.Email, 
            It.IsAny<string>(), 
            It.IsAny<string>()), Times.Once);
    }

    [Fact]
    public void Register_DuplicateEmail_ShouldFail()
    {
        // Arrange
        var request = new RegisterRequest
        {
            Name = "张三",
            Email = "existing@example.com",
            Password = "123456"
        };

        // 模拟邮箱已存在
        _mockRepo.Setup(r => r.GetByEmail(request.Email))
                 .Returns(new User { Email = request.Email });

        // Act
        var result = _userService.Register(request);

        // Assert
        Assert.False(result.Success);
        Assert.Equal("邮箱已注册", result.Message);
        
        // 验证没有调用 Add
        _mockRepo.Verify(r => r.Add(It.IsAny<User>()), Times.Never);
        
        // 验证没有发送邮件
        _mockEmail.Verify(e => e.SendAsync(
            It.IsAny<string>(), 
            It.IsAny<string>(), 
            It.IsAny<string>()), Times.Never);
    }

    [Theory]
    [InlineData("", "test@test.com", "123456", "邮箱不能为空")]
    [InlineData("张三", "test@test.com", "123", "密码至少6位")]
    public void Register_InvalidInput_ShouldFail(
        string name, string email, string password, string expectedError)
    {
        var request = new RegisterRequest
        {
            Name = name,
            Email = email,
            Password = password
        };

        var result = _userService.Register(request);

        Assert.False(result.Success);
        Assert.Contains(expectedError, result.Message);
    }
}

九、最佳实践

9.1 DO(推荐做法)

// ✅ 1. 依赖接口,而非实现
public class OrderService
{
    private readonly IPaymentService _payment;  // ✅ 接口
    
    public OrderService(IPaymentService payment)
    {
        _payment = payment;
    }
}

// ✅ 2. 使用构造函数注入
public class UserService
{
    private readonly IRepository _repo;
    
    public UserService(IRepository repo)  // ✅ 构造函数注入
    {
        _repo = repo ?? throw new ArgumentNullException(nameof(repo));
    }
}

// ✅ 3. 将依赖标记为 readonly
private readonly IEmailService _email;  // ✅ 不可变

// ✅ 4. 在组合根统一配置
// Program.cs
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IEmailService, EmailService>();

// ✅ 5. 使用适当的生命周期
builder.Services.AddSingleton<IConfiguration>();  // 配置
builder.Services.AddScoped<DbContext>();           // 数据库上下文
builder.Services.AddTransient<IValidator>();       // 无状态服务

9.2 DON'T(避免做法)

// ❌ 1. 不要在类内部 new 依赖
public class UserService
{
    private EmailService _email = new EmailService();  // ❌ 硬编码
}

// ❌ 2. 不要使用服务定位器模式
public class UserService
{
    public void DoWork()
    {
        // ❌ 隐藏依赖关系
        var email = ServiceLocator.Get<IEmailService>();
    }
}

// ❌ 3. 不要在 Singleton 中注入 Scoped
public class MySingleton  // Singleton
{
    private readonly IScopedService _scoped;  // ❌ 危险!
}

// ❌ 4. 不要注入太多依赖(超过 3-4 个考虑重构)
public class GodService
{
    public GodService(
        IService1 s1, IService2 s2, IService3 s3,
        IService4 s4, IService5 s5, IService6 s6)  // ❌ 太多了
    { }
}

// ❌ 5. 不要在构造函数中做复杂逻辑
public class UserService
{
    public UserService(IRepository repo)
    {
        _repo = repo;
        _repo.Initialize();  // ❌ 避免在构造函数中调用方法
        LoadData();          // ❌ 避免复杂初始化
    }
}

9.3 设计原则

  1. 【依赖抽象】
     依赖接口,而非具体类
     IEmailService, 不是 SmtpEmailService                        │
                                                                
  2. 【构造函数注入】
     必需依赖通过构造函数注入
     依赖关系清晰可见
                                                                
  3. 【单一职责】
     类只负责自己的业务逻辑
     不负责创建依赖
                                                                
  4. 【组合根配置】
     所有依赖配置集中在一个地方(Program.cs)
     业务代码不关心依赖如何创建
                                                                
  5. 【接口隔离】
     接口要小而专注
     不要一个接口包含太多方法

十、常见问题解答

Q1: 什么时候不需要依赖注入?

// ✅ 这些情况不需要 DI:

// 1. 纯函数/静态工具方法
Math.Max(1, 2);
string.IsNullOrEmpty(s);
DateTime.Now;

// 2. 简单值对象
var user = new User { Name = "张三" };

// 3. 一次性脚本或小工具
Console.WriteLine("Hello");

// 4. 确定不会变化、不需要测试的简单逻辑

Q2: 接口和实现放一个文件还是分开?

// 小项目:放一起,简洁方便
// Services/UserService.cs
public interface IUserService { }
public class UserService : IUserService { }

// 大项目/多实现:分开放
// Interfaces/IMessageService.cs
// Services/EmailService.cs
// Services/SmsService.cs
// Services/WeChatService.cs

Q3: 如何处理多个实现?

// 方式1:注册所有实现,注入集合
services.AddTransient<INotification, EmailNotification>();
services.AddTransient<INotification, SmsNotification>();

public class NotificationManager
{
    private readonly IEnumerable<INotification> _notifications;
    
    public NotificationManager(IEnumerable<INotification> notifications)
    {
        _notifications = notifications;
    }
}

// 方式2:使用键控服务(.NET 8+)
services.AddKeyedTransient<INotification, EmailNotification>("email");
services.AddKeyedTransient<INotification, SmsNotification>("sms");

public class MyService
{
    public MyService(
        [FromKeyedServices("email")] INotification email,
        [FromKeyedServices("sms")] INotification sms)
    { }
}

// 方式3:工厂模式
services.AddTransient<Func<string, INotification>>(provider => key =>
{
    return key switch
    {
        "email" => provider.GetRequiredService<EmailNotification>(),
        "sms" => provider.GetRequiredService<SmsNotification>(),
        _ => throw new ArgumentException($"Unknown key: {key}")
    };
});

Q4: 循环依赖怎么办?

// ❌ 循环依赖
public class A { public A(B b) { } }
public class B { public B(A a) { } }

// ✅ 解决方案1:引入第三个服务
public class A { public A(C c) { } }
public class B { public B(C c) { } }
public class C { }  // 共同依赖

// ✅ 解决方案2:使用 Lazy<T>
public class A
{
    private readonly Lazy<B> _b;
    public A(Lazy<B> b) { _b = b; }
}

// ✅ 解决方案3:方法注入替代构造函数注入
public class A
{
    public void DoWork(B b) { }  // 需要时才传入
}

Q5: 如何注入配置?

// appsettings.json
{
  "EmailSettings": {
    "SmtpServer": "smtp.example.com",
    "Port": 587
  }
}

// 配置类
public class EmailSettings
{
    public string SmtpServer { get; set; } = "";
    public int Port { get; set; }
}

// 注册
builder.Services.Configure<EmailSettings>(
    builder.Configuration.GetSection("EmailSettings"));

// 使用
public class EmailService
{
    private readonly EmailSettings _settings;
    
    public EmailService(IOptions<EmailSettings> options)
    {
        _settings = options.Value;
    }
}

十一、总结

11.1 核心要点

  【概念】
  • IoC:控制权从类内部转移到外部
  • DI:通过注入方式提供依赖,是 IoC 的实现方式
                                                            
  【好处】
  • 松耦合:依赖接口,不依赖实现
  • 可测试:可以注入 Mock 对象
  • 可维护:依赖关系清晰
  • 可扩展:新增实现不影响现有代码
  • 灵活配置:开发/生产环境使用不同实现
                                                            
  【使用方式】
  • 定义接口 → 创建实现 → 构造函数注入 → 组合根配置
                                                            
  【生命周期】
  • Transient:每次新建
  • Scoped:请求内共享
  • Singleton:全局共享

11.2 一句话总结

依赖注入的核心不是"不用 new",而是让业务类不知道也不关心具体用的是什么实现,这个决策权交给了外部配置。

11.3 适用场景

场景 是否使用 DI
学习/Demo/脚本 可选
小型项目 推荐
需要单元测试 必须
需要切换实现 必须
团队协作项目 强烈推荐
中大型项目 必须

11.4 快速参考

// 1. 定义接口
public interface IService { void DoWork(); }

// 2. 实现接口
public class ServiceA : IService { public void DoWork() { } }
public class ServiceB : IService { public void DoWork() { } }

// 3. 构造函数注入
public class Consumer
{
    private readonly IService _service;
    public Consumer(IService service) => _service = service;
}

// 4. 注册服务
builder.Services.AddTransient<IService, ServiceA>();
builder.Services.AddScoped<Consumer>();

// 5. 使用
var consumer = provider.GetRequiredService<Consumer>();
登录写评论