IOC的优点和生命周期
DI容器的职责
先了解一下DI容器的职责是什么。
职责
一般情况下DI容器会提供如下API:
- 注册类型:
Register<TSource,TTarget>
- 注入:
Inject(object obj)
- 解析:
Resolve<T>()
上一篇的例子中,我们使用了Register
和Inject
,剩下的Resolve
在DIContainer中,作用就是根据type返回对应的实例。
上一篇我们用Inject
方法,其内部就是调用了Resolve
来返回实例的。
在决定版架构中,Resolve
方法就是IOCCointainer里面的Get方法
1 | public T Get<T>() where T : class |
这个方法很简单,并且其中说明了每次返回的是同一个实例。
但是在标准的DI容器中,会根据想要的type进行一个判断,如果这个type在加入容器时说明它只有一个实例,那么就返回那个实例,也就是当单例使用;如果没有,那么每次都返回一个新实例。
DI容器的职责是:管理依赖和注入依赖,管理是通过type来管理,注入是向一个对象注入实例。一般DIContainer会用一个Dictionary<Type,object>来做为核心数据结构(实际上可能更复杂,在IOCKit中我们可以看到实际应用的Dictionary的key是一个多值key),根据Type来得到type的实例,就是这个容器能“管理依赖”的概念。
DI容器的优点
之前我们学习了单例的最佳实践,用单例可以很容易实现一个独立模块,而且与其他模块的交互也很容易,但是当单例的数量随着项目变多的时候,单例的瓶颈就出现了:因为单例没有获取限制,会让项目的上下层级混乱,尤其是在多人合作开发过程中。
我们可以设计一下单例的使用,让它规范一点,比如最顶层的模块都用单例,底层模块作为顶层模块的成员变量,从而逻辑层(表现层)无法直接访问底层模块,而是必须通过顶层模块间接地使用底层模块的服务。
这样设计虽然有层级,但是失去了单例易扩展和好维护的优点,必须要考虑依赖关系,还必须要考虑各个模块的生命周期。
单例很难从技术角度给自己添加限制,这个时候DI的优势就体现出来了,能通过技术限制分担一部分约定,还易维护,易拓展。
什么是生命周期的控制
当一个对象依赖另一个对象时,被依赖的对象(或模块)的生命周期一直是架构设计的重点。生命周期问题的其中之一就是要不要“new”、什么时候“new”的问题。
先看如下代码:
1 | public class ModuleA |
代码中ModuleA依赖了ModuleB,ModuleB的生命周期就成了我们主要考虑的问题。
情况一:在A内部直接创建实例
1 | public class ModuleA |
这种情况在ModuleB是简单对象时下很好用,但ModuleB如果是个模块,需要在别的地方公用,就会有问题。
情况二:在外部创建对象
1 | void main() |
这种情况下我们可以知道ModuleB是有实例的,但此时如果我们在moduleA中首先引用了ModuleB,就会出问题
1 | public class ModuleA |
这个时候我们都会想到单例,因为单例就是解决生命周期问题的方法之一
1 | public class ModuleA |
在有大量依赖的结构中,不易维护也不宜扩展就体现在此,因为依赖都要考虑对象的创建过程。
使用DI Container管理依赖
使用DI Container后,代码就会变成如下:
1 | public class ModuleA |
在启动时,我们需要统一注册依赖
1 | public static QFrameworkContainer Container{get;private set;} |
在创建ModuleA对象时注入依赖。
1 | void MethodA() |
使用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 | using UnityEngine; |
通过HashCode来判断当前对象是否是同一个对象