本文示例代码,均采用 .NET 6,具体的代码可以在这个仓库 Articles.DI 中获取。
让我们看这么一段简单的代码:
// https://github.com/alva-lin/Articles.DI/tree/master/WorkerService1
public class MessageWriter {
public void Write(string message) => Console.WriteLine(message);
}
public class Worker : BackgroundService {
private readonly MessageWriter _msgWriter = new MessageWriter();
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
while (!stoppingToken.IsCancellationRequested) {
_msgWriter.Write($"Worker running at: {DateTime.Now}");
await Task.Delay(1000, stoppingToken);
}
}
}
在上面的代码中,Worker
类包含有一个MessageWriter
类的实例成员,这个类创建并直接依赖于MessageWriter
类。这样的代码有一些问题:
- 如果
_msgWriter
变量要替换成别种类型的实现,需要修改Worker
类; - 如果
MessageWriter
类具有其他依赖项,则必须在Worker
类中进行配置; - 难以对
Worker
类进行单元测试。
如果有一个统一的管理者,负责MessageWriter
类型的实例化,然后将其传递给有需求的类,就能做到这两个类之间的依赖解除,减少了硬编码。而这,就是依赖注入。
依赖注入和控制反转#
依赖注入(英文 Dependency Injection,简称 DI),是一种在类及其依赖项之间实现控制反转(英文 Inversion of Control,简称 IoC)的技术。
而控制反转,是面向对象编程中的一种设计原则,可以用来减少程序代码间的耦合程度。除了依赖注入外,还有一种实现控制反转的方法,叫做 “依赖查找”。
大多数程序都由多个类相互引用,彼此依赖合作以实现业务逻辑,这使得每个对象都需要获取其他服务的引用。如果这个获取过程需要靠对象自己实现,那么这将导致类之间高度耦合,难以维护和调试。
传统应用中,如果 A 服务要获取 B 服务,则需要在自己的代码块中实例化。而如果使用依赖注入,A 服务只需声明所需的服务,由外部容器来负责生成实例,A 服务只需接收即可。整个过程从主动变成了被动,反转了对象的获取过程,即实现了控制反转。
下面两张图展示了两种依赖项关系图:
直接依赖项关系:类 A 调用类 B 的方法,类 B 调用类 C 的方法,则在编译时,类 A 依赖与类 B,类 B 依赖与类 C。
应用依赖反转后,类 A 引用的是接口 IB 的方法,不再直接依赖于类 B。在编译时,则是类 A 和类 B 引用了接口 IB,和前面的直接依赖相比,反转了依赖项。而类 B 和类 C 之间也是相似的。
这张图,生动形象的解释了控制反转是如何解耦服务之间的依赖。
参考链接#
依赖关系反转 - 现代 ASP.NET Web 应用程序电子书
Inversion of Control Containers and the Dependency Injection pattern