P3-1 依赖注入
依赖注入
什么是控制反转、服务定位器和依赖注入
控制反转
- 生活中的“控制反转”:自己发电和用电网的电。
- 依赖注入(Dependency Injection,DI)是控制反转(Inversion of Control,IOC)思想的实现方式。
- 依赖注入简化模块的组装过程,降低模块之间的耦合度
1 |
|
代码控制反转的目的
“怎样创建XX对象”→“我要XX对象”
两种实现方式:
- 服务定位器(ServiceLocator)
- 需要先找
服务定位器
去要,然后它给你,你在拿去使用(半自动化)
- 需要先找
- 推荐:依赖注入(Dependency Injection,DI) ⭐️⭐️⭐️
- 自己声明一下,编译器会自动的给你声明的属性赋值(自动化)
1 |
|
.NET Core依赖注入的基本使用
DI的几个概念
- 服务(srevice):对象
- 当你向框架要一个对象的时候,它就会给你提供一个服务
- 注册服务
- 把服务注册进去,你才能拿到这个服务
- 服务容器:负责管理注册的服务
- 查询服务:创建对象及关联对象
- 对象的生命周期
获取服务的时候是创建一个新对象还是用之前的对象
- 瞬态(Transient)
- 每次被请求的时候都会创建一个新对象
- 范围(Scoped)
- 在给定的范围内,可以多次请求同一个服务对象,服务每次返回的对象都是同一个
- 单例(Singleton)
- 全局共享同一个服务对象
- 瞬态(Transient)
.NET 中使用DI
1、测试代码见备注
2、根据类型来获取和注册服务。
可以分别指定服务类型(service type)和实现类型(implementation type)。这两者可能相同,也可能不同。服务类型可以是类,也可以是接口,建议面向接口编程
,更灵活。
3、.NET控制反转组件取名为DependencyInjection,但它包含ServiceLocator的功能。
.NET 中使用DI 2
- 1.通过NuGet安装对应开发包
- Install-Package Microsoft.Extensions.DependencyInjection
- 2.using Microsoft.Extensions.DependencyInjection
- 3.ServiceCollection用来构造容器对象IServiceProvider 。调用ServiceCollection的BuildServiceProvider()创建的ServiceProvider,可以用来获取BuildServiceProvider()之前ServiceCollection中的对象。
1 |
|
生命周期
1、给类构造函数中打印,看看不同生命周期的对象创建,使用serviceProvider.CreateScope()创建Scope
2、如果一个类实现了IDisposable接口,则离开作用域之后容器会自动调用对象的Dispose方法
3、不要在长生命周期的对象中引用比它短的生命周期的对象。在ASP.NET Core中,这样做的话默认会抛异常
生命周期的选择
- 如果类无状态,建议为Singleton
- 无状态
- 类中没有成员变量和属性
- 无状态
- 如果类有状态,且有Scope控制,建议为Scoped,因为通常这种Scope控制下的代码都是运行在同一个线程中的,没有并发修改的问题
- 在使用Transient的时候要谨慎(Transient使用一般很少,因为很容易用了出现内存泄漏)
- 如果类无状态,建议为Singleton
5、.NET注册服务的重载方法很多,看着文档琢磨吧
比较
- 瞬态
- 范围
- 单例
- 瞬态
其他注册方法
- 服务类型和实现类型不一致的注册
- 服务类型:接口
- 实现类型:实现类
- 其他的Add方法
1 |
|
服务定位器
IServiceProvider的服务定位器方法:
- T GetService
() 如果获取不到对象,则返回null。 - object GetService(Type serviceType) —非泛型方法
- T GetRequiredService
()如果获取不到对象,则抛异常 - object GetRequiredService(Type serviceType) —非泛型方法
- IEnumerable
GetServices ()适用于可能有很多满足条件的服务 - IEnumerable
1 |
|
依赖注入的魅力所在
- 1、依赖注入是有“传染性”的,如果一个类的对象是通过DI创建的,那么这个类的构造方法中声明的所有服务类型的参数都会被DI赋值;
- 但是如果一个对象是由开发人员手动创建的,那么这个对象就和DI没有关系,它的构造方法中声明的服务类型参数就不会被自动赋值。
- 一旦使用了DI,就要尽量避免直接通过new关键字来创建对象
- 好处:业务逻辑层的开发人员不用管数据访问类的代码,只需要注册服务去使用即可。不需要知道数据访问类的逻辑与代码。数据访问类就是框架,自动根据你声明的类型去赋值
- 2、.NET的DI默认是构造函数注入。
- 3、案例:编写一个类,连接数据库做插入操作,记录日志(模拟的输出),把Dao、日志都放入单独的服务类
1 |
|
案例
需求说明
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 一致。
代码写在了“ConfigServices”这个项目里
实现2
1、编写配置服务的类库项目ConfigServices。接口IConfigProvider,方法:string GetValue(string name)。
2、环境变量读取配置类EnvVarConfigProvider:Environment.GetEnvironmentVariable(name);编写一个类带扩展方法:AddEnvVarConfig
3、编写从ini文件中读取配置的类ConfigServices。
注意点
- ①读取本地的配置文件,需要修改为“如果较新则复制”
- ②简化注册服务的方法
- 扩展方法
- 1.名称空间名称修改为“Microsoft.Extensions.DependencyInjection”, 这样可以免去using引用
- 2.方法参数中需要加 this,要不然“.”调用时出不来
- 3.扩展方法后,其实有的实现类就可以不写public了
- 扩展方法
- ①读取本地的配置文件,需要修改为“如果较新则复制”
实现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优点:支持属性注入、基于名字注入、基于约定的注入等。
奥卡姆剃刀原理:如无必要,勿增实体