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






4.88/5 (37投票s)
如何创建自定义IoC容器以及如何使用它来遵守DIP
引言
这是我关于依赖倒置原则、IoC容器和依赖注入的文章的第三部分。在本文的上一部分,我试图解释什么是IoC容器。如果您还没有阅读过本文的前几部分,请通过以下链接阅读它们,以便更好地理解DIP、IoC和依赖注入概念的需求。
- 第一部分:依赖倒置原则
- 第二部分:控制反转和IoC容器
- 第三部分:自定义IoC容器(当前阅读)
- 第四部分:带生命周期选项的自定义IoC容器
- 第五部分:使用Microsoft Unity进行依赖注入(DI)
在本文中,我将解释如何创建自定义IoC容器,以及如何使用它来遵守依赖倒置原则。
背景
实现IoC容器有很多方法。在本文中,我将解释自定义IoC容器的开发,它将以与Microsoft Unity Container类似的方式工作。在本文的这一部分,我将解释基本实现,并在文章的后续部分添加更多功能,以便您可以了解Microsoft Unity Container的工作原理。
您应该具备反射的工作知识,才能理解本文中的代码。
自定义IoC容器的工作原理
下图显示了自定义IoC容器的工作原理。每个部分都用一条水平线分隔。下图描述的场景有三个部分
高层模块
如果您还记得,我在本文第一部分中给出的复制示例充当了高层模块。DIP规定,高层模块不应依赖于低层模块的实现,而应公开低层模块应遵循的抽象。
每当需要低层模块实例时,它就会借助容器。上图中下面的语句返回低层模块的实例。
IReader object = customContainer.Resolve<IReader>();
高层模块不关心哪些类实现了IReader
接口。而且,我们不需要将包含低层模块的项目引用添加到包含高层模块的项目中。由容器负责创建低层模块(依赖项)的实例并将其返回给高层模块。
自定义IoC容器
自定义IoC容器将向外部公开两个主要方法。它们是
Register<TypeToResolve, ResolvedType>()
Resolve<TypeToResolve>()
它维护一个字典,其中TypeToResolve
和ResolvedType
的组合将使用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.Abstractions
和DIP.Implementation
:因为它需要进行DI注册。
DIP.MyIocContainer
:它将用作容器,该容器将传递给DIP.HighLevelModule以解析依赖项。DIP.HighLevelModule
:它将使用DIP.HighLevelModule来执行操作。
注意:在上述项目依赖关系中,您可以注意到DIP.HighLevelModule
和DIP.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;
}
}
| 一个 |
| 用于获取TypeToResolve类型的实例的方法 |
| 用于获取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
接口的KeyboardReader
、FileReader
等。
为了使类易于理解,我定义了两个类,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>()
注册了IReader
和KeyboardReader
的映射,因此每当需要实现IReader
对象时,都会返回KeyboardReader
的实例。
注意:有许多方法可以注册依赖项。我们也可以通过配置文件来实现DI注册。
运行应用程序
如果您运行开发的应用程序,您将获得以下输出
在这里,您可以扩展实现而不更改高层程序。您需要做的唯一一件事就是更改注册。
摘要
在本文的这一部分,我试图解释如何开发一个简单的IoC容器。希望您觉得这个主题很好且易于理解。在下一章中,我将介绍一些高级技术,使自定义IoC容器更加实用和有用。
历史
- 2020年4月23日:第二次修订(更新了最终文章的链接)
- 2013年3月29日:第一次修订