手写IOC实践


一、IOC

1.什么是IOC?

控制反转(英语:Inversion of Control,缩写为IoC),是[面向对象编程]中的一种设计原则,可以用来减低计算机代码之间的[耦合度]其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup).

IoC:是一种设计模式

DI:是践行控制反转思想的一种方式

2.为什么要用IOC

因为IoC 控制反转是依赖抽象,而抽象是稳定的,不依赖细节,因为细节还可能会依赖其他细节,为了屏蔽细节,需要使用依赖注入去解决无限层级的对象依赖。

3.Net中常用的IoC容器

目前用的最多的是AutoFac和Castle,在.Net Core中框架内置了IOC容器,Unity和ObjectBuilder是相对比较久远的框架,用的比较少。

  1. AutoFac

  2. Castle

  3. Unity

  4. ObjectBuilder

二、如何手写实现?

1.基本设计

核心思想: 工厂 + 反射

首先我们想自己实现一个IoC容器其实并不难,我们在使用现有的IoC容器都知道,在使用前,我们需要先注册,然后才能使用

所以我们将工厂换成手动注册的方式,因为写一大堆if else 或者switch也不太美观,根据主流IoC的使用方式来以葫芦画瓢,如果后期继续完善功能加入程序集注入的话,还是得实现一个工厂,来省略手动注册

但是这次目标是实现一个简易版的IoC容器,我们先实现基础功能,待后面一步一步去完善,再加入一些新的功能,即我们不考虑性能或者扩展度,目的是循序渐进,在写之前我们先整理出 实现步骤和实现方式

  1. 方便接入和扩展,我们在这先定义一个容器接口 IManualContainer

  2. 定义ManualContainer继承实现IManualContainer

  3. 声明一个静态字典对象存储注册的对象

  4. 利用反射构建对象,考虑到性能可以加入Expression或者Emit的方式来做一些优化

classDiagram IManualContainer <|-- ManualContainer IManualContainer: +Register() IManualContainer: +Resolve() class ManualContainer{ -Dictionary container +Register() +Resolve() -CreateInstance() }
public interface IManualContainer
{
      void Register(string servicesName = null) where To : TFrom;

      Tinterface Resolve(string servicesName = null);
}
2.要实现的功能

1.基本对象构造

2.构造函数注入

3.多级依赖和多构造函数及自定义注入

4.属性注入&方法注入

5.单接口多实现

三、编码实现及思路剖析

1.实现构造对象(单接口注入)

1.首先实现接口来进行编码私有字段 container用来存储注册的类型,key是对应接口的完整名称,Value是需要Resolve的类型。

2.泛型约束保证需要被Resolve类型 (To) 实现或者继承自注册类型 (TFrom)

public class ManualContainer : IManualContainer
{
   //存储注册类型
   private static Dictionary container = 
   new Dictionary();
   
   //注册
    public void Register(string servicesName = null) where To : TFrom
    {
        string Key = $"{typeof(TFrom).FullName}{servicesName}";
        if (!container.ContainsKey(Key))
        {
            container.Add(Key, typeof(To));
        }
    }

1.实现构造对象,首先需要传入被构造的类型的抽象接口T

2.在Resolve中根据T作为Key,在存储容器中找到注册时映射的类型,并通过反射构造对象

   //构建对象
   public TFrom Resolve(string servicesName = null)
   {
       string Key = $"{typeof(TFrom).FullName}{servicesName}";
       container.TryGetValue(key, out Type target);
       if(target is null)
       {
           return default(TFrom);
       }
       object t = Activator.CreateInstance(target);
   }
}

1.首先我们准备需要的接口(ITestA)和实例(TestA)来利用容器来构造对象

public interface ITestA
{
    void Run();
}
public class TestA : ITestA
{
   public void Run()=> Console.WriteLine("这是接口ITestA的实现");
}

2.调用IoC容器来创建对象

IManualContainer container = new ManualContainer();
//注册到容器中
container.Register();
ITestA instance = container.Resolve();
instance.Run();
//out put "这是接口ITestA的实现"
2.构造函数注入

1.假设我们的TestA类中需要ITestB接口的实例或者其他更多类型的实例,并且需要通过构造函数注入,我们应该如何去完善我们的IoC容器呢?

public class TestA : ITestA
{ 
   private ITestB testB = null;
   //构造函数
   public TestA(ITestB testB)=> this.testB = testB;
   
   public void Run()
   {
      this.testB.Run();
      Console.WriteLine("这是接口ITestA的实现");
   }
}

2.我们按照上面的步骤照常注册和构造对象,发现报错了,在Resolve()的时候,经过调试知道是使用反射构造的时候报错了,因为在构造TestA缺少构造参数,那么我们就需要在反射构造时加入参数。

  1. 先定义List集合存储对象构造时需要的参数列表

  2. 通过需要被实例的目标类型找到类中的构造函数,暂不考虑多构造函数case

  3. 找到构造函数参数及类型,然后创建参数的实例加入List中,在反射构造时传入参数就解决了

  4.    //完善Resolve构建对象函数
       public TFrom Resolve(string servicesName = null)
       {
           string Key = $"{typeof(TFrom).FullName}{servicesName}";
           container.TryGetValue(key, out Type target);
           if(target is null)
           {
               return default(TFrom);
           }
           
           //存储参数列表
           List paramList = new List();
           //找到目标类型的构造函数,暂不考虑多构造函数case
           var ctor = target.GetConstructors().FirstOrDefault();
           //找到参数列表
           var ctorParams = ctor.GetParameters();
           foreach (var item in ctorParams)
           {
               //参数类型
               Type paramType = item.ParameterType;
               string paramKey = paramType.FullName;
               //找到参数注册时映射的实例
               container.TryGetValue(paramKey, out Type ParamType);
               //构造出实例然后加入参数列表
               paramList.Add(Activator.CreateInstance(ParamType));
           }     
           object t = Activator.CreateInstance(target,paramList);
       }
    }
    
    
    3.多级依赖(递归)

    根据上面我们目前实现的结果来看,这是解决了构造函数和多参数注入以及基本的构造对象问题,那现在问题又来了

    1. 如果是很多层的依赖该怎么办?

    2. 例如多个构造函数怎么办呢?

    3. 在多个构造函数中用户想自定义需要被注入的构造函数怎么办?

    总结3点问题

    • 1.多级依赖问题

    例如ITestB 的实例中依赖ITestC,一直无限依赖我们怎么解决呢?毫无疑问,做同样的事情,但是要无限做下去,就使用 递归,下面我们来改造我们的方法

    • 2.多个构造函数

      1. 取参数最多的方式注入 (AutoFac)

      2. 取并集方式注入 (ASP .NET Core)

    • 3.自定义注入

      我们可以使用特性标记的方式来实现,用户在需要被选择注入的构造函数上加入特性标签来完成


    1.创建私有递归方法,这个方法的作用就是创建对象用

    private object CreateInstance(Type type,string serviceName = null)
    {
    }
    

    2.我们选择第一种方式实现,修改之前获取第一个构造函数的代码,选择最多参数注入

     //找到目标类型的构造函数,找参数最多的构造函数
      ConstructorInfo ctor = null;
      var ctors = target.GetConstructors();
      ctor = ctors.OrderByDescending(x => x.GetParameters().Length).First();
    

    3.自定义特性CtorInjection,可以使用户自定义选择

     //自定义构造函数注入标记
     [AttributeUsage(AttributeTargets.Constructor)]
     public class CtorInjectionAttribute : Attribute
     {
     }
    

    4.最终代码

    private object CreateInstance(Type type,string serviceName = null)
    {
       string key = $"{ type.FullName }{serviceName}";
       container.TryGetValue(key, out Type target);
       //存储参数列表
       List paramList = new List();
      
       ConstructorInfo ctor = null;
       //找到被特性标记的构造函数作为注入目标
       ctor = target.GetConstructors().FirstOrDefault(x => x.IsDefined(typeof(CtorInjectionAttribute), true));
    
       //如果没有被特性标记,那就取构造函数参数最多的作为注入目标
       if (ctor is null)
       {
          ctor = target.GetConstructors().OrderByDescending(x => x.GetParameters().Length).First();
       }
       //找到参数列表
       var ctorParams = ctor.GetParameters();
       foreach (var item in ctorParams)
       {
           //参数类型
           Type paramType = item.ParameterType;
           //递归调用构建对象
           object paramInstance = CreateInstance(paramType);
           //构造出实例然后加入参数列表
           paramList.Add(paramInstance);
       }    
       object t = Activator.CreateInstance(target);
       return t;
    }
    
     public TFrom Resolve() 
     {
        return (TFrom)this.CreateInstance(typeof(TFrom));
     }
    
    
    4.属性注入&方法注入

    1.自定义特性PropInjection,可以使用户自定义选择

     //自定义构造属性注入标记
     [AttributeUsage(AttributeTargets.Property)]
     public class PropInjectionAttribute : Attribute
     {
     }
      //自定义构造方法注入标记
     [AttributeUsage(AttributeTargets.Method)]
     public class MethodInjectionAttribute : Attribute
     {
     }
    

    2.遍历实例中被标记特性的属性。

    3.获取属性的类型,调用递归构造对象函数。

    4.设置目标对象的属性值。

    5.方法注入也是同理,换汤不换药而已

    private object CreateInstance(Type type,string serviceName = null)
    {
      string key = $"{ type.FullName }{serviceName}";
       container.TryGetValue(key, out Type target);
       //存储参数列表
       List paramList = new List();
      
       ConstructorInfo ctor = null;
       //找到被特性标记的构造函数作为注入目标
       ctor = target.GetConstructors().FirstOrDefault(x => x.IsDefined(typeof(CtorInjectionAttribute), true));
    
       //如果没有被特性标记,那就取构造函数参数最多的作为注入目标
       if (ctor is null)
       {
          ctor = target.GetConstructors().OrderByDescending(x => x.GetParameters().Length).First();
       }
       //找到参数列表
       var ctorParams = ctor.GetParameters();
       foreach (var item in ctorParams)
       {
           //参数类型
           Type paramType = item.ParameterType;
           //递归调用构建对象
           object paramInstance = CreateInstance(paramType);
           //构造出实例然后加入参数列表
           paramList.Add(paramInstance);
       }    
       object t = Activator.CreateInstance(target);
       
       //获取目标类型的被特性标记的属性<属性注入>
       var propetys = target.GetProperties().Where(x => x.IsDefined(typeof(PropInjectionAttribute), true));
       foreach (var item in propetys) 
       {
           //获取属性类型
           Type propType = item.PropertyType;
           object obj = this.CreateInstance(propType);
           //设置值
           item.SetValue(t, obj);
       }
       
       //获取目标类型的被特性标记的方法<方法注入>
       var methods = target.GetMethods().Where(x => x.IsDefined(typeof(MethodInjectionAttribute), true)).ToList();
       foreach (var item in methods)
        {
            List methodParams = new List();
            foreach (var p in item.GetParameters())
            { 
                //获取方法参数类型
                Type propType = p.ParameterType;
                object obj = this.CreateInstance(propType);
                methodParams.Add(obj);
            }
            item.Invoke(t, methodParams);
        }
       return t;
    }
    
     public TFrom Resolve() 
     {
        return (TFrom)this.CreateInstance(typeof(TFrom));
     }
    
    
    5.单接口多实现

    假设我们一个接口被多个实例实现,我们需要注入怎么操作呢?

    毫无疑问我们应该想到在注入时取一个别名,没错正是这种方法,但是也会存在一个问题,我们取了别名之后只能解决注入的场景?那依赖接口的地方如何知道注入哪一个实例呢?

    1.注册单接口多实例

    container.Register("test1");
    container.Register("test2");
    ITest instance = container.Resolve("test1");
    ITest instance1 = container.Resolve("test2");
    

    2.创建标记特性,用来标记目标对象

    [AttributeUsage(AttributeTargets.Parameter| AttributeTargets.Property)]
    public class ParamterInjectAttribute : Attribute
    {
       public ParamterInjectAttribute(string nickName) => NickName = nickName;
       public string NickName { get; private set; }
    }
    

    3.我们需要在依赖“单接口多实例的类中”使用时告诉参数,我们需要的实例,依然使用参数特性标记

    public class use : IUse
    {
        private ITest _Test = null;
        //告诉构造函数依赖ITest接口时使用别名为test1的实例
        public BLL1([ParamterInjectAttribute("test1")] ITest接口时使用别名为test1的实例 Test)
        {
            this._Test = Test;
        }
    }
    

    4.在构造对象时查找是否被特性标记,然后构造对象

    foreach (var item in ctor.GetParameters())
    {
        Type paramType = item.ParameterType;
        string nickName = string.Empty;
        //查找构造函数的参数是否被特性标记
        if (item.IsDefined(typeof(ParamterInjectAttribute), true))
        {
        //找到被标记需要构造的实例别名
            nickName = item.GetCustomAttribute().NickName;
        }
        //根据别名创建对象
        object instance = CreateInstance(paramType,nickName);
        if (instance != null)
        {
            paramList.Add(instance);
        }
     }
    

    四、总结

    目前还不是很完善,只是实现了属性,方法,以及构造函数注入,很多必要功能还没有,下一步将在现有代码基础上利用Emit的方式来创建对象,加入基本的验证环节以提高健壮性,加入生命周期管理,和AOP扩展。

    第一版最终代码

     public class ManualContainer : IManualContainer
     {
            private static Dictionary container = new Dictionary();
            public void Register(string servicesName = null) where To : TFrom
            {
                string Key = $"{typeof(TFrom).FullName}{servicesName}";
                if (!container.ContainsKey(Key))
                {
                    container.Add(Key, typeof(To));
                }
            }
    
            public Tinterface Resolve(string serviceName = null)
            {
                return (Tinterface)this.CreateInstance(typeof(Tinterface), serviceName);
            }
    
            private object CreateInstance(Type type, string serviceName = null)
            {
                string key = $"{ type.FullName }{serviceName}";
                container.TryGetValue(key, out Type target);
                object instance = ctorInjection(target);
                propInjection(target, instance);
                methodInjection(target, instance);
                return instance;
            }
    
    
            /// 
            /// 构造函数注入
            /// 
            /// 需要被创建的类型
            /// 被创建的实例
            private object ctorInjection(Type targetType)
            {
                List paramList = new List();
                ConstructorInfo ctor = null;
                ctor = targetType.GetConstructors().FirstOrDefault(x => x.IsDefined(typeof(CtorInjectionAttribute), true));
                if (ctor is null)
                {
                    ctor = targetType.GetConstructors().OrderByDescending(x => x.GetParameters().Length).First();
                }
                foreach (var item in ctor.GetParameters())
                {
                    Type paramType = item.ParameterType;
                    string nickName = GetNickNameByAttribute(item);
                    object instance = CreateInstance(paramType, nickName);
                    if (instance != null)
                    {
                        paramList.Add(instance);
                    }
                }
                object t = Activator.CreateInstance(targetType, paramList.ToArray());
                return t;
            }
    
    
            /// 
            /// 属性注入
            /// 
            /// 需要被创建的类型
            /// 根据需要被创建的类型构造出的实例
            private void propInjection(Type targetType, object inststance)
            {
                var propetys = targetType.GetProperties().Where(x => x.IsDefined(typeof(PropInjectionAttribute), true));
                foreach (var item in propetys)
                {
                    Type propType = item.PropertyType;
                    string nickName = GetNickNameByAttribute(item);
                    object obj = this.CreateInstance(propType, nickName);
                    item.SetValue(inststance, obj);
                }
            }
    
            /// 
            /// 方法注入
            /// 
            /// 需要被创建的类型
            /// 根据需要被创建的类型构造出的实例
            private void methodInjection(Type targetType, object inststance)
            {
                List methodParams = new List();
                var methods = targetType.GetMethods().Where(x => x.IsDefined(typeof(MethodInjectionAttribute), true)).ToList();
                foreach (var item in methods)
                {
                    foreach (var p in item.GetParameters())
                    {
                        Type propType = p.ParameterType;
                        string nickName = GetNickNameByAttribute(item);
                        object obj = this.CreateInstance(propType, nickName);
                        methodParams.Add(obj);
                    }
                    item.Invoke(inststance, methodParams.ToArray());
                }
            }
            
            private string GetNickNameByAttribute(ICustomAttributeProvider provider)
            {
                if (provider.IsDefined(typeof(ParamterInjectAttribute), true))
                {
                    ParamterInjectAttribute attribute = provider.GetCustomAttributes(typeof(ParamterInjectAttribute), true)[0] as ParamterInjectAttribute;
                    return attribute.NickName;
                }
                return string.Empty;
            }
      }