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

依赖倒置原则、IoC容器和依赖注入:第三部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (37投票s)

2013年3月29日

CPOL

6分钟阅读

viewsIcon

85838

downloadIcon

975

如何创建自定义IoC容器以及如何使用它来遵守DIP

引言

这是我关于依赖倒置原则、IoC容器和依赖注入的文章的第三部分。在本文的上一部分,我试图解释什么是IoC容器。如果您还没有阅读过本文的前几部分,请通过以下链接阅读它们,以便更好地理解DIP、IoC和依赖注入概念的需求。

在本文中,我将解释如何创建自定义IoC容器,以及如何使用它来遵守依赖倒置原则。

背景

实现IoC容器有很多方法。在本文中,我将解释自定义IoC容器的开发,它将以与Microsoft Unity Container类似的方式工作。在本文的这一部分,我将解释基本实现,并在文章的后续部分添加更多功能,以便您可以了解Microsoft Unity Container的工作原理。

您应该具备反射的工作知识,才能理解本文中的代码。

自定义IoC容器的工作原理

下图显示了自定义IoC容器的工作原理。每个部分都用一条水平线分隔。下图描述的场景有三个部分

Click to enlarge image

高层模块

如果您还记得,我在本文第一部分中给出的复制示例充当了高层模块。DIP规定,高层模块不应依赖于低层模块的实现,而应公开低层模块应遵循的抽象。

每当需要低层模块实例时,它就会借助容器。上图中下面的语句返回低层模块的实例。

IReader object = customContainer.Resolve<IReader>();

高层模块不关心哪些类实现了IReader接口。而且,我们不需要将包含低层模块的项目引用添加到包含高层模块的项目中。由容器负责创建低层模块(依赖项)的实例并将其返回给高层模块。

自定义IoC容器

自定义IoC容器将向外部公开两个主要方法。它们是

  • Register<TypeToResolve, ResolvedType>()
  • Resolve<TypeToResolve>()

它维护一个字典,其中TypeToResolveResolvedType的组合将使用Register()方法以KeyValuePair的形式存储。

Resolve方法首先验证TypeToResolve是否已在字典中注册,如果已注册,则尝试使用反射创建其对应的ResolvedType的实例。

注意:自定义IoC容器可以有多种实现方式。这种实现是一种方式。

高层模块的消费者

基本上,这是高层模块用于执行操作的地方。它可以是Windows/Web/控制台应用程序。此应用程序应了解低层实现。

对于我们由高层模块提供的IReader抽象示例,有一个名为KeyBoardReader的实现类。消费者项目应具有指向包含低层模块实现的项目的引用。因此,当低层实现有任何添加时,消费者可能会改变(取决于消费者的实现),但高层模块不会改变,因为它没有低层模块的引用,也不知道IReader抽象的实现者是谁。

编码自定义IoC容器

步骤1:在Visual Studio中创建一个空白解决方案,并创建以下项目

  • DIP.Abstractions(类库项目)
    • IReader.cs
    • IWriter.cs
  • DIP.HighLevelModule(类库项目)
    • Copy.cs
  • DIP.MyIoCContainer(类库项目)
    • Container.cs
  • DIP.Implementation(类库项目)
    • KeyboardReader.cs
    • PrinterWriter.cs
  • DIP.Consumer(控制台应用程序)
    • Program.cs

在此,需要注意的重要事项是项目引用(项目之间的依赖关系)。

  • DIP.Abstractions项目没有任何项目引用,因为它独立于所有内容。
  • DIP.HighLevelModule项目有两个引用
    • 引用DIP.Abstractions:因为它使用抽象而不依赖于实现
    • 引用DIP.MyIoCContainer:因为它将使用容器来解析依赖项
  • DIP.Implementations具有对DIP.Abstractions的引用,因为它将实现抽象
  • DIP.MyIoCContainer没有任何项目引用,因为它是一个库,不依赖于任何项目
  • DIP.Consumer具有对以下项目的引用
    • DIP.AbstractionsDIP.Implementation:因为它需要进行DI注册。
    • DIP.MyIocContainer:它将用作容器,该容器将传递给DIP.HighLevelModule以解析依赖项
    • DIP.HighLevelModule:它将使用DIP.HighLevelModule来执行操作

注意:在上述项目依赖关系中,您可以注意到DIP.HighLevelModuleDIP.Implementation之间没有依赖关系。因此,在DIP.implementation中添加或删除类不会改变DIP.HighLevelModule中的任何内容。

步骤2:将以下代码添加到Container.cs(DIP.MyIoCContainer项目)

    public class Container
    {
        private Dictionary<Type,Type> iocMap = new Dictionary<Type,Type>();
 
        public void Register<TypeToResolve,ResolvedType>()
        {
            if (iocMap.ContainsKey(typeof(TypeToResolve)))
            {
                throw new Exception(string.Format
                ("Type {0} already registered.", typeof(TypeToResolve).FullName));
            }
            iocMap.Add(typeof(TypeToResolve), typeof(ResolvedType));
        }
 
        public T Resolve<T>()
        {
            return (T)Resolve(typeof(T));
        }
 
        public object Resolve(Type typeToResolve)
        {
            // Find the registered type for typeToResolve
            if (!iocMap.ContainsKey(typeToResolve))
                throw new Exception(string.Format("Can't resolve {0}. 
                Type is not registered.", typeToResolve.FullName));
 
            Type resolvedType = iocMap[typeToResolve];
 
            // Try to construct the object
            // Step-1: find the constructor 
            // (ideally first constructor if multiple constructors present for the type)
            ConstructorInfo ctorInfo = resolvedType.GetConstructors().First();
 
            // Step-2: find the parameters for the constructor and try to resolve those
            List<parameterinfo> paramsInfo = ctorInfo.GetParameters().ToList();
            List<object> resolvedParams = new List<object>();
            foreach (ParameterInfo param in paramsInfo)
            {
                Type t = param.ParameterType;
                object res = Resolve(t);
                resolvedParams.Add(res);
            }
 
            // Step-3: using reflection invoke constructor to create the object
            object retObject = ctorInfo.Invoke(resolvedParams.ToArray());
 
            return retObject;
        }
    }
  • iocMap

一个Dictionary<Type,Type>成员,它将保存已注册的TypeToResolve及其对应的ResolvedType类型的列表。

  • Register

用于获取TypeToResolve类型的实例的方法
语法:void Register<TypeToResolve, ResolvedType>();

  • 解析

用于获取TypeToResolve类型的实例的方法
语法:<TypeToResolve> Resolve<TypeToResolve>();

步骤3:构建DIP.Abstractions项目

该项目包含HighLevelModule用于执行操作的抽象(接口)。在我们的例子中,我们有两个抽象

IReader.cs

    public interface IReader
    {
        string Read();
    }

IWriter.cs

    public interface IWriter
    {
        void Write(string data);
    }

步骤4:开发DIP.HighLevelModule

该模块将使用抽象来执行操作。在我们的例子中,我们有一个Copy.cs,它将从IReader复制到IWriter

    public class Copy
    {
        private Container _container;
        private IReader _reader;
        private IWriter _writer;
 
        public Copy(Container container)
        {
            _container = container;
            _reader = _container.Resolve<IReader>();
            _writer = _container.Resolve<IWriter>();
        }
 
        public void DoCopy()
        {
            string stData = _reader.Read();
            _writer.Write(stData);
        }

步骤5:实现抽象(DIP.Implementation)

该项目包含DIP.Abstractions中定义的抽象的实现。一个抽象可以有多个实现。例如,我们可以有实现自IReader接口的KeyboardReaderFileReader等。

为了使类易于理解,我定义了两个类,KeyboardReader(实现IReader)和PrinterWriter(实现IWriter)。

注意:这并没有实现键盘的实际读取。我将其保持简单,仅用于演示IoC容器的用法,而不是如何从键盘读取。

KeyboardReader.cs

    public class KeyboardReader:IReader
    {
        public string Read()
        {
            return "Reading from \"Keyboard\"";
        }
    }

PrinterWriter.cs

    public class PrinterWriter:IWriter
    {
        public void Write(string data)
        {
            Console.WriteLine(string.Format("Writing to \"Printer\": [{0}]", data));
        }
    }

步骤6:最后开发将实际使用HighLevelModule的Consumer类(DIP.Consumer)

这是一个与用户交互的控制台应用程序。

Program.cs

    class Program
    {
        static void Main(string[] args)
        {
            Container container = new Container();
            DIRegistration(container);
            Copy copy = new Copy(container);

            copy.DoCopy();
            Console.Read();
        }

        static void DIRegistration(Container container)
        {
            container.Register<IReader,KeyboardReader>();
            container.Register<IWriter,PrinterWriter>();
        }
    }

您会注意到它还包含一个名为DIRegistration()的方法。它基本上执行依赖项的注册。语句container.Register<IReader, KeyboardReader>()注册了IReaderKeyboardReader的映射,因此每当需要实现IReader对象时,都会返回KeyboardReader的实例。

注意:有许多方法可以注册依赖项。我们也可以通过配置文件来实现DI注册。

运行应用程序

如果您运行开发的应用程序,您将获得以下输出

在这里,您可以扩展实现而不更改高层程序。您需要做的唯一一件事就是更改注册。

摘要

在本文的这一部分,我试图解释如何开发一个简单的IoC容器。希望您觉得这个主题很好且易于理解。在下一章中,我将介绍一些高级技术,使自定义IoC容器更加实用和有用。

历史

  • 2020年4月23日:第二次修订(更新了最终文章的链接)
  • 2013年3月29日:第一次修订
© . All rights reserved.