依赖注入初级
依赖注入DependencyInjection俗称DI,是控制反转InversionOfControl俗称IOS思想的实现方式,它可以简化模块的过程,降低模块之间的耦合度
一、基本概念与简单例子
- 依赖注入的几个概念
- 服务(Service):对象
- 服务容器:负责管理注册服务
- 创建对象以及关联对象
- 对象生命周期:Transient(瞬态)、SCocped(范围:同一个指定的作用域获取的是同一个对象,不同作用域它会多新建另外一个对象)、Singleton(单例:无论谁调用都是调用同一个对象)
- 服务类型(ServiceType):表示通过什么类型来获得这个服务
- 实现类型(ImplementationType):实现服务的类是什么(可以跟服务类型一样,也可以不一样)
2、下面有一个ItestService接口,并且有两个类继承这个接口并对其接口函数进行实现。分别输出中文和英文
public interface ItestService
{
string Name { get; set; }
void SayHi();
}
public class TestServiceImp1:ItestService
{
public string Name { get; set; }
public void SayHi()
{
Console.WriteLine(Name + "中文:你好!" );
}
}
public class TestServiceImp2 : ItestService
{
public string Name { get ; set ; }
public void SayHi()
{
Console.WriteLine(Name + "English:Hi!");
}
}
}
3、普通方法调用接口类
4、依赖注入方法调用接口类
利用Nuget安装依赖注入包
调用
二、获得对象的生命周期
1、Transient(瞬态):每次获取都会创建一个新的对象。如下t与t2不是同一个对象
- SCocped(范围):同一个指定的作用域获取的是同一个对象如下t1与t2是同一个对象
- 直接使用服务获得提供者
- 先使用服务获得一个Scope,再从Scope中获得对象
- Singleton:所有范围获得的都是同一个。所以下面t1,t2,t3都是同一个对象
- 对于生命周期的选择:
- 如果累无状态,建议用Singleton
- 如果有状态则建议用Scope
- 上面的都是实现类和服务类是一样的
services.AddSingleton<TestServiceImp1>();
t1 = scope1.ServiceProvider.GetService<TestServiceImp1>();
也可以是不一样的,一般用虚基类或接口作为服务类,继承后的类作为实现类,如下:
二、注册带构造函数参数的类
- 以上的服务类型是ItestService,实现类型是TestServiceImp1。获取服务是通过ItestService来获取的。不能通过TestServiceImp1来获取,否则返回的对象为null
- 假如仍然是需要通过TestServiceImp1来获得服务,可以把服务类型和实现类型都设置为TestServiceImp1
三、通过基层接口获取多个服务
TestServiceImp1和TestServiceImp2都继承与接口ItestService,则通过ItestService可以获得这它所有已经注册的实现类对象
四、依赖注入综合例1
.net的DI(依赖注入)默认是构造函数注入。它是有"传染性"的,即,假如A构造函数中需要B,B构造函数中需要C。那么它内部会像递归一样,找到最低层C然后先构造C。如果一个类的对象是通过DI创建的,那么这个类的构造函数中声明的所有服务类型的参数都会被DI创建。但是如果一个对象是程序员手动创建的,那么这个对象就和DI没有关系,它的构造函数中声明的服务类型参数就不会被自动赋值
例:现需要实现
- 通过人名从数据库中找出对应的IP地址
- 向IP地址发送一段信息,并记录执行过程
- 新建一个用于写记录文件的接口
interface LogTool
{
void WriteLog(string msg);
}
class LogClass : LogTool
{
public LogClass()
{
}
public void WriteLog(string msg)
{
Console.WriteLine($"日志:{msg}");
}
}
- 新建一个用于由姓名从数据库中获得IP地址的接口(这里需要通过构造函数传入一个写记录文件的对象)
interface GetIpAdressTool
{
string ReadFileFromDB(string name);
}
class GetIpAdressClass : GetIpAdressTool
{
private readonly LogTool mLogTool;
public GetIpAdressClass(LogTool mLogTool) {
this.mLogTool = mLogTool;
}
public string ReadFileFromDB(string name)
{
mLogTool.WriteLog("开始读取IP地址");
string Ret= "192.168.1.1";
mLogTool.WriteLog("读取成功");
return Ret;
}
}
- 新建一个用于联网发送数据的接口(这里需要从构造函数中传入一个由姓名获取IP地址的对象)
interface SendTool
{
void Send(string content, string name);
}
class SendClass : SendTool
{
private readonly GetIpAdressTool mGetIpAdressTool;
public SendClass(GetIpAdressTool mGetIpAdressTool)
{
this.mGetIpAdressTool = mGetIpAdressTool;
}
public void Send(string content, string name)
{
string server = mGetIpAdressTool.ReadFileFromDB(name);
Console.WriteLine($"向服务器:{server}的文件名为{name}上传{content}");
}
}
- 新建一个总控制类(需要从构造函数中传入发送对象与文件记录对象)
class Controller
{
private readonly LogTool mLogTool;
private readonly SendTool mSendTool;
public Controller(LogTool mLogTool, SendTool mSendTool)
{
this.mLogTool = mLogTool;
this.mSendTool = mSendTool;
}
public void Test()
{
this.mLogTool.WriteLog("开始上传");
this.mSendTool.Send("你好", "hh");
this.mLogTool.WriteLog("上传完成");
}
}
- 注册服务:把所有需要用到的类添加到依赖注入的服务数组中。
ServiceCollection service = new ServiceCollection();
service.AddScoped<Controller>();
service.AddScoped<LogTool, LogClass>();
service.AddScoped<SendTool, SendClass>();
service.AddScoped<GetIpAdressTool, GetIpAdressClass>();
- 使用
using (var sp = service.BuildServiceProvider())
{
var c = sp.GetRequiredService<Controller>();
c.Test();
}
Console.ReadKey();
- 效果:
- 上述实现了一个综合的依赖注入,是通过构造函数栈的顺序来注入的。类与类之间互相多次使用(比如上面的文件记录的类LogTool),程序员只管把所有服务添加进去,在使用时框架会先自动通过构造函数构造出对象,提供使用。而不需要管创建每个类的对象。下图是在调用var c = sp.GetRequiredService<Controller>();获得服务时,框架自动按一下顺序调用构造函数。(按构造函数的栈顺序来调用,最底层的最先调用)
注:GetIpAdressTool的构造函数中有LogTool对象,Controller的构造函数中也有LogTool对象,在框架自动调用构造函数时,只会调用一次构造函数
五、改进1:修改读取配置方式
上述的读取配置是从注册表或环境变量中读取,没有传入参数。现需改成传入一个文件路径,从这个文件路径下读取。
- 新增一个从文件读取类,文件路径外部传入
class GetIpAdressClassFromFile : GetIpAdressTool
{
public string FilePath { get; set; }
public GetIpAdressClassFromFile(string mPath )
{
this. FilePath = mPath;
}
public string ReadFileFromDB(string name)
{
string Ret =$"从文件{FilePath}读取192.168.1.1";
return Ret;
}
}
- 把原来的
service.AddScoped<GetIpAdressTool, GetIpAdressClass>();
替换成
service.AddScoped(typeof(GetIpAdressTool), s => new GetIpAdressClassFromFile("c:/hh.123" ));
六、改进2:扩展方法修改AddScope
上面的AddScope都是单独通过AddScope添加到ServiceCollection中的,下面通过扩展方法,把它改进添加到ServiceCollection类中。
ServiceCollection service = new ServiceCollection();
service.AddScoped<Controller>();
service.AddScoped<LogTool, LogClass>();
service.AddScoped<SendTool, SendClass>();
1、新建类,把namespace改成与ServiceCollection类一样。添加IserviceCollection类的扩展方法
namespace Microsoft.Extensions.DependencyInjection
{
public static class ConsoleLogExtensions
{
public static void AddConsoleLog(this IServiceCollection services)
{
services.AddScoped<传染性DI.Program.LogClass>();
}
}
}
2、后面可以把
service.AddScoped<LogTool, LogClass>();
替换成
service.AddConsoleLog();