【.Net技术栈梳理】08-控制反转(IoC)与依赖注入(DI)

发布于:2025-09-13 ⋅ 阅读:(20) ⋅ 点赞:(0)

IoC 和 DI 是为了解决 OOP 中对象间耦合问题而生的设计原则和模式。

1. 控制反转 (IoC)

1.1 概念

IoC是一种设计原则,其核心是将程序的控制权反转。

  • 传统控制流程:在传统代码中,一个对象需要依赖另一个对象时,它会自己主动去 new 这个依赖(例如 var service = new EmailService();)。它控制着依赖的创建和生命周期。
public class OrderService
{
    private readonly EmailService _emailService;

    public OrderService()
    {
        // OrderService 控制着 EmailService 的创建和生命周期
        _emailService = new EmailService(); // "主动"获取依赖
    }

    public void ProcessOrder()
    {
        // ... 处理订单逻辑
        _emailService.Send(); // 使用依赖
    }
}
  • 反转后的控制流程:对象的依赖不再由自己创建,而是由一个外部容器(IoC 容器)来创建并“注入”给它。控制权从程序内部转移到了外部容器。
public class OrderService
{
    private readonly IEmailService _emailService;

    // 依赖通过构造函数"注入"进来
    public OrderService(IEmailService emailService) // 依赖抽象
    {
        // OrderService 失去了对依赖的控制权,只是被动接收
        _emailService = emailService;
    }

    public void ProcessOrder()
    {
        // ... 处理订单逻辑
        _emailService.Send(); // 使用依赖
    }
}
  • 比喻:传统方式就像你自己去厨房做菜(主动控制)。IoC 就像去餐厅点菜,你只需说“我要一份牛排”(声明需求),厨师(容器)会做好并端给你(注入依赖),你失去了做菜的控制权,但获得了更大的灵活性。

1.2 好处

  1. 解耦:OrderService 不再依赖具体的 EmailService,只依赖接口 IEmailService。

  2. 可测试性:在单元测试中,你可以轻松注入一个“模拟”的 IEmailService(使用 Moq 等框架)。

  3. 可扩展性:更换实现(如从 EmailService 换成 SendGridEmailService)只需修改配置,无需修改 OrderService 的代码。

1.3 IoC 容器

  • 概念:一个专门负责依赖创建和依赖注入的框架组件。它是 IoC 原则的物理实现。

  • 工作流程:

    1. 注册:告诉容器,当请求某个接口(抽象)时,应该创建哪个具体的实现类,并如何管理其生命周期(单例、每次请求创建新实例等)。

    2. 解析:在应用程序的入口点或需要的地方,请求容器创建一个对象(如 OrderService)。容器会分析其构造函数,发现它需要 IEmailService,于是自动创建 IEmailService 的具体实例,并注入到 OrderService 中,最后将完全构建好的 OrderService 返回。

2. 依赖注入 (DI)

2.1 概念

实现控制反转的一种具体技术模式。通过“注入”的方式(通过构造函数、属性或方法参数等方式)来实现控制权的反转,将一个对象的依赖项从外部传递给它。

  • 原理
    基于控制反转(IoC) 和依赖倒置原则(DIP),将依赖的创建权从使用方剥离,交给外部容器管理,从而实现解耦。

  • 目的:解耦。让类不再直接创建它的依赖,而是接收它们。这使得代码更易于测试(例如,在测试时可以轻松注入一个“模拟”依赖)和维护。

2.2 DI 的三种注入方式

// 1. 构造函数注入 (最常用、最推荐)
public class OrderService
{
    private readonly IEmailService _emailService;
    // 依赖通过构造函数传入
    public OrderService(IEmailService emailService)
    {
        _emailService = emailService;
    }
    public void ProcessOrder()
    {
        // ... 业务逻辑
        _emailService.SendConfirmation();
    }
}

// 2. 属性注入
public class OrderService
{
    // 依赖通过公共属性设置
    public IEmailService EmailService { get; set; }
}

// 3. 方法注入
public class OrderService
{
    // 依赖通过方法参数传入
    public void ProcessOrder(IEmailService emailService)
    {
        emailService.SendConfirmation();
    }
}

2.3 DI 的核心组件

.NET 的 DI 系统主要由三个部分协作完成:

  1. 服务集合 (IServiceCollection):用于注册服务。

  2. 服务提供者 (IServiceProvider):用于解析(构建)服务,是真正的容器。

  3. 构造函数:用于注入依赖

应用程序宿主 IServiceCollection IServiceProvider 控制器 服务 启动阶段 1. 注册服务:AddScoped<IService, Service> 2. 构建服务提供程序:BuildServiceProvider(SC) 容器根据注册信息初始化 运行时阶段(每个请求) 3. 创建作用域:CreateScope() 返回IServiceScope 4. 从作用域解析控制器:GetService<Controller>() 5. 解析控制器的依赖(如IService) 6. 创建服务实例(根据生命周期) 7. 返回控制器实例(已注入服务) 8. 调用服务方法 9. 返回结果 请求结束 10. 释放作用域 应用程序宿主 IServiceCollection IServiceProvider 控制器 服务
  1. 启动阶段:

    • 应用程序启动时,在ConfigureServices方法中,所有服务(包括接口和对应的实现类)都被注册到IServiceCollection中,并指定它们的生命周期(Transient、Scoped或Singleton)。

    • 然后,使用IServiceCollection构建出IServiceProvider(即DI容器)。容器会根据注册信息来管理服务的生命周期和创建。

  2. 运行时阶段(针对每个请求):

    • 当一个HTTP请求到达时,ASP.NET Core会为该请求创建一个服务作用域(IServiceScope)。

    • 在该作用域内,当需要实例化某个组件(如MVC控制器)时,容器会接管该组件的创建过程。

    • 容器检查该组件的构造函数,识别出它所依赖的服务。

    • 容器根据注册的生命周期设置,决定是创建一个新的服务实例还是重用现有的实例(如果是Singleton则重用容器根中的实例,如果是Scoped则重用当前作用域内的实例,如果是Transient则每次都创建新的实例)。

    • 容器递归地解析所有依赖,直到所有依赖都被解析完毕,然后使用这些依赖实例化目标组件。

    • 控制器(或其他组件)被实例化后,就可以使用注入的服务来处理请求。

    • 请求处理结束后,服务作用域被释放,所有在该作用域内创建的、实现了IDisposable接口的Scoped和Transient服务都会被 dispose。

这样,依赖注入容器就完成了它的角色:管理服务的生命周期并在需要时注入依赖。

2.4 依赖的生命周期

这是 DI 中至关重要的一环,它决定了对象的存活时间和复用范围。.NET DI 容器支持三种生命周期:

生命周期 注册方法 说明 示例场景
瞬时 AddTransient<T>() 每次请求都会创建一个新的实例 无状态的服务、轻量级服务。例如,一个简单的计算器 Calculator。
作用域 AddScoped<T>() 同一个作用域内(如一次 Web 请求),每次请求返回同一个实例;不同作用域则实例不同。 需要在其范围内保持状态的服务。这是绝大多数应用服务的默认选择,如 DbContext(数据库上下文)。
单例 AddSingleton<T>() 整个应用程序生命周期内只创建一个实例,所有请求共享该实例。 全局状态、缓存、配置读取器、日志服务。需要是线程安全的。

2.5 DI 在 ASP.NET Core 中的具体实现

在 Program.cs 中,整个流程非常清晰:

// 1. 创建宿主构建器,它内部已经初始化了一个 IServiceCollection
var builder = WebApplication.CreateBuilder(args);

// 2. 【注册阶段】向 IServiceCollection 注册服务
// - 注册控制器相关服务(MVC)
builder.Services.AddControllers();

// - 注册应用自定义服务
builder.Services.AddScoped<IOrderService, OrderService>(); // 作用域服务
builder.Services.AddSingleton<ILoggerService, FileLoggerService>(); // 单例服务
builder.Services.AddTransient<IEmailValidator, EmailValidator>(); // 瞬时服务

// 3. 【构建阶段】构建 IServiceProvider
var app = builder.Build(); // 这里内部会调用 builder.Services.BuildServiceProvider()

// 4. 【配置中间件管道】
app.UseRouting();
app.UseAuthorization();

// 5. 【映射端点】当请求到来时,路由引擎会决定由哪个控制器处理
app.MapControllers();

// 6. 【运行】
app.Run();

当一个 HTTP 请求到达时:

  1. 服务器接收请求,创建 HttpContext。

  2. 中间件管道开始处理。当到达 EndpointMiddleware 时,它知道要调用某个 Controller 的 Action 方法。

  3. 它向根容器(或从根容器创建的一个作用域)请求解析该 Controller 类型。

  4. 容器开始工作,分析 Controller 的构造函数(如 public HomeController(IOrderService orderService))。

  5. 容器接着去解析 IOrderService,发现它被映射到 OrderService。

  6. 容器分析 OrderService 的构造函数,递归地解析它的所有依赖,直到整个对象树构建完毕。

  7. 容器最终将完全构建好的 Controller 实例返回给中间件。

  8. Controller 的 Action 方法被调用,它使用已注入的 IOrderService 来完成业务逻辑。

  9. 请求处理结束后,如果创建了作用域,则该作用域被释放,其中所有的 IDisposable 资源会被处理。

2.6 DI 工作流程

工作流程:

  1. 注册:在启动时,将所有服务(接口、实现、生命周期)告知 IServiceCollection。

  2. 构建:将 IServiceCollection 转换为 IServiceProvider(容器)。

  3. 解析:在运行时,容器负责递归地分析构造函数、创建实例并注入所有依赖。

数据流转
数据(对象实例)的创建和传递完全由容器控制,从最底层的依赖开始构建,最终组合成所需的目标对象,并通过构造函数注入。

总结关系:IoC 是目的(反转控制权),DI 是手段(通过注入实现控制反转),而 IoC 容器是工具(自动化实现 DI)。