.NET 依赖注入(Dependency Injection, DI)技术笔记
一、依赖倒转原则(Dependency Inversion Principle, DIP)
核心思想
高层模块不应该依赖低层模块,而应该依赖抽象。这是SOLID原则中的重要组成部分,它要求:
高层模块不应该依赖具体实现
低层模块不应该定义抽象
抽象不应该依赖细节,细节应该依赖抽象
为什么要依赖倒转?
依赖倒转原则让我们能够:
专注于单一类的业务逻辑本身,而不是纠结于对象如何创建
通过依赖抽象而非具体实现,降低模块间的耦合度
提高代码的可测试性和可维护性
使得系统更容易扩展和修改
与控制反转的关系
依赖倒转原则是控制反转(IoC)思想的基础。DIP提供了理论指导,而IoC则是实现这一原则的具体方法论。
二、控制反转(IoC, Inversion of Control)
什么是控制反转?
控制反转是一种设计思想,它将对象创建与依赖关系的控制权交给外部容器,而不是由对象自己控制。传统方式中,对象自己负责创建或查找它所依赖的对象;而在IoC模式下,这些工作由外部容器完成。
两种典型实现方式
1. 服务定位器(Service Locator)
提供一个中心化的注册表
对象主动从服务定位器中查找所需的依赖
存在隐式依赖,不够透明
2. 依赖注入(Dependency Injection, DI)
对象被动接收依赖
依赖通过构造函数、属性或方法注入
依赖关系显式声明,更加透明
.NET 中的选择
.NET 中主要使用 DI 实现 IoC。从 .NET Core 开始,依赖注入已经成为框架的核心功能,内置了强大的 DI 容器。
三、.NET 中的依赖注入机制
1. 服务与接口
服务的概念
在 DI 上下文中,"服务"指的是需要被使用的对象,它可以是:
业务逻辑组件
数据访问层
工具类
任何可重用的功能模块
面向接口编程
一般通过接口定义服务类型,通过类实现具体逻辑:
接口定义契约和行为规范
具体类实现业务逻辑
依赖方只需要知道接口,不需要知道具体实现
服务注册
在 .NET 的 DI 容器中注册服务:
// 注册服务
services.AddTransient<IService, ServiceImpl>();
services.AddScoped<IRepository, SqlRepository>();
services.AddSingleton<ICache, MemoryCache>();
重点:注册时使用接口类型,获取对象时也要使用接口。这是面向接口编程的核心原则,确保了代码的灵活性和可替换性。
2. 构造函数注入
默认注入方式
.NET 默认使用构造函数注入,这是最推荐的依赖注入方式:
public class OrderService
{
private readonly IRepository _repository;
private readonly ILogger _logger;
public OrderService(IRepository repository, ILogger logger)
{
_repository = repository;
_logger = logger;
}
}
自动依赖解析
如果类没有无参构造函数,容器会根据构造函数参数自动从已注册服务中提供依赖
DI 容器会递归解析所有依赖关系
形成完整的依赖链
处理未注册的依赖
如果依赖中存在未注册项,可以通过以下方式处理:
手动创建实例:
services.AddTransient<IService>(provider => new ServiceImpl("config"));
使用工厂模式:
services.AddTransient<IService>(provider =>
{
var logger = provider.GetService<ILogger>();
return new ServiceImpl(logger, new CustomConfig());
});
四、服务的获取与管理
1. 获取单个服务
GetService<T>() 方法
var service = serviceProvider.GetService<IService>();
如果服务未注册,会返回
null
不会抛出异常,需要自行判断空值
GetRequiredService<T>() 方法
var service = serviceProvider.GetRequiredService<IService>();
如果服务未注册,会抛出异常
适用于必须存在的服务
多次注册的处理
当同一接口有多个实现被注册时,默认返回最后注册的实例。
2. 获取多个服务
使用 GetServices<T>()
获取所有注册的服务集合:
var services = serviceProvider.GetServices<INotification>();
foreach (var service in services)
{
service.Send();
}
这在实现插件模式或策略模式时特别有用。
五、依赖注入的"传染性"
什么是 DI 的传染性?
DI 具有"传染性"特征:
第一个服务可能需要通过服务定位器方式获取
但其构造函数中的依赖会被容器自动补全
一旦开始使用 DI,整个对象图都会被 DI 容器管理
灵活性体现
通过接口注册 + 实例注入,可以灵活替换实现而不修改业务逻辑:
// 开发环境
services.AddTransient<IEmailService, MockEmailService>();
// 生产环境
services.AddTransient<IEmailService, SmtpEmailService>();
优点总结
服务对象具有高度替代性:轻松切换实现
降低耦合:依赖于抽象而非具体实现
修改业务逻辑时无需改动主流程:只需替换具体实现
提高可测试性:便于使用模拟对象进行单元测试
增强可维护性:清晰的依赖关系,易于理解和维护
六、示例代码:构造函数集合注入
using GetAddress;
using System;
using System.Collections.Generic;
namespace SendService
{
public class TheNew : ISendService
{
private readonly IEnumerable<IGetAddress> set;
public TheNew(IEnumerable<IGetAddress> set)
{
this.set = set;
}
public void Send()
{
string info = "";
foreach (IGetAddress item in set)
{
info = item.GetAddress();
}
Console.WriteLine(info + "发送");
}
}
}
代码说明
集合注入:构造函数参数使用
IEnumerable<IGetAddress>
自动装配:只要向容器注册了多个实现了
IGetAddress
的对象,容器就能自动注入整个集合灵活扩展:可以动态添加新的
IGetAddress
实现,无需修改TheNew
类体现了 .NET DI 的灵活与自动化特性
注册示例
services.AddTransient<IGetAddress, EmailAddress>();
services.AddTransient<IGetAddress, SmsAddress>();
services.AddTransient<IGetAddress, PushAddress>();
services.AddTransient<ISendService, TheNew>();
七、总结
核心价值
关注点分离:依赖注入让我们只关注业务逻辑,而非对象构建
降低耦合:通过依赖抽象减少模块间的直接依赖
提高可维护性:清晰的依赖关系和易于替换的实现
最佳实践
面向接口编程是核心原则:始终通过接口定义服务契约
优先使用构造函数注入:明确表达依赖关系
合理选择生命周期:Transient、Scoped、Singleton
避免服务定位器反模式:尽量使用依赖注入而非主动获取服务