理解和入门依赖注入
概览:
-
什么是依赖注入?
-
伪代码演示
• 依赖注入提倡使用接口而不是类 -
C# 代码演示
• 版本一:最直接粗暴的版本
• 版本二:随Host自启动的服务 -
依赖的实例化策略 —— 每次都实例化/单例/Scoped实例
• Scoped实例化策略的C#代码演示
1. 什么是依赖注入?
来自百科的定义
在软件工程中,依赖注入(dependency injection,缩写为 DI)是一种软件设计模式,也是实现控制反转的其中一种技术。这种模式能让一个物件接收它所依赖的其他物件。“依赖”是指接收方所需的对象。“注入”是指将“依赖”传递给接收方的过程。在“注入”之后,接收方才会调用该“依赖”。此模式确保了任何想要使用给定服务的对象不需要知道如何建立这些服务。取而代之的是,连接收方对象也不知道它存在的外部代码提供接收方所需的服务。
解析一下:
- 依赖注入是一种软件设计模式
- 方便实现控制反转(稍后解释)
- 通常在面向对象语言中使用 —— 如果对象A代码里调用了对象B,那么 B就是A的“依赖”。
- “注入”指的是把对象B传递给对象A的过程
- “注入”过程需要一套“外部代码”帮助实现 —— 且称之为 依赖注入系统
2. 伪代码示例
不使用依赖注入
不使用依赖注入,必须在代码的某个地方主动地创建或者获取对象B。
public class ClassA { private readonly ClassB _classB; public ClassA() { _classB = new ClassB(); //主动创建对象B } public void Process() { _classB.DoSomething(); /// ... } } public class ClassB { public void DoSomething() { /// ... } }
使用依赖注入(以C#为例)
public class ClassA { private readonly ClassB _classB; public ClassA(ClassB classB) // 声明构造器里需要对象B引用,依赖注入框架就会自动注入对象B { _classB = classB; } public void Process() { _classB.DoSomething(); // ... } } public class ClassB { public void DoSomething() { // ... } } // 伪代码: 向依赖注入系统中注册 ClassA 和 ClassB DependencyInjectionSystem.AddType(ClassA); DependencyInjectionSystem.AddType(ClassB);</code></pre>
• 使用依赖注入,不需要在代码里主动地创建或者获取对象B,相反,只需要在构造器参数里声明需要对象B的引用。(在其他一些语言的具体实现里,可能是加注解、或者特殊的修饰符来声明)。
• 伪代码中虚构了一个 DependencyInjectionSystem 指代依赖注入系统
• 需要向依赖注入系统中注册对应的类
有读者可能会问:“那是不是在业务代码中需要写 new ClassA(new ClassB()) 来实例化ClassA呢?"
不需要。使用了依赖注入后,业务代码中基本就看不到 new 的踪影了,你会感觉不管是ClassA对象还是ClassB对象都仿佛是凭空生成的 —— 其实是依赖注入系统在程序运行过程中自动生成的。
提倡使用接口而不是类
在使用依赖注入时,更多时候,对于“依赖”,接口是更推荐的。改进上面的例子。
public class ClassA { private readonly IInterfaceB _b; public ClassA(IInterfaceB b) { _b = b; } public void Process() { _b.DoSomething(); //... } } public class ClassB : IInterfaceB { public void DoSomething() { Console.WriteLine("class B is doing something ..."); } } public interface IInterfaceB { void DoSomething(); } // 伪代码: 向依赖注入系统中注册 ClassA 和 ClassB DependencyInjectionSystem.AddType(ClassA); DependencyInjectionSystem.AddType(IInterfaceB, ClassB);
• 新增了一个接口 IInterfaceB
• ClassB 实现了这个接口
• ClassA 里声明引用接口 IInterfaceB 而不是直接引用类 ClassB
• 注册ClassB时需要指明对应的接口 IInterfaceB
优点:解耦了接口和实现,ClassA不需要知道最终它获得的是怎么样的实现,它只需要知道 IInterfaceB 有个方法叫 DoSomething() 并且调用它就行了。当前它可能用的是 ClassB 实例,为了可能用的是改进后的 ClassB2 实例,也可能是全新的 ClassX 实例,但ClassA不需要更改任何代码。
原本是应该 ClassA 来控制使用怎样的 ClassB 实例(IInterfaceB 实现),使用了依赖注入设计后,这种控制关系(决定关系)就反过来由外部控制了,这就是所谓的“控制反转”。
同时鼓励接口的设计可以引导我们写出更符合里氏代换原则的代码 (里氏代换原则_百度百科 )
里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
3. C# 代码演示
理论到此为止,接下来看看如何写出能实际运行的代码(以C#为例)
// .csproj <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" /> </ItemGroup> </Project>
ClassA.cs
namespace ConsoleDIApp.demo { public class ClassA { private readonly IInterfaceB _b; public ClassA(IInterfaceB b) { _b = b; } public void Process() { Console.WriteLine("Class A start process"); _b.DoSomething(); Console.WriteLine("Class A finish process"); } } }
ClassB.cs
namespace ConsoleDIApp.demo { public class ClassB : IInterfaceB { public void DoSomething() { Console.WriteLine("class B is doing something ..."); } } }
IInterfaceB.cs
namespace ConsoleDIApp.demo { public interface IInterfaceB { void DoSomething(); } }
Program.cs
using ConsoleDIApp.demo; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace ConsoleDIApp { internal class Program { public static void Main(string[] args) { IHost host = Host.CreateDefaultBuilder(args) .ConfigureServices((context, services) => { services.AddSingleton<ClassA>(); services.AddSingleton<IInterfaceB, ClassB>(); }) .Build(); ClassA a = host.Services.GetRequiredService<ClassA>(); a.Process(); } } }
实际跟依赖注入系统有关的代码只有大概7行。运行以上代码,得到:
Class A start process class B is doing something ... Class A finish process
解析:
• Host 以及它内部的 Services 可以简单理解为 C# 提供的 依赖注入系统
• 通过 GetRequiredService 获得想要的对应的实例(这里是 ClassA 实例)
• 调用了ClassA实例 a 的方法 Process() 后的输出结果可以证实 ClassB 的实例的确被自动生成了
问题:这份程序的逻辑上入口其实就是执行 ClassA 实例的 Process() 方法,在上面的代码里显得十分生硬。
有没有更优雅的写法呢? —— 有的!
可运行版本二
新增类HostedService.cs
using ConsoleDIApp.demo; using Microsoft.Extensions.Hosting; namespace ConsoleDIApp { public class HostedService : IHostedService { private readonly ClassA _a; public HostedService(ClassA a) { _a = a; } public Task StartAsync(CancellationToken cancellationToken) { _a.Process(); return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } } }
修改 Program.cs
namespace ConsoleDIApp { internal class Program { public static async Task Main(string[] args) { IHost host = Host.CreateDefaultBuilder(args) .ConfigureServices((context, services) => { services.AddSingleton<ClassA>(); services.AddSingleton<IInterfaceB, ClassB>(); services.AddHostedService<HostedService>(); }) .Build(); await host.RunAsync(); } } }
运行的结果是一样的。
解析:
• IHostedService 是一个特殊的接口,实现这个接口的类在通过在 Host services 里注册后,可以在 Host 运行时自动执行
• services.AddHostedService
• await host.RunAsync(); 运行 Host 实例
上面的全部代码
using ConsoleDIApp.demo; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace ConsoleDIApp { internal class Program { public static async Task Main(string[] args) { IHost host = Host.CreateDefaultBuilder(args) .ConfigureServices((context, services) => { services.AddSingleton<ClassA>(); services.AddSingleton<IInterfaceB, ClassB>(); services.AddHostedService<HostedService>(); }) .Build(); await host.RunAsync(); } } } namespace ConsoleDIApp.demo { public class ClassA { private readonly IInterfaceB _b; public ClassA(IInterfaceB b) { _b = b; } public void Process() { Console.WriteLine("Class A start process"); _b.DoSomething(); Console.WriteLine("Class A finish process"); } } } namespace ConsoleDIApp.demo { public class ClassB : IInterfaceB { public void DoSomething() { Console.WriteLine("class B is doing something ..."); } } } namespace ConsoleDIApp.demo { public interface IInterfaceB { void DoSomething(); } } namespace ConsoleDIApp { public class HostedService : IHostedService { private readonly ClassA _a; public HostedService(ClassA a) { _a = a; } public Task StartAsync(CancellationToken cancellationToken) { _a.Process(); return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } } }
4. 依赖的实例化策略
上面演示的 ClassB 太简单了,没有内部状态。因此不管是每次注入时都新建一个实例还是每次注入同一个实例,对程序都不会有影响。
但对于一些复杂的类, 比如 Config (存放了程序配置的类), UserPreference (存放了当前用户的偏好设置)就得考虑合适的实例化策略。
显然对于多数程序, Config (存放了程序配置的类)只需要实例化一次就行,重复实例化只会浪费资源;而UserPreference (存放了当前用户的偏好设置),既不是每次都实例化也不是只实例化一次,而是应该针对每个用户实例化一次,同一个用户的所有活动都应该对应同一个 UserPreference 实例。
依赖的实例化策略可以分成三种类型:
每次注入前都生成一个全新的实例
对于一个类只生成一个实例,每次注入同一个实例
针对同一scope(范畴)只生成一个实例 —— scope 需要自定义,比如一次 http request 或者同一用户的所有访问 等等。
对此,C# 有对应的API
分别是
services.AddTransient<IInterfaceB, ClassB>(); // 每次都生成一个实例 services.AddSingleton<IInterfaceB, ClassB>(); // 单例 services.AddScoped<IInterfaceB, ClassB>(); // 每个 scope 一个实例
(services 类型是 Microsoft.Extensions.DependencyInjection.IServiceCollection)
如何获取 Scoped 实例
• IServiceProvider.CreateScope() 方法能创建IServiceScope实例。
• serviceScope.ServiceProvider 域也是一个IServiceProvider实例。
• 获取 Scoped 实例只能通过“可运行版本一”中主动调用IServiceProvider.GetRequiredService()的方式;
• 获取实例通常是一个链路过程,链路上的依赖如果是Scoped 实例就返回该Scoped唯一的实例,如果不是( AddTransient 或 AddSingleton)就按对应的策略实例化对应的依赖。
结合这三点,以下为核心代码
IServiceProvider serviceProvider = host.Services; IServiceScope serviceScope = serviceProvider.CreateScope(); IServiceProvider scopedServiceProvider = serviceScope.ServiceProvider; ClassA a = scopedServiceProvider.GetRequiredService<ClassA>();
觉得迷糊不要紧,尝试运行以下示例代码,摸索一下其中的原理
ClassA.cs
namespace ConsoleDIApp.demo { public class ClassA { private readonly IInterfaceB _b; private readonly long _id; public ClassA(IInterfaceB b) { _b = b; _id = DateTime.UtcNow.Ticks; // 利用 id 来区分是否是同一个实例 } public void Process() { Console.WriteLine($"[{_id}]: Class A start process"); _b.DoSomething(); Console.WriteLine("Class A finish process"); } } }
ClassB.cs
namespace ConsoleDIApp.demo { public class ClassB : IInterfaceB { private readonly long _id; public ClassB() { _id = DateTime.UtcNow.Ticks; // 利用 id 来区分是否是同一个实例 } public void DoSomething() { Console.WriteLine($"[{_id}]: class B is doing something ..."); } } }
IInterfaceB.cs
namespace ConsoleDIApp.demo { public interface IInterfaceB { void DoSomething(); } }
HostedService.cs
using ConsoleDIApp.demo; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace ConsoleDIApp { public class HostedService : IHostedService { private readonly IServiceProvider _services; // 关联用户名和IServiceScope实例的集合 private Dictionary<string, IServiceScope> _scopes = new(); public HostedService(IServiceProvider a) { _services = a; } public Task StartAsync(CancellationToken cancellationToken) { while (true) { Console.WriteLine("Input User Name: "); var user = Console.ReadLine(); if (user == null || user == "q") { break; // 退出循环 } if (!_scopes.ContainsKey(user)) { // 对新用户,创建一个新的 IServiceScope 实例 IServiceScope newServiceScope = _services.CreateScope(); _scopes.Add(user, newServiceScope); } IServiceScope serviceScope = _scopes[user]; IServiceProvider serviceProvider = serviceScope.ServiceProvider; ClassA a = serviceProvider.GetRequiredService<ClassA>(); a.Process(); } return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } } }
Program.cs
public static async Task Main(string[] args) { IHost host = Host.CreateDefaultBuilder(args) .ConfigureServices((context, services) => { // 每此实例一个新的ClassA services.AddTransient<ClassA>(); // 相同scope共享同一个ClassB实例 services.AddScoped<IInterfaceB, ClassB>(); services.AddHostedService<HostedService>(); }) .Build(); await host.RunAsync(); }
运行效果
Input User Name: tom [638831071897299805]: Class A start process [638831071897292850]: class B is doing something ... Class A finish process Input User Name: jerry [638831071933082335]: Class A start process [638831071933082295]: class B is doing something ... Class A finish process Input User Name:
观察点:
• 每次 ClassA 的 id 都不同 (因为 AddTransient )
• tom 的 ClassB 的 id 都一样 ,jerry 的 ClassB 的 id 都一样, 说明 AddScoped 如预期一样表现
后面的全部代码
using ConsoleDIApp; using ConsoleDIApp.demo; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace ConsoleDIApp.demo { internal class Program { public static async Task Main(string[] args) { IHost host = Host.CreateDefaultBuilder(args) .ConfigureServices((context, services) => { // 每此实例一个新的ClassA services.AddTransient<ClassA>(); // 相同scope共享同一个ClassB实例 services.AddScoped<IInterfaceB, ClassB>(); services.AddHostedService<HostedService>(); }) .Build(); await host.RunAsync(); } } } namespace ConsoleDIApp.demo { public class ClassA { private readonly IInterfaceB _b; private readonly long _id; public ClassA(IInterfaceB b) { _b = b; _id = DateTime.UtcNow.Ticks; // 利用 id 来区分是否是同一个实例 } public void Process() { Console.WriteLine($"[{_id}]: Class A start process"); _b.DoSomething(); Console.WriteLine("Class A finish process"); } } } namespace ConsoleDIApp.demo { public class ClassB : IInterfaceB { private readonly long _id; public ClassB() { _id = DateTime.UtcNow.Ticks; // 利用 id 来区分是否是同一个实例 } public void DoSomething() { Console.WriteLine($"[{_id}]: class B is doing something ..."); } } } namespace ConsoleDIApp.demo { public interface IInterfaceB { void DoSomething(); } } namespace ConsoleDIApp { public class HostedService : IHostedService { private readonly IServiceProvider _services; // 关联用户名和IServiceScope实例的集合 private Dictionary<string, IServiceScope> _scopes = new(); public HostedService(IServiceProvider a) { _services = a; } public Task StartAsync(CancellationToken cancellationToken) { while (true) { Console.WriteLine("Input User Name: "); var user = Console.ReadLine(); if (user == null || user == "q") { break; // 退出循环 } if (!_scopes.ContainsKey(user)) { // 对新用户,创建一个新的 IServiceScope 实例 IServiceScope newServiceScope = _services.CreateScope(); _scopes.Add(user, newServiceScope); } IServiceScope serviceScope = _scopes[user]; IServiceProvider serviceProvider = serviceScope.ServiceProvider; ClassA a = serviceProvider.GetRequiredService<ClassA>(); a.Process(); } return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } } }
本文对 C# 的API都只是简单介绍,.Net 开发者可以自行阅读官方文档,进一步理解各个相关的类,方法等。
总结
只要软件中使用了大量的面向对象写法,依赖注入往往是避不开的一个技术。
具体的API可能有所不同,但是背后的思想和设计理念大同小异。
希望本文能对读者有启发!