DI容器的职责

先了解一下DI容器的职责是什么。

职责

一般情况下DI容器会提供如下API:

  • 注册类型:Register<TSource,TTarget>
  • 注入:Inject(object obj)
  • 解析:Resolve<T>()

上一篇的例子中,我们使用了RegisterInject,剩下的Resolve在DIContainer中,作用就是根据type返回对应的实例。

上一篇我们用Inject方法,其内部就是调用了Resolve来返回实例的。

在决定版架构中,Resolve方法就是IOCCointainer里面的Get方法

1
2
3
4
5
6
7
8
9
10
11
public T Get<T>() where T : class
{
var key = typeof(T);

if (mInstances.TryGetValue(key, out var retInstance))
{
return retInstance as T;
}

return null;
}

这个方法很简单,并且其中说明了每次返回的是同一个实例。

但是在标准的DI容器中,会根据想要的type进行一个判断,如果这个type在加入容器时说明它只有一个实例,那么就返回那个实例,也就是当单例使用;如果没有,那么每次都返回一个新实例。

DI容器的职责是:管理依赖和注入依赖,管理是通过type来管理,注入是向一个对象注入实例。一般DIContainer会用一个Dictionary<Type,object>来做为核心数据结构(实际上可能更复杂,在IOCKit中我们可以看到实际应用的Dictionary的key是一个多值key),根据Type来得到type的实例,就是这个容器能“管理依赖”的概念。

DI容器的优点

之前我们学习了单例的最佳实践,用单例可以很容易实现一个独立模块,而且与其他模块的交互也很容易,但是当单例的数量随着项目变多的时候,单例的瓶颈就出现了:因为单例没有获取限制,会让项目的上下层级混乱,尤其是在多人合作开发过程中。

我们可以设计一下单例的使用,让它规范一点,比如最顶层的模块都用单例,底层模块作为顶层模块的成员变量,从而逻辑层(表现层)无法直接访问底层模块,而是必须通过顶层模块间接地使用底层模块的服务。

对单例进行设计

这样设计虽然有层级,但是失去了单例易扩展和好维护的优点,必须要考虑依赖关系,还必须要考虑各个模块的生命周期。

单例很难从技术角度给自己添加限制,这个时候DI的优势就体现出来了,能通过技术限制分担一部分约定,还易维护,易拓展。

什么是生命周期的控制

当一个对象依赖另一个对象时,被依赖的对象(或模块)的生命周期一直是架构设计的重点。生命周期问题的其中之一就是要不要“new”、什么时候“new”的问题。

先看如下代码:

1
2
3
4
5
6
7
8
public class ModuleA
{
public ModuleB ModuleB;
}
public class ModuleB
{

}

代码中ModuleA依赖了ModuleB,ModuleB的生命周期就成了我们主要考虑的问题。

情况一:在A内部直接创建实例

1
2
3
4
public class ModuleA
{
public ModuleB ModuleB = new ModuleB();
}

这种情况在ModuleB是简单对象时下很好用,但ModuleB如果是个模块,需要在别的地方公用,就会有问题。

情况二:在外部创建对象

1
2
3
4
5
void main()
{
var moduleA = new ModuleA();
moduleA.ModuleB = new ModuleB();
}

这种情况下我们可以知道ModuleB是有实例的,但此时如果我们在moduleA中首先引用了ModuleB,就会出问题

1
2
3
4
5
6
7
8
9
public class ModuleA
{
public ModuleB ModuleB;
//...
void XXX()
{
ModuleB.//ModuleB到底有没有值?在哪里设置的值?
}
}

这个时候我们都会想到单例,因为单例就是解决生命周期问题的方法之一

1
2
3
4
5
6
7
public class ModuleA
{
void XXX()
{
ModuleB.Instance.Dosomething();
}
}

在有大量依赖的结构中,不易维护也不宜扩展就体现在此,因为依赖都要考虑对象的创建过程。

使用DI Container管理依赖

使用DI Container后,代码就会变成如下:

1
2
3
4
5
6
7
8
9
10
public class ModuleA
{
[Inject]
public ModuleB ModuleB{get;set;};
//...
void XXX()
{
ModuleB.Dosomething();
}
}

在启动时,我们需要统一注册依赖

1
2
3
4
5
6
7
public static QFrameworkContainer Container{get;private set;}

void Main()
{
Container = new Container();
Container.Register<ModuleB>();
}

在创建ModuleA对象时注入依赖。

1
2
3
4
void MethodA()
{
App.Container.Inject(new ModuleA());//ModuleA内部不管引用了多少个模块(ModuleD、ModuleE。。。)都可以注入进去
}

使用DI Container之后,结构就会变成下面:

没有依赖

在模块内部声明变量,不需要考虑创建过程,这样每个模块都是可独立测试的(易维护),而扩展一个模块,不需要使用单例实现,而是注册到类型容器即可,比单例还容易。

设计依赖关系就是设计架构

从最开始的对象之间的交互,到单例模式,再到消息机制(观察者模式),都离不开依赖这个概念,设计依赖关系就是设计架构。而DI Container有的时候被叫做架构工具,就是因为它可以让我们从依赖关系的角度看待整个架构,而DIContainer又直接参与了依赖的管理,是我们能够去设计依赖关系,所以很多架构师都非常喜欢用DIContainer。

狠多大名鼎鼎的框架都大量使用了DIContainer。.Net Core、Spring(Java)、uFrame(Unity)等。

DI容器生命周期

在上面我们说过,DIContainer的职责之一就是管理生命周期,这个生命周期一般也叫做“Scope”或“域”。

在IOCKit DIContainer方案中,根据使用注册API的不同来确定是否返回同一个实例:

  • 当注册依赖时,使用Register<Type>()方法时,每次注入都会创建一个新的实例,一般情况下管这种方式叫做Transient(短暂的)。
  • 当注册依赖时,使用RegisterInstance<Type>()方法时,每次注入都会使用同一个实例,一般情况下管这种方式叫做Singleton(单例)。

我们测试一下:

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
using UnityEngine;

namespace FrameworkDesign2021.Example
{
public class SomeService//经常复用的使用RegisterInstance
{
public void Description()
{
Debug.Log("SomeService:" + this.GetHashCode());
}
}

public class SomeObject//一般对象使用Register
{
public void Description()
{
Debug.Log("SomeObject:" + this.GetHashCode());
}
}

public class IOCScopeExample : MonoBehaviour
{
[Inject] public SomeObject ObjA {get;set;}
[Inject] public SomeObject ObjB {get;set;}
[Inject] public SomeObject ObjC {get;set;}

[Inject] public SomeService ServiceA {get;set;}
[Inject] public SomeService ServiceB {get;set;}

void Start()
{
var container = new QFrameworkContainer();

container.Register<SomeObject>();

container.RegisterInstance(new SomeService());

container.Inject(this);

ObjA.Description();//output: 2127165056
ObjB.Description();//output: 1661804032
ObjC.Description();//output: 1196443008
ServiceA.Description();//output: -1702441216
ServiceB.Description();//output: -1702441216
}
}
}

通过HashCode来判断当前对象是否是同一个对象