文章目录
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 好处
解耦:OrderService 不再依赖具体的 EmailService,只依赖接口 IEmailService。
可测试性:在单元测试中,你可以轻松注入一个“模拟”的 IEmailService(使用 Moq 等框架)。
可扩展性:更换实现(如从 EmailService 换成 SendGridEmailService)只需修改配置,无需修改 OrderService 的代码。
1.3 IoC 容器
概念:一个专门负责依赖创建和依赖注入的框架组件。它是 IoC 原则的物理实现。
工作流程:
注册:告诉容器,当请求某个接口(抽象)时,应该创建哪个具体的实现类,并如何管理其生命周期(单例、每次请求创建新实例等)。
解析:在应用程序的入口点或需要的地方,请求容器创建一个对象(如 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 系统主要由三个部分协作完成:
服务集合 (IServiceCollection):用于注册服务。
服务提供者 (IServiceProvider):用于解析(构建)服务,是真正的容器。
构造函数:用于注入依赖
启动阶段:
应用程序启动时,在ConfigureServices方法中,所有服务(包括接口和对应的实现类)都被注册到IServiceCollection中,并指定它们的生命周期(Transient、Scoped或Singleton)。
然后,使用IServiceCollection构建出IServiceProvider(即DI容器)。容器会根据注册信息来管理服务的生命周期和创建。
运行时阶段(针对每个请求):
当一个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 请求到达时:
服务器接收请求,创建 HttpContext。
中间件管道开始处理。当到达 EndpointMiddleware 时,它知道要调用某个 Controller 的 Action 方法。
它向根容器(或从根容器创建的一个作用域)请求解析该 Controller 类型。
容器开始工作,分析 Controller 的构造函数(如 public HomeController(IOrderService orderService))。
容器接着去解析 IOrderService,发现它被映射到 OrderService。
容器分析 OrderService 的构造函数,递归地解析它的所有依赖,直到整个对象树构建完毕。
容器最终将完全构建好的 Controller 实例返回给中间件。
Controller 的 Action 方法被调用,它使用已注入的 IOrderService 来完成业务逻辑。
请求处理结束后,如果创建了作用域,则该作用域被释放,其中所有的 IDisposable 资源会被处理。
2.6 DI 工作流程
工作流程:
注册:在启动时,将所有服务(接口、实现、生命周期)告知 IServiceCollection。
构建:将 IServiceCollection 转换为 IServiceProvider(容器)。
解析:在运行时,容器负责递归地分析构造函数、创建实例并注入所有依赖。
数据流转:
数据(对象实例)的创建和传递完全由容器控制,从最底层的依赖开始构建,最终组合成所需的目标对象,并通过构造函数注入。
总结关系:IoC 是目的(反转控制权),DI 是手段(通过注入实现控制反转),而 IoC 容器是工具(自动化实现 DI)。