本文示例代码,均采用 .NET 6,具体的代码可以在这个仓库 Articles.DI 中获取。
在 .NET 中的依赖关系注入是一等公民,官方框架了提供配置、日志记录和选项等模式。
依赖注入(DI)通过下面的方式,解决了前面的这些问题:
- 使用接口或基类,将依赖关系实现抽象化;
- 使用统一容器注册服务;
- 将服务注入到使用它的类中,由框架负责服务的实例化,且由框架管理它的声明周期;
我们用一个示例,来展示如何在 .NET 中,使用依赖注入。下面这个示例是一个后台服务 Worker
,其依赖 MessageWriter
,每隔 1s,调用 MessageWriter.Write 输出一行简单的日志。
// https://github.com/alva-lin/Articles.DI/tree/master/WorkerService2
// 定义 host 服务
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHostedService<Worker>();
})
.Build();
await host.RunAsync(); // 启动
// 具体的后台服务类
public class Worker : BackgroundService
{
// 硬编码依赖,需要自行初始化成员变量
private readonly MessageWriter _messageWriter = new();
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
await Task.Delay(1000, stoppingToken);
}
}
}
public class MessageWriter
{
public void Write(string message)
{
Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
}
}
使用依赖注入
前面的代码中,Worker
类显式依赖 MessageWriter
类,且在自己的代码块中,自己实例化了对应实例。
随着项目的复杂度增加,Worker
类包含多个不同变量,那么都要 Worker
类自行实例化,而且若是这些变量又依赖其他服务,那么光是初始化的代码就会非常的繁杂。并且后续不使用 MessageWriter
类,想换一种实现,那么也要修改代码,两个类之间的耦合非常紧密。
为了解开二者之间的耦合,我们根据下面的几个步骤,使用依赖注入,做到控制反转:
- 将
MessageWriter
类的行为抽象出一个接口IMessageWriter
; - 在容器中注册服务,将
IMessageWriter
的实现注册为MessageWriter
; - 将
_messageWriter
的类型修改为接口IMessageWriter
,Worker
类只依赖接口; -
Worker
类在构造函数中,声明需要的类型变量,由容器进行注入;
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHostedService<Worker>();
// 2. 注册服务
services.AddSingleton<IMessageWriter, MessageWriter>();
})
.Build();
await host.RunAsync();
// 1. 抽象接口
public interface IMessageWriter
{
void Write(string message);
}
public class MessageWriter : IMessageWriter
{
public void Write(string message)
{
Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
}
}
public class Worker : BackgroundService
{
// 3. 松耦合,只依赖接口,不依赖具体实现。Worker 类不负责初始化成员
private readonly IMessageWriter _messageWriter;
// 4. 从构造函数传递注入具体的实例,将其赋值给 _messageWriter 成员
public Worker(IMessageWriter messageWriter)
{
_messageWriter = messageWriter;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
await Task.Delay(1000, stoppingToken);
}
}
}
上述代码中,Worker
类和 MessageWriter
类之间并没有直接联系,前者拥有一个 IMessageWriter
类型的成员,容器服务会通过构造函数注入一个实例进来,Worker
类可以直接使用,而无需知道如何构造一个该类型的实例。后者 MessageWriter
是一个实现 IMessageWriter
接口的类,它只需要根据接口要求,实现对应方法,不用去管其他人员会如何使用它,而在容器注册服务时,将 MessageWriter
注册为 IMessageWriter
的实现,那么在有服务依赖于 IMessageWriter
时,容器会自动生成一个 MessageWriter
实例注入进去。
依赖注入将两个类解耦,后续如果想要更换 IMessageWriter
的具体实现,可以直接添加一个新的实现类,然后修改注册即可,可以参考下面的代码:
public class LoggingMessageWriter : IMessageWriter
{
private readonly ILogger<LoggingMessageWriter> _logger;
public LoggingMessageWriter(ILogger<LoggingMessageWriter> logger)
{
_logger = logger;
}
public void Write(string message)
{
_logger.LogInformation(message);
}
}
// 注册服务: ConfigureServices 代码块
// services.AddSingleton<IMessageWriter, MessageWriter>();
// 在注册服务时更改 IMessageWriter 的实现,无需修改每一处业务代码
services.AddSingleton<IMessageWriter, LoggingMessageWriter>();
Worker
类只依赖于 IMessageWriter
接口,不去关注如何实现,如何生成,这些事情由框架去做。我们要做的只是重新写一个实现类,然后修改注册服务的代码即可。