65.9K
CodeProject 正在变化。 阅读更多。
Home

让我们编写一个微型 IoC 容器来学习(和娱乐)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (15投票s)

2016年1月29日

CPOL

7分钟阅读

viewsIcon

47619

downloadIcon

375

本文包含一个简小的 IoC 容器实现,仅用于教学目的。

引言

本文包含一个简小的 IoC 容器实现,仅用于教学目的。

背景

前几天,我和一位朋友讨论了服务定位器和 IoC 容器之间的关系以及依赖注入的最佳实践。这又引发了我与一些初级开发人员关于 IoC 容器内部工作原理的讨论。这给了我一个想法,即编写一个小型 IoC 容器可能是一个很好的练习,可以向这些人解释 IoC 容器是如何在内部工作的。

因此,本文简要介绍了一个我花了一个小时创建的小型 IoC 容器,用于向一些开发人员讲授 IoC 容器的基础知识以及如何实现一个。我将它放在网上,希望能让一些人受益。

什么是 IoC 容器

为了开始讨论,我们先来理解什么是 IoC 容器。IoC 容器是一个组件,它允许我们将具体的类依赖与我们的契约(即接口)进行注册,以便对于任何给定的接口,都会实例化注册的具体类。这使得高层模块可以指定自己的具体类,注册它们,并将它们注入到应用程序中以供任何给定的接口使用。

上面的解释只是 IoC 容器的依赖注入部分。IoC 容器还可以管理对象的生命周期。存在许多功能齐全的 IoC 容器,它们为所有控制反转和对象生命周期管理需求提供了全面的解决方案。本文中的代码绝不应用于生产应用程序。它只是一个简单的练习,用于理解和一窥 IoC 容器的内部工作原理。

Using the Code

让我们开始看看我们容器库提供的函数集,也就是容器中已实现的功能。

  1. RegisterInstanceType:将接口与具体类型注册,对于每个 Resolve 请求,都将返回一个新实例。
  2. RegisterSingletonType:将接口与具体类型注册,对于所有 Resolve 请求,都将返回一个单例实例。
  3. Resolve:解析接口并检索给定接口已配置的具体类型。

除此之外,容器还公开了一个名为 TinyDependencyAttribute 的属性,用于处理嵌套的依赖注入。

为了处理嵌套依赖,该容器支持构造函数注入。自定义属性 TinyDependencyAttribute 应用于需要注入其他依赖的构造函数。我们的容器将使用此属性来注入已注册的依赖项到
给定类型的构造函数中,即用我们的自定义属性装饰的构造函数。

现在,让我们简要看一下代码中的各个组件。

  • IContainer - 包含我们容器所有方法的接口。
  • Container - 实现 IContainer 接口的具体类,封装了注册和实例解析的内部工作原理。
  • RegistrationModel - 一个简单的模型,用于保存有关被注册类型及其生命周期的信息。
  • InstanceCreationService - 一个负责从给定类型创建实例的服务。此类还负责嵌套依赖及其注入。
  • SingletonCreationService - 一个服务,用于跟踪单例实例并在 Resolve 请求时创建单例实例。

现在有了这些解释,我们就可以看到 IoC 容器功能是如何实现的了。

让我们从查看 IContainer 接口开始。

public interface IContainer
{
    void RegisterInstanceType< I, C >()
        where I : class
        where C : class;

    void RegisterSingletonType< I, C >()
        where I : class
        where C : class;

    T Resolve< T >();
}

因此,这三个方法是我们将在容器中公开的。应用程序的用户可以使用 RegisterInstanceType 来注册接口的常规实例类型依赖,并使用 RegisterSingletonType 来注册单例对象依赖。

现在让我们看看封装了这些功能的 Container 类的实现。

public class Container : IContainer
{
    Dictionary< type, registrationmodel >  instanceRegistry = new Dictionary< type, registrationmodel >();
        
    public void RegisterInstanceType< I, C >()
        where I : class
        where C : class
    {
        RegisterType< I, C >(REG_TYPE.INSTANCE);
    }

    public void RegisterSingletonType< I, C >()
        where I : class
        where C : class
    {
        RegisterType< I, C >(REG_TYPE.SINGLETON);
    }

    private void RegisterType< I, C >(REG_TYPE type)
    {
        if (instanceRegistry.ContainsKey(typeof(I)) == true)
        {
            instanceRegistry.Remove(typeof(I));
        }

        instanceRegistry.Add(
            typeof(I),
                new RegistrationModel
                {
                    RegType = type,
                    ObjectType = typeof(C)
                }
            );
    }

    public I Resolve< I >()
    {
        return (I)Resolve(typeof(I));            
    }

    private object Resolve(Type t)
    {
        object obj = null;

        if (instanceRegistry.ContainsKey(t) == true)
        {
            RegistrationModel model = instanceRegistry[t];

            if (model != null)
            {
                Type typeToCreate = model.ObjectType;
                ConstructorInfo[] consInfo = typeToCreate.GetConstructors();

                var dependentCtor = consInfo.FirstOrDefault(item => item.CustomAttributes.FirstOrDefault(att => att.AttributeType == typeof(TinyDependencyAttribute)) != null);
                
                if(dependentCtor == null)
                {
                    // use the default constructor to create
                    obj = CreateInstance(model);
                }
                else
                {
                    // We found a constructor with dependency attribute
                    ParameterInfo[] parameters = dependentCtor.GetParameters();

                    if (parameters.Count() == 0)
                    {
                        // Futile dependency attribute, use the default constructor only
                        obj = CreateInstance(model);
                    }
                    else
                    {
                        // valid dependency attribute, lets create the dependencies first and pass them in constructor
                        List< object > arguments = new List< object >();

                        foreach (var param in parameters)
                        {
                            Type type = param.ParameterType;
                            arguments.Add(this.Resolve(type));
                        }

                        obj = CreateInstance(model, arguments.ToArray());
                    }
                }
            }
        }

        return obj;
    }

    private object CreateInstance(RegistrationModel model, object[] arguments = null)
    {
        object returnedObj = null;
        Type typeToCreate = model.ObjectType;

        if (model.RegType == REG_TYPE.INSTANCE)
        {
            returnedObj = InstanceCreationService.GetInstance().GetNewObject(typeToCreate, arguments);
        }
        else if (model.RegType == REG_TYPE.SINGLETON)
        {
            returnedObj = SingletonCreationService.GetInstance().GetSingleton(typeToCreate, arguments);
        }

        return returnedObj;
    }
}

此类的工作原理是在字典中跟踪所有接口类型及其具体实现类型。Register 和 resolve 方法将分别注册依赖项并返回注册类型的实例。RegistrationModel 对象用于跟踪具体对象类型和请求的生命周期。该模型如下所示。

internal enum REG_TYPE
{
    INSTANCE,
    SINGLETON
};

internal class RegistrationModel
{
    internal Type ObjectType { get; set; }
    internal REG_TYPE RegType { get; set; }
}

第二件需要注意的是 resolve 方法。resolve 方法会查看为接口注册的具体类型,然后检查我们的自定义属性 TinyDependencyAttribute 是否存在于任何构造函数上。如果此属性存在,则表示存在嵌套依赖,因此我们需要创建并传入依赖对象到构造函数中。如果没有构造函数包含此属性,我们将简单地使用默认构造函数来创建已注册具体类型的实例。

resolve 方法使用另外两个类来从给定类型实际实例化对象。SingletonCreationService 将管理单例对象,并将注册的实例返回给调用者。如果给定对象的实例已经存在,它将返回相同的实例。否则,它将创建一个实例然后返回。此外,它还会保存该实例以备下次调用此单例对象的 resolve。

internal class SingletonCreationService
{
    static SingletonCreationService instance = null;
    static Dictionary< string, object > objectPool = new Dictionary< string, object >());

    static SingletonCreationService()
    {
        instance = new SingletonCreationService();
    }

    private SingletonCreationService()
    { }

    public static SingletonCreationService GetInstance()
    {
        return instance;
    }

    public object GetSingleton(Type t, object[] arguments = null)
    {
        object obj = null;

        try
        {
            if (objectPool.ContainsKey(t.Name) == false)
            {
                obj = InstanceCreationService.GetInstance().GetNewObject(t, arguments);
                objectPool.Add(t.Name, obj);
            }
            else
            {
                obj = objectPool[t.Name];
            }
        }
        catch
        {
            // log it maybe
        }

        return obj;
    }
}

InstanceCreationService 始终为每次 resolve 调用返回一个新对象。

internal class InstanceCreationService
{
    static InstanceCreationService instance = null;

    static InstanceCreationService()
    {
        instance = new InstanceCreationService();
    }

    private InstanceCreationService()
    { }

    public static InstanceCreationService GetInstance()
    {
        return instance;
    }

    public object GetNewObject(Type t, object[] arguments = null)
    {
        object obj = null;

        try
        {
            obj = Activator.CreateInstance(t, arguments);
        }
        catch
        {
            // log it maybe
        }

        return obj;
    }
}

现在我们已经看到了容器库中涉及的所有类,让我们看看它们将如何协调工作。

  1. 调用者将在容器上调用 Register 函数。
  2. 容器将根据注册方法的类型(即实例或单例)将接口和具体类类型存储为依赖项。
  3. 当调用者调用 resolve 时,容器将使用 InstanceCreationService 来创建已注册实例类型的对象并将其返回给用户。
  4. 当调用者调用 resolve 时,如果注册类型是单例,容器将使用 SingeltonCreationService 来创建对象或返回已存在的注册类型对象。

现在我们已经了解了 IoC 容器库的内部结构,让我们看看如何测试该容器。

让我们测试一下容器

为了测试容器,让我们创建一些虚拟接口和一些具体类。让我们从简单的依赖项开始,并使用它们来测试我们的注册方法。

interface ITest1
{
    void Print();
}

class ClassTest1 : ITest1
{
    public void Print()
    {
        Console.WriteLine("ClassName: {0}, HashCode: {1}", this.GetType().Name, this.GetHashCode());
    }
}

interface ITest2
{
    void Print();
}

class ClassTest2 : ITest2
{
    public void Print()
    {
        Console.WriteLine("ClassName: {0}, HashCode: {1}", this.GetType().Name, this.GetHashCode());
    }
}

让我们测试上面接口和类的注册和解析功能。

IContainer container = new yaTinyIoCContainer.Container();

// testing instance type resigtration for class
container.RegisterInstanceType< itest1, classtest1 >();
ITest1 obj1 = container.Resolve< itest1 >();
obj1.Print();


// testing singleton registration for class
container.RegisterSingletonType< itest2, classtest2 >();
ITest2 obj5 = container.Resolve< itest2 >();
obj5.Print();

为了测试嵌套依赖,让我们创建一些类,这些类需要在它们中注入其他接口依赖。

interface One
{
    void FunctionOne();
}

interface Two
{
    void FunctionTwo();
}

class ClassOne : One
{
    ITest1 m_Itest1 = null;
    
    [TinyDependency]
    public ClassOne(ITest1 test1)
    {
        m_Itest1 = test1;
    }
    
    public void FunctionOne()
    {
        Console.WriteLine("ClassName: {0}, HashCode: {1}", this.GetType().Name, this.GetHashCode());
        m_Itest1.Print();
    }
}

class ClassTwo : Two
{
    One m_One = null;
    ITest1 m_Itest1 = null;
        
    [TinyDependency]
    public ClassTwo(ITest1 test1, One one)
    {
        m_Itest1 = test1;
        m_One = one;
    }
    
    public void FunctionTwo()
    {
        Console.WriteLine("ClassName: {0}, HashCode: {1}", this.GetType().Name, this.GetHashCode());
        m_Itest1.Print();
        m_One.FunctionOne();
    }
}

请注意上面类构造函数中使用了 TinyDependencyAttribute 。现在,为了测试这些,让我们注册它们并尝试解析它们。

 // testing nested dependency for 2 levels
container.RegisterInstanceType< one, classone >();
One obj9 = container.Resolve< one >();
obj9.FunctionOne();

// testing nested dependency for 2 levels with 2 arguments
container.RegisterInstanceType< two, classtwo >();
Two obj10 = container.Resolve< two >();
obj10.FunctionTwo();

现在,当我们运行应用程序时,我们可以看到所有依赖项都已解析到它们注册的类。

在结束讨论之前,这里有一些重要提示,在查看源代码之前可能会有所帮助。

  • 所有注册都是通过代码完成的。此代码可以增强以从配置文件读取依赖项,但这不属于此应用程序的范围。
  • 此容器能够注入嵌套依赖项,前提是所有依赖项都在 Resolve 调用之前注册。我最多测试了 3 级嵌套依赖项,但理论上它应该可以处理 N 级。
  • 此应用程序的测试是一个控制台应用程序,其中包含许多接口和类,所有依赖项都在 Main 函数中注册和解析。

关注点

这个小应用程序是经过一个小时编码的成果。这个应用程序的主要思想是演示 IoC 容器是如何工作的。它被写成一个教学/学习的练习,因此代码标准和最佳实践未达到预期。代码以文章的形式发布,只是为了让其他人(主要是初学者)能够获取,以便他们也能一窥 IoC 容器是如何工作的。 

历史

  • 2016 年 2 月 1 日 - 引入了 Attribute 以更好地处理嵌套依赖。
  • 2016 年 1 月 29 日 - 第一个版本
© . All rights reserved.