1 DI概念
1.1 基本定义
依赖注入(Dependency Injection,DI)是控制反转(Inversion of Control,IOC)思想的实现方式
简化模块的组装过程,降低模块之间的耦合度。
1.2 理解及实现方式
代码控制反转的目的:“怎样创建xxx对象”—>”我要xxx对象”
两种实现方式:1)服务定位器(ServiceLocator) 2)依赖注入(Dependency Injection,DI),省略GetService步骤
1.3 几个关键概念
服务(service):要获得的对象
注册服务:将对象注册进去
服务容器:负责管理注册的服务
查询服务:创建对象及关联对象
对象生命周期(销毁的时间):Transient(瞬态)每次获取都获得一个新的对象 Scoped(范围) Singleton(单例)同一个对象
1.4 .NET中的DI
Interface ITest
TestInterfaceImplementA:ITest
TestInterfaceImplementB:ITest
- 根据类型来获取和注册服务:可以分别指定服务类型(service type)和实现类型(implementattion type)。服务类型尽可能的选择是接口,面向接口编程。
- .NET控制反转组件取名为DependencyInjection,但它包含ServiceLocator功能。
使用DI 2:
1.Install-Package Microsoft.Extensions.DependencyInjection
2.using Microsoft.Extensions.DependencyInjection
3.ServiceCollection 用来构造容器对象IServiceProvider 调用ServiceCollection的BuildServiceProvider()创建的ServiceProvider,可以用来获取BuildServiceProvider()之前ServiceCollection中的对象。
2 服务的生命周期
AddSingleton拿到的是同一个对象
Scope创建范围,创建一个CreateScope() 就是一个范围,通常using的范围就是Scope范围
当一个类没有成员变量,没有属性的类,建议声明为Singleton。如果类有状态且有Scope范围,建议为Scoped。因为通常这种Scope控制下的代码都是运行在同一线程中的,没有并发修改的问题;在使用Transient瞬态的时候要谨慎。
using System;
using Microsoft.Extensions.DependencyInjection;
namespace DIAndIOC
{
class Program
{
static void Main(string[] args)
{
//创建一个容器
var serviceCollection = new ServiceCollection();
//将对象注入到瞬态中
serviceCollection.AddTransient<TestServiceImpA>();
//将对象创建成实例,服务定位器
using (var sp = serviceCollection.BuildServiceProvider())
{
//我要容器里这个对象TestServiceImpA
var testServiceImpA = sp.GetService<TestServiceImpA>();
testServiceImpA.Name = "Albert";
testServiceImpA.ShowName();
}
//创建一个容器
var serviceCollection2 = new ServiceCollection();
//将对象注入到范围中,在范围内拿到的是同一个对象
serviceCollection2.AddScoped<TestServiceImpA>();
using (var sp = serviceCollection2.BuildServiceProvider())
{
using(var scope = sp.CreateScope())
{
var testServiceImpA = scope.ServiceProvider.GetService<TestServiceImpA>();
testServiceImpA.Name = "Albert";
testServiceImpA.ShowName();
}
}
Console.ReadLine();
}
}
interface ITestService
{
public string Name { get; set; }
public void ShowName();
}
public class TestServiceImpA : ITestService
{
public string Name { get; set; }
public void ShowName()
{
Console.WriteLine(this.Name);
}
}
}
3 其他注册方法
- 服务类型和实现类型不一致的注册(尽量注册一个接口,服务类型为接口)
- 简单看看其他Add*方法:
- T GetService
() 如果找不到会返回Null object GetService(typeof(T)) - GetRequiredService
() 如果找不到直接异常 - IEnumerable
GetServices ()适用于可能有多个满足条件的服务
*T是根据注册服务类型来的,上面是ITestService,这边必须是ITestService,找不到服务返回null**
static void Main(string[] args)
{
//new collection--addscope声明服务和对象--BuilderServicerProvider()创建服务提供者
//--GetService<T>获取服务
ServiceCollection serviceCollection = new ServiceCollection();
//服务类型和实现类型
serviceCollection.AddScoped<ITestService,TestServiceImpA>();
using (var sp = serviceCollection.BuildServiceProvider())
{
//此处不关心你实现类是什么
//GetService<T>() T是根据注册服务类型来的,上面是ITestService,这边必须是ITestService,找不到服务返回null
var ts = sp.GetService<ITestService>();
Console.WriteLine(ts.GetType());
Console.ReadLine();
}
}
4 依赖注入DI显威力
- 依赖注入具有“传染性”,如果一个类的对象是通过DI创建,那么这个类的构造函数中声明的所有服务类型的参数都会被DI赋值;但是如果一个对象是成员手动创建的,那么这个对象就和DI没有关系,它的构造函数中声明的服务类型参数就不会被自动赋值。(服务相互依赖,多层依赖关系,自动创建其他依赖服务,根服务是自己创建的)
- .NET的DI默认是构造函数注入(Java中的Spring是属性注入)
- 例子:编写一个类,连接数据库做插入操作,并且记录日志(模拟的输出),把Dao、日志都放入单独的服务类,connstr见备注。
- 总结:依赖注入用于多个接口(服务)相互依赖的情景下,接口A依赖接口B,要在A中通过构造函数给接口B的属性(接口A)进行赋值,在使用BuilderServiceProvider之前要将所有的服务注册进去,服务和实现类一起注入其中。Controller必须是容器创建出来的,然后DI将服务传进去。 ```csharp using Microsoft.Extensions.DependencyInjection; using System;
namespace _210720_Demon02_InfectDI {
class Program {
static void Main(string[] args) {
ServiceCollection services = new ServiceCollection();
services.AddScoped
using (var sp = services.BuildServiceProvider()) {
var controller = sp.GetRequiredService<Controller>();
controller.Test();
}
Console.ReadLine();
}
}
class Controller {
private readonly ILog log;
private readonly ICloud cloud;
public Controller(ILog log, ICloud cloud) {
this.log = log;
this.cloud = cloud;
}
public void Test() {
this.log.Log("开始上传");
this.cloud.Save(".NET第一行代码", "albert.txt");
this.log.Log("上传完毕");
}
}
interface ILog {
public void Log(string msg);
}
interface IConfig {
public string GetValue(string name);
}
interface ICloud {
public void Save(string content, string name);
}
class LogImp : ILog {
public void Log(string msg) {
Console.WriteLine($"日志:{msg}");
}
}
class ConfigImp : IConfig {
public string GetValue(string server) {
return $"返回服务名为{server}";
}
}
class CloudImp : ICloud {
private readonly IConfig config;
public CloudImp(IConfig config) {
this.config = config;
}
public void Save(string content, string name) {
string server = config.GetValue("albert_server");
Console.WriteLine($"向服务{server}中上传的内容为{content},内容保存在{name}文件下");
}
}
}
5 DI综合案例1
需求说明:
1.目的:演示DI的能力
2.有配置服务、日志服务,然后再开发一个邮件发送器服务(MailK)。可以通过配置服务来从文件、环境变量、数据库等地方读取配置,可以通过日志服务来将程序运行过程中的日志信息写入文件、控制台、数据库等。
3.说明:案例中开发了自己的日志、配置等接口,这只是在揭示原理,.NET有现成的。
第一个根上对象只能用服务定位器的方式,如果需要属性赋值在构造函数中则需写成:service.AddScoped(typeof(IT),s=>new TImp(){xxx=xxx})
代码:https://gitee.com/hongyongzhao/dot-net
项目克隆:https://gitee.com/hongyongzhao/dot-net.git
6 DI综合案例2
实现一个“可覆盖的配置读取器”(集群:多台服务器—从单本地配置文件web.config从中心化配置服务器中一键修改所有的配置文件)