控制反转、服务定位器、依赖注入
控制反转:使用对象或者服务的时候,不需要自己去创建/new服务,而是在使用的时候直接声明,容器会自动分配一个服务实例。相当于自己用发电机发电使用和利用电网公司的电的区别,自己发电,我需要一台发电机,安装发电机,自己设置电压,频率等等,而使用电网公司的只需要花钱,就能使用;(自己创建和问别人要的区别)
- 依赖注入(Dependency Injection,DI)是控制反转(Inversion Of Control,IOC)思想的一种实现方式
- 依赖注入简化模块的组装过程,降低模块之间的耦合度
- 实现控制反转的两种方式:1.服务定位器,2.依赖注入
//1.自己发电的方式
var connSettings = ConfigurationManager.ConnectionStrings["connStr"];
string connStr = connSettings.ConnectionString;
SqlConnection conn = new SqlConnection(connStr);
//2.服务定位器实现控制反转(伪代码)
IDbConnection conn = ServiceLocator.GetService<IDbConnection>();
//3.依赖注入实现控制反转(伪代码)
class Demo
{
//只需要声明,表示我需要一个数据库连接的实例对象
public IDbConnection Conn {get;set;}
public void InsertDB()
{
IDbCommand command = Conn.CreateCommand();
}
}
-
Dependency Injection的几个概念
- 服务(service):对象,用来使用的
- 注册服务:将服务注册到相应的容器中
- 服务容器:负责管理注册的服务
- 查询服务:创建对象及关联对象
- 对象生命周期:Transient(瞬态)、Scoped(范围)、Singletion(单例)
namespace DependencyInjectionDemo
{
class Program
{
static void Main(string[] args)
{
//新建服务容器
ServiceCollection services = new ServiceCollection();
//将服务注册到容器中
services.AddTransient<TestServiceImpl1>();
//再从服务容器中获取指定的服务来使用
using (ServiceProvider provider= services.BuildServiceProvider())
{
TestServiceImpl1 serviceImpl1 = provider.GetService<TestServiceImpl1>();
serviceImpl1.Name = "Tim Duncan";
serviceImpl1.sayHi();
}
}
}
interface ITestService {
public string Name { get; set; }
public void sayHi();
}
class TestServiceImpl1:ITestService
{
public string Name { get; set; }
public void sayHi()
{
Console.WriteLine("hello,my name is "+ Name);
}
}
class TestServiceImpl2 : ITestService
{
public string Name { get; set; }
public void sayHi()
{
Console.WriteLine("你好,我的名字是 " + Name);
}
}
}
-
服务的生命周期
-
1.Service.AddTransient<T>,创建一个瞬时的对象,每次创建都是一个新的对象服务;
static void Main(string[] args)
{
//新建服务容器
ServiceCollection services = new ServiceCollection();
//将服务注册到容器中(瞬态注册)
services.AddTransient<TestServiceImpl1>();
//再从服务容器中获取指定的服务来使用
using (ServiceProvider provider= services.BuildServiceProvider())
{
TestServiceImpl1 serviceImpl1 = provider.GetService<TestServiceImpl1>();
serviceImpl1.Name = "james";
serviceImpl1.sayHi();
TestServiceImpl1 serviceImpl2 = provider.GetService<TestServiceImpl1>();
serviceImpl2.Name = "kobe";
serviceImpl2.sayHi();
//比较两个对象是否相同
Console.WriteLine(object.ReferenceEquals(serviceImpl1, serviceImpl2));
//再次输出serviceImpl1
serviceImpl1.sayHi();
}
}
可以看到,两个服务的是不相同的,并且再次调用第一个服务的方法,服务里面的值也不会改变,这个就是瞬时获取,每个服务实例都不一样;
2.Service.Singletion<T>,单例模式,每次拿到都都是同一个对象实例
static void Main(string[] args)
{
//新建服务容器
ServiceCollection services = new ServiceCollection();
//将服务注册到容器中(单例模式注册)
services.AddSingleton<TestServiceImpl1>();
//再从服务容器中获取指定的服务来使用
using (ServiceProvider provider= services.BuildServiceProvider())
{
TestServiceImpl1 serviceImpl1 = provider.GetService<TestServiceImpl1>();
serviceImpl1.Name = "james";
serviceImpl1.sayHi();
TestServiceImpl1 serviceImpl2 = provider.GetService<TestServiceImpl1>();
serviceImpl2.Name = "kobe";
serviceImpl2.sayHi();
//比较两个对象是否相同
Console.WriteLine(object.ReferenceEquals(serviceImpl1, serviceImpl2));
//再次输出serviceImpl1
serviceImpl1.sayHi();
}
}
可以看到,这个时候两个对象是一样的,并且第一个对象经过第二个对象赋值之后,再重新打印,属性值已经变为第二次赋值的kobe了,这就是单例模式,永远都是一个对象,每次赋值都会把之前的覆盖掉,这种适合于创建不需要状态的服务对象,比如一些辅助帮助类等。
-
3.AddScoped<T>,是指对一定范围内的代码有作用,出了这个范围,服务自动被释放,调用对象的IDispose方法
static void Main(string[] args)
{
//新建服务容器
ServiceCollection services = new ServiceCollection();
//将服务注册到容器中(范围模式注册)
services.AddScoped<TestServiceImpl1>();
//再从服务容器中获取指定的服务来使用
using (ServiceProvider provider= services.BuildServiceProvider())
{
//作用域范围
using (IServiceScope scope1 = provider.CreateScope())
{
var serviceImpl1 = scope1.ServiceProvider.GetService<TestServiceImpl1>();
serviceImpl1.Name = "james";
serviceImpl1.sayHi();
var serviceImpl2 = scope1.ServiceProvider.GetService<TestServiceImpl1>();
serviceImpl1.Name = "kobe";
serviceImpl1.sayHi();
//比较两个对象是否相同
Console.WriteLine(object.ReferenceEquals(serviceImpl1, serviceImpl2));
//再次输出serviceImpl1
serviceImpl1.sayHi();
}
}
}
可以看到,在同一个using范围内,Scope模式获取的两个服务对象是一样的 ,类似于上面单例模式
下面看另外一种情况,在不同作用域中定义的对象进行比较
static void Main(string[] args)
{
//新建服务容器
ServiceCollection services = new ServiceCollection();
//将服务注册到容器中(作用域范围模式注册)
services.AddScoped<TestServiceImpl1>();
//再从服务容器中获取指定的服务来使用
using (ServiceProvider provider= services.BuildServiceProvider())
{
TestServiceImpl1 outSideService;
//作用域范围1
using (IServiceScope scope1 = provider.CreateScope())
{
var serviceImpl1 = scope1.ServiceProvider.GetService<TestServiceImpl1>();
serviceImpl1.Name = "james";
serviceImpl1.sayHi();
outSideService = serviceImpl1;
}
//作用域范围2
using (IServiceScope scope2 = provider.CreateScope())
{
var serviceImpl1 = scope2.ServiceProvider.GetService<TestServiceImpl1>();
serviceImpl1.Name = "james";
serviceImpl1.sayHi();
//比较外部服务对象和内部服务对象
Console.WriteLine(object.ReferenceEquals(outSideService, serviceImpl1));
}
}
}
-
4.需要注意的点
- 不要再长生命周期的对象中引用比它短的生命周期的对象,这样会默认抛异常
- 生命周期的选择:如果类无状态,没有成员属性变量,就是简单的一些处理逻辑返回值等,定义为Singletion;如果有状态,且有Scope控制,建议为Scoped,因为通常这种Scope控制下代码都是运行在同一个线程中的,并没有并发修改的问题;在使用Transient的时候需要谨慎
其他注册方法
- 我们一般把服务定义为接口,要用的对象为接口的实现类,如下:TestServiceImpl2为接口ITestService的实现类
{
class Program
{
static void Main(string[] args)
{
ServiceCollection services = new ServiceCollection();
services.AddScoped<ITestService, TestServiceImpl2>();
using (ServiceProvider sp = services.BuildServiceProvider())
{
ITestService implService = sp.GetService<ITestService>();
implService.Name = "John";
implService.sayHi();
Console.WriteLine(implService.GetType());
}
}
}
interface ITestService {
public string Name { get; set; }
public void sayHi();
}
class TestServiceImpl1:ITestService
{
public string Name { get; set; }
public void sayHi()
{
Console.WriteLine("hello,my name is "+ Name);
}
}
class TestServiceImpl2 : ITestService
{
public string Name { get; set; }
public void sayHi()
{
Console.WriteLine("你好,我的名字是 " + Name);
}
}
}
运行结果为:
- 在provider.GetService<T>的方法里我们也可以使用typeof(T)的方式来获取服务对象:如下:
static void Main(string[] args)
{
ServiceCollection services = new ServiceCollection();
services.AddScoped<ITestService, TestServiceImpl2>();
using (ServiceProvider sp = services.BuildServiceProvider())
{
ITestService implService =(ITestService) sp.GetService(typeof(ITestService));
implService.Name = "John";
implService.sayHi();
Console.WriteLine(implService.GetType());
}
}
运行结果也是一样的,如下:
- 还有可以使用provider.GetRequiredService<T>()方法,这种方式,如果泛型T不是注册时的服务类型,则会抛出异常,所以,在获取服务对象时的类型必须与注册时候的类型一致:
- provider.GetServices(),获取该注册服务的多个实现类集合。如果注册服务是一个接口,此接口下面有多个实现类的话,那么使用GetServices方法可以遍历出所有的实现类(前提是这个实现类必须跟随接口一起注册在服务容器中):
static void Main(string[] args)
{
ServiceCollection services = new ServiceCollection();
services.AddScoped<ITestService, TestServiceImpl1>();
services.AddScoped<ITestService, TestServiceImpl2>();
using (ServiceProvider sp = services.BuildServiceProvider())
{
var implServices = sp.GetServices<ITestService>();
foreach (var item in implServices)
{
Console.WriteLine(item.GetType());
}
}
}
运行结果如下:两个实现类都打印出来了
- 注意,如果一个服务接口注册了多个实现类,此时使用GetService<T>方法获取的是最后一个注册到容器中的子类类型,如下:
打印结果如下: