来,从零手写一个IOC容器

x33g5p2x  于2022-06-16 转载在 其他  
字(6.7k)|赞(0)|评价(0)|浏览(422)

一、简介

IOC(控制翻转)是程序设计的一种思想,其本质就是上端对象不能直接依赖于下端对象,要是依赖的话就要通过抽象来依赖。比如,上端对象如 BLL 层中,需要调用下端对象的 DAL 层时不能直接调用 DAl 的具体实现,而是通过抽象的方式来进行调用。

有这么一个场景,项目本来是用 Sqlserver 来进行数据访问的,那么就会有一个 SqlserverDal 对象。BLL 层调用的时候通过 new SqlserverDal(),直接创建一个 SqlserverDal 对象进行数据访问,现在项目又要改为 Mysql 数据库,用 MysqlDal 进行数据访问。这时候就麻烦了,你的 BLL 层将 new SqlserverDal() 全部改为 new MysqlDal()。同理 BLL 层也是这个道理。这么做,从程序的架构而言是相当不合理的。

这时 IOC 就排上用场了,IOC 的核心理念就是上端对象通过抽象来依赖下端对象,那么我们在 BLL 中,不能直接通过 new SqlserverDal()来创建一个对象,而是通过结构来声明(抽象的形式来进行依赖),当我们替换 MysqlDal 时我们只需让 MysqlDal 也继承这个接口,那么我们 BLL 层的逻辑就不用动了。

现在又有一个问题,对象我们可以用接口来接收,所有子类出现的地方都可以用父类来替代,这没毛病。但对象的创建还是要知道具体的类型,还是通过之前的 new SqlserverDal() 这种方式创建对象。肯定是不合理的,这里我们还是依赖于细节。这时候 IOC 容器就该上场了,IOC 容器可以理解为一个第三方的类,专门为我们创建对象用的,它不需要关注具体的业务逻辑,也不关注具体的细节。你只需将你需要的创建的对象类型传给它,它就能帮我们完成对象的创建。

二、.Net 内置 IOC

接触 .Net Core 的小伙伴可能对容器很熟悉,.Net Core 中将 IOC 容器内置了,创建对象需要先进行注册。如下:

public void ConfigureServices(IServiceCollection services)
{
       services.AddTransient<IHomeBll,HomeBll>();
       services.AddTransient<Iservice,LoginService>();
}

三、手写 IOC 容器

从 .Net Core 中可以看出,是通过 ServiceCollection 容器帮我们完成对象的创建,我们只需将接口的类型和要创建对象的类型传进去,它就能帮我们完成对象的创建。那么它是什么原理呢,我们能不能创建自已的容器来帮我们完成对象的创建呢? 答案:当然可以。

3.1 容器雏形

首先不考虑那么多,先写一个容器,帮我们完成对象的创建工作。如下:

public interface IHTContainer
{
    void RegisterType<IT, T>();
    IT Resolve<IT>();
}
public class HTContainer : IHTContainer
{
    //创建一个Dictionary数据类型的对象用来存储注册的对象
    private Dictionary<string, Type> TypeDictionary = new Dictionary<string, Type>();

    //注册方法,用接口的FullName为key值,value为要创建对象的类型
    public void RegisterType<IT, T>()
    {
        this.TypeDictionary.Add(typeof(IT).FullName, typeof(T));
    }

    //创建对象通过传递的类型进行匹配
    public IT Resolve<IT>()
    {
        string key = typeof(IT).FullName;

        //获取要创建对象的类型
        Type type = this.TypeDictionary[key];

        //这里先不考虑有参构造函数的问题,后面会逐一的解决这些问题
        //通过反射完成对象的创建,这里我们先不考虑参数问题
        return (IT)Activator.CreateInstance(type);
    }
}

简单调用,现在是通过一个第三方的容器为我们创建对象,并且我们不用依赖于细节,通过接口的类型完成对象的创建,当要将 SqlserverDal 替换为MysqlDal 时,我们只需要在注册的时候将 SqlserverDal 替换为 MysqlDal 即可。

//实例化容器对象
IHTContainer container = new HTContainer();
//注册对象
container.RegisterType<IDatabase,SqlserverDal>();
//通过容器完成对象的创建,不体现细节,用抽象完成对象的创建
IDatabase dal = container.Resolve<IDatabase>();
dal.Connection("con");

3.2 升级容器(单构造函数)

上面将传统对象创建的方式,改为使用第三方容器来帮我们完成对象的创建。但这个容器考虑的还不是那么的全面,例如有参构造的问题,以及对象的依赖问题还没有考虑到。接下来继续完善这个容器,这里先不考虑多个构造函数的问题,这里先解决只有一个构造函数场景的参数问题。

3.2.1 一个参数

构造函数,单个参数情况。如下:

public class HTContainer : IHTContainer
{
    //创建一个Dictionary数据类型的对象用来存储注册的对象
    private Dictionary<string, Type> TypeDictionary = new Dictionary<string, Type>();

    //注册方法,用接口的FullName为key值,value为要创建对象的类型
    public void RegisterType<IT, T>()
    {
        this.TypeDictionary.Add(typeof(IT).FullName, typeof(T));
    }

    //创建对象通过传递的类型进行匹配
    public IT Resolve<IT>()
    {
        //获取要创建对象的类型
        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);   //创建对象并传递所依赖的对象
    }
}

3.2.2 多个参数

上面看了构造函数只有一个参数的问题,是通过构造函数的类型创建一个对象,并将这个对象作为参数传递到要实例化的对象中。那么多参数就需要创建多个参数的对象传递到要实例的对象中。如下:

public class HTContainer : IHTContainer
{
    //创建一个Dictionary数据类型的对象用来存储注册的对象
    private Dictionary<string, Type> TypeDictionary = new Dictionary<string, Type>();

    //注册方法,用接口的FullName为key值,value为要创建对象的类型
    public void RegisterType<IT, T>()
    {
        this.TypeDictionary.Add(typeof(IT).FullName, typeof(T));
    }

    //创建对象通过传递的类型进行匹配
    public IT Resolve<IT>()
    {
        //获取要创建对象的类型
        string key = typeof(IT).FullName;
        Type type = this.TypeDictionary[key];

        //这里先考虑只有一个构造函数的场景
        var ctor = type.GetConstructors()[0];

        //多个参数的形式,声明一个list来存储参数类型的对象
        List<object> paraList = new List<object>();

        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.2.3 循环依赖

通过上面的两步操作,已经能对构造函数中的参数初始化对象并传递到要实例的对象中,但这只是一个层级的。刚才做的只是解决了这么一个问题,假设要创建 A 对象,A 对象依赖于 B 对象。要做的就是创建了 B 对象作为参数传递给 A 并创建 A 对象,这只是一个层级的。当 B 对象又依赖于 C 对象,C 对象又依赖于 D 对象,这么一直循环下去。这样的场景该怎么解决呢?下面将通过递归的方式来解决这一问题。如下:

public class HTContainer : IHTContainer
{
    //创建一个Dictionary数据类型的对象用来存储注册的对象
    private Dictionary<string, Type> TypeDictionary = new Dictionary<string, Type>();

    //注册方法,用接口的FullName为key值,value为要创建对象的类型
    public void RegisterType<IT, T>()
    {
        this.TypeDictionary.Add(typeof(IT).FullName, typeof(T));
    }

    //创建对象通过传递的类型进行匹配
    public IT Resolve<IT>()
    {
        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());
    }
}

3.3 升级容器(多构造函数)

上面只是考虑了只有一个构造函数的问题,那初始化的对象有多个构造函数该如何处理呢,可以像 Autofac 那样选择一个参数最多的构造函数,也可以像 ServiceCollection 那样选择一个参数的超集来进行构造,当然也可以声明一个特性,那个构造函数中标记了这个特性,然后就采用那个构造函数。如下:

public class HTAttribute : Attribute
{
}
public class HTContainer : IHTContainer
{
    //创建一个Dictionary数据类型的对象用来存储注册的对象
    private Dictionary<string, Type> TypeDictionary = new Dictionary<string, Type>();

    //注册方法,用接口的FullName为key值,value为要创建对象的类型
    public void RegisterType<IT, T>()
    {
        this.TypeDictionary.Add(typeof(IT).FullName, typeof(T));
    }

    //创建对象通过传递的类型进行匹配
    public IT Resolve<IT>()
    {
        return (IT)this.ResolveObject(typeof(IT));
    }

    //通过递归的方式创建多层级的对象
    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<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());
    }
}

四、总结

通过上面的教程,可以自己写一个 IOC 容器了,也就可以看到 IOC 容器的本质了,其实用了大量的反射。实际生产使用,会有 Singleton、Transient、Scoped 三种生命周期,设计时需要结合 static、httpcontext 等进行实现(这个以后再进行讲解)。如果用现有的 autofac、Microsoft.Extensions.DependencyInjection 等则就不需要啦。

相关文章