控制反转、依赖注入(IOC、DI)
- IOC: Inversion Of Control 控制反转
- DI: Dependency Injection 依赖注入
1.控制反转 Inversion Of Control 的前世今生
1.1 IOC理论产生的背景
讨论控制反转之前,先看看软件系统提出控制反转
的前世今生。
一个完整精密的软件系统,组件之间就像齿轮,协同工作,相互耦合。
- 一个零件不正常,整个系统就崩溃了。
- 系统对象之间耦合关系无法避免,在项目规模和复杂度变大的情况下,管理类之间的依赖关系将会很复杂。
- 对象之间耦合度很高的系统,架构师和开发人员对于系统的修改,必然会出现牵一发而动全身的情形。
- 对象之间耦合性依赖,单元测试很复杂。
1.2 IOC理论
软件专家为此提出IOC理论,用来实现对象之间的解耦。
再来看看,控制反转(IOC)到底为什么要起这么个名字?我们来对比一下:
- 软件系统在没有引入IOC容器之前,对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,自己必须主动去创建对象B或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手上。
- 软件系统在引入IOC容器之后,这种情形就完全改变了,由于IOC容器的加入,对象A与对象B之间失去了直接联系,所以,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。
通过前后对比,我们不难看出:
对象A获得依赖对象B的过程,由主动变为了被动行为,控制权颠倒过来,这就是“控制反转”的由来。
1.3 控制反转 和 依赖注入
有些人会把控制反转和依赖注入等同,实际上有本质区别:
控制反转是一种思想;依赖注入是一种设计模式。
依赖注入是实现控制反转的一种方式,但是控制反转还有其他实现方式,例如说ServiceLocator
(服务定位器、依赖查找),所以不能将控制反转和依赖注入等同。
2 依赖注入 Dependency Injection
依赖注入:容器全权负责组件的装配,它会把符合依赖关系的对象通过属性或者构造函数传递给需要的对象。
符合依赖倒置原则,高层模块不应该依赖低层模块,两者都应该依赖其抽象
2.1 ASP.NET Core依赖注入
使用方式大体类似:
- 定义依赖实现的接口或者抽象类
- 在服务容器中注册组件依赖 :
IServiceProvider
- 在构造函数中注入服务, 框架会负责创建和销毁实例
1 // 编写组件和服务 2 public interface IMyDependency 3 { 4 string WriteMessage(string message); 5 } 6 --- 7 public class MyDependency : IMyDependency 8 { 9 public string WriteMessage(string message) 10 { 11 return $"MyDependency.WriteMessage Message: {message}"; 12 } 13 } 14 // 注册组件和依赖,下面注册的`IMyDependency`在一个web请求中有效 15 public void ConfigureServices(IServiceCollection services) 16 { 17 services.AddScoped<IMyDependency, MyDependency>(); 18 services.AddRazorPages(); 19 } 20 --- 21 // 在构造函数注入组件 22 public class HomeController: AbpController 23 { 24 private readonly IMyDependency _dep; 25 public HomeController(IMyDependency dep) 26 { 27 _dep = dep; 28 } 29 30 public IActionResult Index() 31 { 32 var content = _dep.WriteMessage($"The Reflection instance is {_dep.GetType().FullName} "); 33 return Content(content); 34 } 35 }
在请求某个服务时,框架会完整解析出这个对象的依赖树和作用范围。
上面的示例代码形成 req--->HomeController--->IMyDependency依赖树。
IMyDependency在每个web请求范围内使用同一服务实例。
输出:MyDependency.WriteMessage Message: The Reflection instance is TestDI.MyDependency
2.2 对象生命周期
根据现实需要,前人从使用场景中总结出三种服务生命周期。
ASP.NET Core提供了一个枚举ServiceLifetime
:
-- | --- | --- | --- |
---|---|---|---|
Singleton | 单例 | 服务容器首次请求会创建,后续都使用同一实例 | AddSingleton |
Scoped | 特定范围 | 在一个请求(连接)周期内使用一个示例 | AddScoped |
Transient | 瞬时 | 服务容器每次请求,都会创建一个实例 | AddTransient |
对于Scoped Service
的理解:
在webapp:scoped service 会在请求结束时被销毁;
在EFCore:使用AddDbContext默认注册的是特定范围的DbContext,这意味在我们可以在一次sql连接内,使用同一个DbContext实例进行多次DB操作。
2.3 依赖注入实现原理
结合理论、使用方式 猜测依赖注入的原理:
实现DI,核心在于依赖注入容器IContainer
,该容器具有以下功能
①.(容器)保存可用服务的集合
// 要用的特定对象、特定类、接口服务
②.(注册)提供一种方式将各种部件与他们依赖的服务绑定到一起;
// Add...函数或containerBuilder.Register函数
③.(解析点)为应用程序提供一种方式来请求已配置的对象:构造函数注入、属性注入.
运行时,框架会一层层通过反射构造实例,最终得到完整对象。
3.源码导航
利用反射
产生对象是依赖注入的核心过程,这也是面试造航母时经常问到的。
.NETSystem.Reflection
、System.Type
命名空间中的类可以获取可装配组件、类、接口的信息,并提供了在运行时创建实例,调用动态实例方法、获取动态实例的能力。
实际上,我们可以在依赖树的尾部对象的构造函数手动抛出异常,异常的调用栈就是一个天然的源码导航。
于是我在上面示例代码的request----> HomeController--->MyDependency MyDependency构造函数中添加异常代码:
1 public MyDependency() 2 { 3 throw new Exception("exception content!"); 4 }
结果如下图:
从Github Dependency Injection 库进入System.Reflection的调用分界线代码:
1 protected override object VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context) 2 { 3 object[] parameterValues; 4 if (constructorCallSite.ParameterCallSites.Length == 0) 5 { 6 parameterValues = Array.Empty<object>(); 7 } 8 else 9 { 10 parameterValues = new object[constructorCallSite.ParameterCallSites.Length]; 11 for (var index = 0; index < parameterValues.Length; index++) 12 { 13 parameterValues[index] = VisitCallSite(constructorCallSite.ParameterCallSites[index], context); 14 } 15 } 16 17 try 18 { 19 return constructorCallSite.ConstructorInfo.Invoke(parameterValues); 20 } 21 catch (Exception ex) when (ex.InnerException != null) 22 { 23 ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); 24 // The above line will always throw, but the compiler requires we throw explicitly. 25 throw; 26 } 27 }
黄色背景行就是.NET反射特性的体现:
对类型信息(构造函数、参数)使用Invoke
方法产生对象。
干货旁白
- 控制反转是一种在软件工程中解耦合的思想,调用方依赖接口或抽象类,减少了耦合,控制权交给了服务容器,由容器维护注册项,并将具体的实现动态注入到调用方。
- 有些人会把控制反转和依赖注入等同,实际上有本质区别:
- 控制反转是一种思想;
- 依赖注入是一种设计模式。
- 依赖注入是实现控制反转的一种方式,但是控制反转还有其他实现方式,例如说
ServiceLocator
,所以不能将控制反转和依赖注入等同。 - 在运行时,框架会解析依赖树、依赖图,通过反射在运行期生成对象。