手写IOC容器-探究IOC的本质原理
点击上方蓝色字体,选择“标星公众号”
优质文章,第一时间送达
作者 | hello-*-world
来源 | cnblogs.com/HTLucky/p/13379900.html
IOC(控制翻转)是程序设计的一种思想,其本质就是上端对象不能直接依赖于下端对象,要是依赖的话就要通过抽象来依赖。这是什么意思呢?意思就是上端对象如BLL层中,需要调用下端对象的DAL层时不能直接调用DAl的具体实现,而是通过抽象的方式来进行调用。这样做是有一定的道理的。有这么一个场景,你们的项目本来是用Sqlserver来进行数据访问的,那么就会有一个SqlserverDal对象。BLL层调用的时候通过new SqlserverDal(),直接创建一个SqlserverDal对象进行数据访问,现在项目又要改为Mysql数据库,用MysqlDal进行数据访问。这时候就麻烦了,你的BLL层将new SqlserverDal()全部改为new MysqlDal()。同理BLL层也是这个道理。这么做,从程序的架构而言是相当不合理的,我只是想将SqlserverDal替换为MysqlDal。按道理说我只要添加MysqlDal对象就可以了。可现在的做法是还要将BLL中的new SqlserverDal()全部改一遍。这未免有点得不偿失了。这时IOC就排上用场了,IOC的核心理念就是上端对象通过抽象来依赖下端对象,那么我们在BLL中,不能直接通过new SqlserverDal()来创建一个对象,而是通过结构来声明(抽象的形式来进行依赖),当我们替换MysqlDal时我们只需让MysqlDal也继承这个接口,那么我们BLL层的逻辑就不用动了。那么现在又有一个问题,对象我们可以用接口来接收,所有子类出现的地方都可以用父类来替代,这没毛病。但对象的创建还是要知道具体的类型,还是通过之前的new SqlserverDal()这种方式创建对象。肯定是不合理的,这里我们还是依赖于细节。
那我们需要怎么处理呢?这时候IOC容器就该上场了,IOC容器可以理解为一个第三方的类,专门为我们创建对象用的,它不需要关注具体的业务逻辑,也不关注具体的细节。你只需将你需要的创建的对象类型传给它,它就能帮我们完成对象的创建。常见的IOC容器有Autofac,Unity
接触.net core的小伙伴可能对容器很熟悉,.net core中将IOC容器内置了。创建对象需要先进行注册
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient();
services.AddTransient();
}
从上面的示例我们可以看到.net core中是通过ServiceCollection容器帮我们完成对象的创建,我们只需将接口的类型和要创建对象的类型传进去,它就能帮我们完成对象的创建。那么它的原理是啥呢,我们能不能创建自已的容器来帮我们完成对象的创建呢,让我们带着疑惑继续往下走
一.容器雏形
这里我们先不考虑那么多,我们先写一个容器,帮我们完成对象的创建工作。
public class HTContainer : IHTContainer
{
//创建一个Dictionary数据类型的对象用来存储注册的对象
private Dictionary<string, Type> TypeDictionary = new Dictionary<string, Type>();
//注册方法,用接口的FullName为key值,value为要创建对象的类型
public void RegisterType()
{
this.TypeDictionary.Add(typeof(IT).FullName, typeof(T));
}
//创建对象通过传递的类型进行匹配
public IT Resolve()
{
string key = typeof(IT).FullName;
Type type = this.TypeDictionary[key]; //获取要创建对象的类型
//这里先不考虑有参构造函数的问题,后面会逐一的解决这些问题
return (IT)Activator.CreateInstance(type); //通过反射完成对象的创建,这里我们先不考虑参数问题
}
}
简单调用
//实例化容器对象
IHTContainer container = new HTContainer();
//注册对象
container.RegisterType();
//通过容器完成对象的创建,不体现细节,用抽象完成对象的创建
IDatabase dal = container.Resolve();
dal.Connection("con");
通过上边的一顿操作,我们做了什么事呢?我们完成了一个大的飞跃,通常创建对象我们是直接new一个,现在我们是通过一个第三方的容器为我们创建对象,并且我们不用依赖于细节,通过接口的类型完成对象的创建,当我们要将SqlserverDal替换为MysqlDal时,我们只需要在注册的时候将SqlserverDal替换为MysqlDal即可
二.升级改造容器(解决参数问题)
上面我们将传统对象创建的方式,改为使用第三方容器来帮我们完成对象的创建。但这个容器考虑的还不是那么的全面,例如有参构造的问题,以及对象的依赖问题我们还没有考虑到,接下来我们继续完善这个容器,这里我们先不考虑多个构造函数的问题。这里先解决只有一个构造函数场景的参数问题
1.构造函数只有一个参数的情况
//创建对象通过传递的类型进行匹配
public IT Resolve()
{
string key = typeof(IT).FullName;
Type type = this.TypeDictionary[key]; //获取要创建对象的类型
var ctor = type.GetConstructors()[0]; //这里先考虑只有一个构造函数的场景
//一个参数的形式
var paraList = ctor.GetParameters();
var para = paraList[0];
Type paraInterfaceType = para.ParameterType;
Type paraType = this.TypeDictionary[paraInterfaceType.FullName]; //还是要先获取依赖对象的类型
object oPara = Activator.CreateInstance(paraType); //创建参数中所依赖的对象
return (IT)Activator.CreateInstance(type,oPara); //创建对象并传递所依赖的对象
}
2.构造函数多参数的情况
上面我们解决了构造函数只有一个参数的问题,我们是通过构造函数的类型创建一个对象,并将这个对象作为参数传递到要实例化的对象中。那么多参数我们就需要创建多个参数的对象传递到要实例的对象中
//创建对象通过传递的类型进行匹配
public IT Resolve()
{
string key = typeof(IT).FullName;
Type type = this.TypeDictionary[key]; //获取要创建对象的类型
var ctor = type.GetConstructors()[0]; //这里先考虑只有一个构造函数的场景
//多个参数的形式
List<object> paraList = new List<object>(); //声明一个list来存储参数类型的对象
foreach (var para in ctor.GetParameters())
{
Type paraInterfaceType = para.ParameterType;
Type paraType = this.TypeDictionary[paraInterfaceType.FullName];
object oPara = Activator.CreateInstance(paraType);
paraList.Add(oPara);
}
return (IT)Activator.CreateInstance(type, paraList.ToArray()); //创建对象并传递所依赖的对象数组
}
3.解决对象的循环依赖问题
通过上面的两步操作,我们已经能对构造函数中的参数初始化对象并传递到要实例的对象中,但这只是一个层级的。我们刚才做的只是解决了这么一个问题,假设我们要创建A对象,A对象依赖于B对象。我们做的就是创建了B对象作为参数传递给A并创建A对象,这只是一个层级的。当B对象又依赖于C对象,C对象又依赖于D对象,这么一直循环下去。这样的场景我们该怎么解决呢?下面我们将通过递归的方式来解决这一问题
//创建对象通过传递的类型进行匹配
public IT Resolve()
{
return (IT)this.ResolveObject(typeof(IT));
}
//通过递归的方式创建多层级的对象
private object ResolveObject(Type abstractType)
{
string key = abstractType.FullName;
Type type = this.TypeDictionary[key]; //获取要创建对象的类型
var ctor = type.GetConstructors()[0];
//多个参数的形式
List<object> paraList = new List<object>();
foreach (var para in ctor.GetParameters())
{
Type paraInterfaceType = para.ParameterType;
Type paraType = this.TypeDictionary[paraInterfaceType.FullName];
object oPara = ResolveObject(paraInterfaceType); //自已调用自己,实现递归操作,完成各个层级对象的创建
paraList.Add(oPara);
}
return (object)Activator.CreateInstance(type, paraList.ToArray());
}
三.继续升级(考虑多个构造函数的问题)
上面我们只是考虑了只有一个构造函数的问题,那初始化的对象有多个构造函数我们该如何处理呢,我们可以像Autofac那样选择一个参数最多的构造函数,也可以像ServiceCollection那样选择一个参数的超集来进行构造,当然我们也可以声明一个特性,那个构造函数中标记了这个特性,我们就采用那个构造函数。
//通过递归的方式创建多层级的对象
private object ResolveObject(Type abstractType)
{
string key = abstractType.FullName;
Type type = this.TypeDictionary[key]; //获取要创建对象的类型
var ctorArray = type.GetConstructors(); //获取对象的所有构造函数
ConstructorInfo ctor = null;
//判断构造函数中是否标记了HTAttribute这个特性
if (ctorArray.Count(c => c.IsDefined(typeof(HTAttribute), true)) > 0)
{
//若标记了HTAttribute特性,默认就采用这个构造函数
ctor = ctorArray.FirstOrDefault(c => c.IsDefined(typeof(HTAttribute), true));
}
else
{
//若都没有标记特性,那就采用构造函数中参数最多的构造函数
ctor = ctorArray.OrderByDescending(c => c.GetParameters().Length).FirstOrDefault();
}
//多个参数的形式
List
上面的操作我们通过依赖注入的方式完成了对容器的升级,那么依赖注入到底是啥呢?
依赖注入(Dependency Injection,简称DI)就是构造A对象时,需要依赖B对象,那么就先构造B对象作为参数传递到A对象,这种对象初始化并注入的技术就叫做依赖注入。IOC是一种设计模式,程序架构的目标。DI是IOC的实现手段
感谢点赞支持下哈