依赖注入

控制反转(inversion of control, IoC)是设计模式中非常重要的思想,而依赖注入(dependency iniection, DI)是控制反转思想的一种重要的实现方式。依赖注入简化了模块的组装过程,减小了模块之间的耦合性,因此.NET Core中大量应用了依赖注入的开发模式。

DI几个概念

服务(service):对象;
注册服务;
服务容器:负责管理注册的服务;
查询服务:创建对象及关联对象;
对象生命周期:Transient(瞬态);Scoped(范围);Singleton(单例);

微软把内置.NET Core的控制反转组件命名为DependencyInjection,但是它含了服务定位器的功能。把这个功能统一称为依赖注入。

1.瞬态(transient):每次被请求的时候都会创建一个新对象。这种生命周期适合有状态的对象,可以避免多段的代码用于同一个对象而造成状态混乱,其缺点是生成的对象比较多,会浪费内存。

2.范围(scoped):再给定的范围内,多次请求共享同一个服务对象,服务每次被请求的时候都会返回同一个对象;在不同的范围内,服务每次被我请求的时候对返回不同的对象。这个范围可以由框架定义,也可以由开发人员自定义。在ASP.NET Core中,服务默认的范围是第一次HTTP请求,也就是在同一次HTTP请求中,不同的注入会获得同一个对象;在不同的HTTP请求中,不同的注入会获得不同的对象。这种方式适合用于在同一个范围内共享同一个对象的情况。

3.单例(singleton):全局共享同一个服务对象。这种生命周期可以节省创建新对象的资源。为了避免并发修改等问题,单列的服务对象最好是无状态对象。

这三种生命周期中如何选择:如果一个类没有状态,建议把服务的声明周期设置为单列;如果一个类有状态,并且在框架环境中的范围控制(比如ASP.NET Core中有默认的请求相关的范围),在这种情况下建议把服务的生命周期设置为范围,因为通常在范围控制下,代码都是运行在同一个线程中的,没有并发修改的问题;在使用瞬态生命周期的时候要谨慎,尽量在子范围中使用它们,因为如果我们控制不好,容易造成程序中出现内存泄露的问题。

依赖注入框架是根据服务的类型来获取服务的,因此在获取服务的时候必须指定获取什么类型的服务。依赖注入注册服务的时候可以分别指定服务类型和实现类型,这两者可能相同,也可能不同。
比如在注册服务的时候,可以设定服务类型和实现类型都是SqlConnection,这样在获取SqlConnection类型服务的时候,容器就会返回注册的SqlConnection类型的对象;也可以在注册服务的时候,设定服务类型是IDbConnection,实现类型是SqlConnection,这样在获取IDbConnection类型服务的时候,容器就会返回注册的SqlConnection类型的对象。(在注册服务的时候只能要求注入IDbConnection)类型的服务,不能直接要求注入SqlConnection类型的服务。

在面向对象编程中,推荐使用面向接口编程,这样我们的代码就依赖于服务接口,而不是依赖于实现类,可以实现代码解耦。因此在使用依赖注入的时候,推荐服务类型用接口类型。

测试用的服务的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface ITestService
{
public string Name { get; set; }
public void SayHi();
}

public class TestServiceImp1 : ITestService
{
public string Name { get; set; }

public void SayHi()
{
Console.WriteLine($"Hi,I'm {Name}");
}
}

创建用于注册服务的容器。容器的接口是IServiceCollection,其默认实现类是ServiceCollection。IServiceCollecion接口中定义了AddTransient、AddScoped和AddSingleton这三组扩展方法,分别用来注册瞬态、范围和单例服务。注册完成后,我们调用IServiceCollection的BuildDerviceProvider方法创建一个ServiceProivider对象,这个ServiceProvider对象就是一个服务定位器。由于ServiceProvider对象实现了IDisposable接口,因此需要使用using对其进行资源的释放。在我们需要获取服务的时候,可以调用ServiceProvider类的GetRequiredService方法。

服务的注册及获取过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using Microsoft.Extensions.DependencyInjection;

ServiceCollection services = new ServiceCollection();
services.AddTransient<TestServiceImp1>();
using (ServiceProvider sp = services.BuildServiceProvider())
{
TestServiceImp1 testService = sp.GetService<TestServiceImp1>();
testService.Name = "张三";
testService.SayHi();
}
public interface ITestService
{
public string Name { get; set; }
public void SayHi();
}

public class TestServiceImp1 : ITestService
{
public string Name { get; set; }

public void SayHi()
{
Console.WriteLine($"Hi,I'm {Name}");
}
}

TestServiceImpl注册为瞬态服务,然后在第6行代码中通过GetRequiredService方法来获取TestServiceImpl对象,这种用法属于服务定位器方式。(如果一个被依赖注入容器管理的类实现了IDisposable接口,则离开作用域之后容器会自动调用对象的Dispose方法,这样就可以及时释放非托管资源)

不要再生命周期的对象中引用比它短的生命周期的对象。比如不能在单例服务中引用范围服务,否则可能会导致被引用的对象已经释放或这导致内存泄漏。在ASP.NET Core的默认依赖注入容器中,这种“在长生命周期的对象中引用短生命周期的对象”的代码会发生异常。

DI魅力渐显:依赖注入

1.依赖注入是有“传染性”的,如果一个类的对象是通过DI创建的,那么这个类的构造函数中声明的所有服务类型的参数都会被DI赋值;但是如果一个对象是程序员手动创建的,那么这个对象就和DI没有关系,它的构造函数中声明的服务类型参数就不会被自动赋值。
2..NET的DI默认是构造函数注入。
3.举例:编写一个类,连接数据库做插入操作,并且记录日志(模拟的输出),把Dao、日志都放入单独的服务类。connstr见备注。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
using Microsoft.Extensions.DependencyInjection;

ServiceCollection services = new ServiceCollection();
services.AddScoped<Controller>();
services.AddScoped<ILog, LogImp1>();
services.AddScoped<IStorage, StorageImp1>();
services.AddScoped<IConfig, ConfigImp1>();

using(var sp=services.BuildServiceProvider())
{
var c=sp.GetRequiredService<Controller>();
c.Test();
}
Console.ReadKey();
class Controller
{
private readonly ILog log;
private readonly IStorage storage;

public Controller(ILog log, IStorage storage)
{
this.log = log;
this.storage = storage;
}
public void Test()
{
this.log.Log("开始测试");
this.storage.Save("测试内容", "test.txt");
this.log.Log("测试结束");
}
}
interface ILog
{
public void Log(string msg);
}

class LogImp1 : ILog
{
public void Log(string msg)
{
Console.WriteLine($"日志:{msg}");
}
}
interface IConfig
{
public string GetValue(string name);
}
class ConfigImp1 : IConfig
{
public string GetValue(string name)
{
return "hello";
}
}
interface IStorage
{
public void Save(string content, string name);
}
class StorageImp1 : IStorage
{
private readonly IConfig config;
public StorageImp1(IConfig config)
{
this.config = config;
}

public void Save(string content, string name)
{
string server=config.GetValue("server");
Console.WriteLine($"向服务器{server}的文件名为{name}的文件中写入内容{content}");
}
}