当前位置: 首页 > news >正文

C#_依赖注入(DI)


4.1 DI的核心概念与价值(解耦、可测试性)

在深入代码之前,我们必须先理解“为什么”。依赖注入解决了两个最基本的问题:耦合可测试性

4.1.1 问题:紧耦合的弊端

让我们回顾一下没有DI的“传统”代码:

public class OrderService {// OrderService  tightly coupled to SqlOrderRepositoryprivate readonly SqlOrderRepository _repository = new SqlOrderRepository();public void ProcessOrder(Order order) {// ... business logic ..._repository.Save(order);}
}public class SqlOrderRepository {public void Save(Order order) {// Directly using SqlConnection - coupled to SQL Server!using (var connection = new SqlConnection("HardCodedConnectionString")){// ... save order ...}}
}

这段代码存在多个耦合问题:

  1. OrderService 耦合于 SqlOrderRepository:它直接实例化了一个具体的实现。如果想换成 FileOrderRepositoryMockOrderRepository,必须修改 OrderService 的源代码。
  2. SqlOrderRepository 耦合于SQL Server:它直接使用了 SqlConnection 和硬编码的连接字符串。
  3. 难以测试:如何对 ProcessOrder 方法进行单元测试?测试会不可避免地连接到真实的SQL Server数据库,这变成了一个慢速、脆弱、需要配置的集成测试。你无法将一个“Mock”仓库注入进去。

这种代码被形象地称为“粘合剂代码”(glue code)——组件自己创建和管理它所依赖的对象,导致它们无法被拆开。

4.1.2 解决方案:依赖注入

依赖注入通过一种称为“控制反转(IoC)”的机制来解决这个问题。控制反转是一个更广泛的原则,指将程序流程的控制权从应用代码转移给一个外部框架或容器(在这是DI容器)。依赖注入是实现IoC的一种特定技术。

DI的核心思想非常简单:一个类不应该自己创建它的依赖项;相反,它的依赖项应该被“注入”到它里面。

重构后的代码:

// 1. 依赖于抽象
public class OrderService {private readonly IOrderRepository _repository;// 2. 依赖通过构造函数“注入”public OrderService(IOrderRepository repository) {_repository = repository; // Assignment is done by an external component}public void ProcessOrder(Order order) {// ... business logic ..._repository.Save(order);}
}// 3. 抽象接口
public interface IOrderRepository {void Save(Order order);
}// 4. 具体实现
public class SqlOrderRepository : IOrderRepository {private readonly string _connectionString;// 5. 它的依赖(连接字符串)也可以被注入!public SqlOrderRepository(string connectionString) {_connectionString = connectionString;}public void Save(Order order) { ... }
}

4.1.3 DI带来的核心价值

  1. 解耦(Loose Coupling)OrderService 现在只依赖于 IOrderRepository 接口。它完全不知道也不关心接口是如何实现的。这意味着你可以轻松替换实现,而无需修改 OrderService 的代码。这是依赖倒置原则(DIP)开放/封闭原则(OCP) 的直接体现。

  2. 可测试性(Testability):这是DI带来的最立竿见影的好处。在单元测试中,你可以注入一个模拟对象(Mock)。

    // 使用 Moq 框架进行测试
    [Test]
    public void ProcessOrder_Should_Call_Repository_Save() {// Arrangevar mockRepo = new Mock<IOrderRepository>();var orderService = new OrderService(mockRepo.Object); // Inject the mockvar testOrder = new Order();// ActorderService.ProcessOrder(testOrder);// AssertmockRepo.Verify(r => r.Save(testOrder), Times.Once); // Verify Save was called
    }
    

    这个测试快速、可靠,且不依赖任何外部基础设施。它只测试 OrderService 的逻辑是否正确调用了仓库。

  3. 可维护性和灵活性:应用程序的配置和组件的组装被集中到了应用程序的入口点(“组合根”)。更改应用程序的行为(例如,为不同的环境切换不同的实现)变得非常简单和清晰。

  4. 更好的代码组织:明确的依赖关系迫使开发者思考类的职责和边界,通常会导致更好的API设计和更清晰的架构。

架构师视角
采用依赖注入不仅仅是一种技术选择,更是一种文化和技术决策。它要求团队遵循一套明确的规则(如“所有服务都通过构造函数注入”),但这套规则所带来的长期好处——可测试性、可维护性和灵活性——远远超过初期的学习成本。作为架构师,你应在项目伊始就强制推行DI,并将其作为所有架构讨论的基础。


4.2 .NET内置DI容器详解与生命周期管理

虽然你可以手动实现依赖注入(称为“Pure DI”),但对于大型项目,使用一个DI容器(或称IoC容器)来自动化管理对象的创建和生命周期是更高效的选择。ASP.NET Core自带了一个轻量级、高性能且功能齐全的DI容器。

4.2.1 服务注册(Service Registration)

在应用程序启动时(通常在 Program.csStartup.cs 中),你需要向容器注册所有服务及其对应的抽象。

// 在 Program.cs 中构建 Host 或 Application Builder
var builder = WebApplication.CreateBuilder(args);// 在 IServiceCollection 上调用方法进行服务注册
builder.Services.AddScoped<IOrderService, OrderService>(); // Registering a service
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();// 注册其他服务...
// builder.Services.AddSingleton<IMySingletonService, MySingletonService>();
// builder.Services.AddTransient<IMyTransientService, MyTransientService>();var app = builder.Build();

.AddScoped<TService, TImplementation>() 是注册方法之一。.AddSingleton<>.AddTransient<> 是另外两个,它们定义了服务的生命周期。

4.2.2 服务生命周期(Service Lifetimes)

理解并正确选择服务生命周期是使用DI容器的关键。.NET DI容器提供了三种基本生命周期:

生命周期注册方法描述
瞬时(Transient)AddTransient<T>()每次请求都会创建一个新的实例。最适合轻量级、无状态的(Stateless)服务。
作用域(Scoped)AddScoped<T>()每个客户端请求(作用域) 创建一个实例。在Web应用中,一个HTTP请求就是一个作用域。这是处理“每个请求一个上下文”(如Entity Framework Core的 DbContext)的理想选择。
单例(Singleton)AddSingleton<T>()整个应用程序生命周期内只创建一个实例。所有请求共享同一个实例。用于创建全局、线程安全的共享服务(如配置、日志器、缓存)。

生命周期选择指南:

  • AddTransient:用于无状态的服务。如果服务很简单,并且不依赖任何特定于请求的状态,通常选择这个。例如,一个简单的计算器服务、DTO映射器。
  • AddScoped:这是最常用的生命周期,用于代表一个工作单元的服务。最重要的例子是Entity Framework Core的 DbContext。在一个Web请求中,所有类都共享同一个 DbContext 实例,这确保了所有操作在同一个逻辑事务中进行,并且变更跟踪保持一致。
  • AddSingleton:用于真正的应用程序全局单例。必须确保该服务是线程安全的,因为多个请求会同时访问它。常用于缓存、应用程序配置、日志器(如 ILogger<T> 本身就是一个单例)。

重要警告避免 captive dependency!即,一个生命周期长的服务(如Singleton)不应该依赖于一个生命周期短的服务(如Scoped或Transient)。这会导致短生命周期服务的行为像单例一样,可能引起难以发现的bug。容器在运行时通常会对此抛出异常。

4.2.3 服务解析(Service Resolution)

服务通常在构造函数中自动解析(“构造函数注入”)。这是最推荐的方式。

// Controller中自动解析
[ApiController]
public class OrderController : ControllerBase {private readonly IOrderService _orderService; // 依赖// 容器会自动注入所需的IOrderService实现public OrderController(IOrderService orderService) {_orderService = orderService;}[HttpPost]public IActionResult CreateOrder(Order order) {_orderService.ProcessOrder(order);return Ok();}
}

在某些边缘情况下(如在 Program.cs 的早期配置或是在静态方法中),你可能需要手动从容器中解析服务。可以通过 IServiceProvider 来实现。

// 在构建完app之后,可以获取根服务提供器(谨慎使用!)
var app = builder.Build();// 创建一个短暂的作用域来解析Scoped服务
using (var serviceScope = app.Services.CreateScope()) {var scopedService = serviceScope.ServiceProvider.GetRequiredService<IMyScopedService>();scopedService.DoSomething();
}

注意:应尽量避免手动解析(称为“服务定位器模式”),因为它会隐藏类的依赖关系,使代码更难理解和测试。始终优先使用构造函数注入。

4.2.4 其他注册方式

容器提供了灵活的注册API。

// 1. 注册现有实例(常用于选项模式)
var myInstance = new MySingletonService();
builder.Services.AddSingleton<IMySingletonService>(myInstance);// 2. 注册工厂方法(用于复杂的初始化逻辑)
builder.Services.AddScoped<IService>(serviceProvider => {var otherService = serviceProvider.GetService<IOtherService>();return new ServiceImpl(otherService, "some parameter");
});// 3. 注册泛型接口
// 假设有 IRepository<T>
builder.Services.AddScoped(typeof(IRepository<>), typeof(GenericRepository<>));

.NET内置容器总结
对于绝大多数应用程序,.NET内置的DI容器已经完全够用。它性能优异、与框架无缝集成、API简单直观。它的设计哲学是“简单够用”,避免了更高级容器可能带来的复杂性。

http://www.dtcms.com/a/348215.html

相关文章:

  • vulnhub-billu_b0x靶机渗透
  • HPA 数据库实用指南:解决科研文章逻辑衔接难题的实操教程
  • 05 线性代数【动手学深度学习v2】
  • 构建wezzer平台!
  • VirtualBox 中安装 Ubuntu 22.04
  • daily notes[5]
  • 计算机视觉与自然语言处理技术体系概述
  • 深度学习之第一课深度学习的入门
  • Go语言IDE安装与配置(VSCode)
  • VSCode远程开发实战:SSH连接服务器详解(附仙宫云平台示例)
  • Linux综合练习(dns,dhcp,nfs,web)
  • Spring Boot 中 @Controller与 @RestController的区别及 404 错误解析
  • 【嵌入式汇编基础】-数据处理指令(二)
  • VSCode+Qt+CMake详细地讲解
  • VSCode无权访问扩展市场
  • QT面试题总结(持续更新)
  • Java的IO流和IO流的Buffer包装类
  • Postman参数类型、功能、用途及 后端接口接收详解【接口调试工具】
  • 单链表:数据结构中的高效指针艺术
  • Shell脚本-until应用案例
  • C/C++数据结构之循环链表
  • Dify 部署+deepseek+python调用(win11+dockerdesktop)
  • 大数据、hadoop、爬虫、spark项目开发设计之基于数据挖掘的交通流量分析研究
  • 【运维进阶】case、for、while、until语句大合集
  • rust语言 (1.88) egui (0.32.1) 学习笔记(逐行注释)(十七)设置主题
  • CF757F 题解
  • SO_REUSEADDR
  • RuoYi-Vue3项目中Swagger接口测试404,端口问题解析排查
  • 【力扣】2623. 记忆函数——函数转换
  • 硬件抽象层 (HAL, Hardware Abstraction Layer)的简单使用示例