32、依赖注入DI/控制反转IoC
生活中的“控制反转”:自己买发电机发电和用电网的电。
依赖注入(Dependency Injection,DI)是控制反转(Inversion of Control,IOC)思想的实现方式。
依赖注入简化模块的组装过程,降低模块之间的耦合度
自己发电的代码
var connSettings = ConfigurationManager.ConnectionStrings["connStr1"];
string connStr = connSettings.ConnectionString;SqlConnection conn = new SqlConnection(connStr);
代码控制反转的目的
从“怎样创建XX对象” 到 “我要XX对象” 的两种实现方式:
1) 服务定位器(ServiceLocator);
2) 依赖注入(Dependency Injection,DI);
服务定位器
IDbConnection conn = ServiceLocator.GetService<IDbConnection>();
依赖注入
// (伪代码)
class Demo{
public IDbConnection Conn { get; set; }
public void InsertDB()
{
IDbCommand cmd = Conn.CreateCommand();
}
}
33、依赖注入2 NET中DI服务注册
DI几个概念
服务(service):就是你要的 对象;
注册服务;就是 创建对象
服务容器:负责管理注册的服务;
查询服务:创建对象及关联对象;
对象生命周期:Transient(瞬态); Scoped(范围); Singleton(单例);
.NET 中使用DI
1、测试代码见备注
public interface ITestService{
public string Name{get;set;}
public void SayHi();
}
public class TestServicelmpl: ITestService{
public string Name{get;set;}
public void SayHi() { Console.WriteLine($"Hi, I'm {Name}");}
}
public class TestServicelmpl2: ITestService{
public string Name{get;set;}
public void SayHi() { Console.WriteLine($"你好,我是 {Name}");}
}
// 正常使用
ITestService t = new TestServicelmpl();
t.Name="xiaoming";
t.SayHi();
2、根据类型来获取和注册服务。可以分别指定 服务类型(service type)
和 实现类型(implementation type)
。这两者可能相同,也可能不同。服务类型可以是类,也可以是接口,建议面向接口编程,更灵活。
上面代码中,服务类型为 ITestService,实现类型为 TestServicelmpl
3、.NET控制反转组件取名为DependencyInjection,但它包含ServiceLocator的功能。
安装控制反转的组件:
1、Install-Package Microsoft.Extensions.DependencyInjection
2、using Microsoft.Extensions.DependencyInjection;
3、ServiceCollection 用来构造容器对象 IServiceProvider 。调用 ServiceCollection的BuildServiceProvider() 创建的 ServiceProvider,可以用来获取 BuildServiceProvider() 之前 ServiceCollection 中的对象。
static void Mian(string[] args){
// 构造容器对象 services
ServiceCollection services = new ServiceCollection();
// 注册一个瞬态的服务,服务类型和实现类型都是 TestServicelmpl
services.AddTransient<TestServicelmpl>();
// 这个相当于服务定位器
using(ServiceProvider serviceProvider = services.BuildServiceProvider()){
// 从服务器容器中获取TestServicelmpl服务
TestServicelmpl testService = serviceProvider.GetService<TestServicelmpl>();
testService.Name = "tom";
testService.SayHi();
}
}
// 这代码总的来说就是:创建 服务容器,将 服务 注册到 服务容器 中,然后再从服务容器获取指定服务出来执行,,,
34、依赖注入3 服务生命周期
1、给类构造函数中打印,看看不同生命周期的对象创建,使用serviceProvider.CreateScope()创建Scope。
// 构造容器对象 services
ServiceCollection services = new ServiceCollection();
// 注册一个瞬态的服务,服务类型和实现类型都是 TestServicelmpl
//services.AddTransient<TestServicelmpl>();
//services.AddSingleton<TestServicelmpl>();
services.AddScoped<TestServicelmpl>();
// 这个相当于服务定位器
using (ServiceProvider serviceProvider = services.BuildServiceProvider())
{
// 从服务器容器中获取TestServicelmpl服务
TestServicelmpl testService = serviceProvider.GetService<TestServicelmpl>();
TestServicelmpl testService2 = serviceProvider.GetService<TestServicelmpl>();
Console.WriteLine("testService 和 testService2:" + object.ReferenceEquals(testService, testService2));
// Scope 作用域
using (IServiceScope scope1 = serviceProvider.CreateScope()){
// 在 scope 中获取Scope相关对象,即 scope1.ServiceProvider 而不是 serviceProvider
var scopeS1 = scope1.ServiceProvider.GetService<TestServicelmpl>();
var scopeS2 = scope1.ServiceProvider.GetService<TestServicelmpl>();
Console.WriteLine("scopeS1 和 scopeS2:" + object.ReferenceEquals(scopeS1, scopeS2));
Console.WriteLine("testService 和 scopeS2:" + object.ReferenceEquals(testService, scopeS2));
}
}
2、如果一个类实现了IDisposable接口,则离开作用域之后容器会自动调用对象的Dispose方法。
3、不要在长生命周期的对象中引用比它短的生命周期的对象。在ASP.NET Core中,这样做默认会抛异常。
4、生命周期的选择:如果类无状态,建议为Singleton;如果类有状态,且有Scope控制,建议为Scoped,因为通常这种Scope控制下的代码都是运行在同一个线程中的,没有并发修改的问题;在使用Transient的时候要谨慎。
5、.NET注册服务 的重载方法很多,看着文档琢磨吧。
35、依赖注入4 服务定位器
其他注册方法:
- 服务类型和实现类型不一致的注册
- 简单看看其他Add*方法
// 注册一个瞬态的服务,服务类型和实现类型都是 TestServicelmpl
services.AddTransient<TestServicelmpl>();
services.AddTransient<ITestService, TestServicelmpl>();
services.AddTransient(typeof(ITestService), typeof(TestServicelmpl));
services.AddTransient(typeof(ITestService), new TestServicelmpl());
IServiceProvider的服务定位器方法:
T GetService<T>()
如果获取不到对象,则返回null。
object GetService(Type serviceType)
T GetRequiredService<T>()
如果获取不到对象,则抛异常
object GetRequiredService(Type serviceType)
IEnumerable<T> GetServices<T>()
适用于可能有很多满足条件的服务
IEnumerable<object> GetServices(Type serviceType)
36、依赖注入5 NET依赖注入
DI魅力渐显:依赖注入
1、依赖注入是有“传染性”的,如果一个类的对象是通过DI创建的,那么这个类的构造函数中声明的所有服务类型的参数都会被DI赋值;但是如果一个对象是程序员手动创建的,那么这个对象就和DI没有关系,它的构造函数中声明的服务类型参数就不会被自动赋值。
2、.NET的DI默认是构造函数注入。
3、案例。
37-39、依赖注入6 DI综合案例1-3
需求说明
1、目的:演示DI的能力;
2、有配置服务、日志服务,然后再开发一个邮件发送器服务。可以通过配置服务来从文件、环境变量、数据库等地方读取配置,可以通过日志服务来将程序运行过程中的日志信息写入文件、控制台、数据库等。
3、说明:案例中开发了自己的日志、配置等接口,这只是在揭示原理,.NET有现成的,后面讲。
实现1
1、创建四个.NET Core类库项目,ConfigServices是配置服务的项目,LogServices是日志服务的项目,MailServices是邮件发送器的项目,然后再建一个.NET Core控制台项目MailServicesConsole来调用MailServices。MailServices项目引用ConfigServices项目和LogServices项目,而MailServicesConsole项目引用MailServices项目。
2、编写类库项目LogServices,创建ILogProvider接口。编写实现类ConsoleLogProvider。编写一个ConsoleLogProviderExtensions定义扩展方法AddConsoleLog,namespace和IServiceCollection 一致。
实现2
1、编写配置服务的类库项目ConfigServices。接口IConfigProvider,方法:string GetValue(string name)。
2、环境变量读取配置类EnvVarConfigProvider:Environment.GetEnvironmentVariable(name);编写一个类带扩展方法:AddEnvVarConfig
3、编写从ini文件中读取配置的类ConfigServices。
实现3
1、“可覆盖的配置读取器”。配置中心服务器。可以本地的覆盖配置服务器的,或者配置文件覆盖环境变量的。例如,按照“配置中心服务器”、“本地环境变量”、“本地配置文件”的顺序添加了三个配置提供者,在“配置中心服务器”中提供了“a=1;b=2;c=3”这三个配置项,在“本地环境变量”中配置了“a=10;b=20;”,在“本地配置文件”中配置了“b=200”,那么最终我们读取的时候读到的就是“a=10;b=200;c=3;”
2、定义一个从各个ConfigProvider中读取项的IConfigReader接口。编写实现类LayeredConfigReader 。
实现4
1、编写发送邮件的服务。IMailSender接口,实现类DefaultMailSender。注入IConfigReader ,ILogProvider ,不真的发邮件,想真的发邮件用MailKit。
2、整合、调试。
总结
关注于接口,而不是关注于实现,各个服务可以更弱耦合的协同工作。在编写代码的时候,我们甚至都不知道具体的服务是什么。
第三方DI容器:Autofac等。Autofac优点:支持属性注入、基于名字注入、基于约定的注入等。
如无必要,勿增实体