Introducing IoCy - 简单而强大的 IoC 容器





5.00/5 (9投票s)
新的简单而强大的 IoC 容器
引言
重要提示:本文已不再更新。有关正确的 API,请参阅 https://codeproject.org.cn/Articles/5349911/Generic-Minimal-Inversion-of-Control-Dependency-In。
我为什么写这篇文章
在我的一生中,我处理过许多不同的 IoC 容器,包括 MEF、Unity、AutoFac 和 Castle Dynamic Proxy。我还研究过 Ninject。
我见过一些容器被使用,但并没有解决任何问题——它们被使用是因为项目的架构师无法想象没有 IoC 来构建项目。
我还注意到,通常只有一小部分 IoC 容器的功能实际上是有用的。
最后,大多数容器包都很大——比它们有用部分的功能大得多。
综合以上所有原因,我决定推出我自己的容器 IoCy,它经过优化,性能高且体积非常小,但包含了可以简化项目架构师和开发人员生活的必要功能。
Paulo Zemek 在 CodeProject 上发表了一篇很棒的文章 Expandable IoC Container。这篇文章描述了如何构建一个非常简单的 IoC 容器,最初我打算将该容器用于我的目的,但在看过它之后,我认为其中描述的容器有点过于粗糙。我希望有一个 IoC 容器,可以让我添加整个程序集,并且还允许我构建插件架构,而这些部分在那里是缺失的。
除了介绍一个新的 IoC 容器,我还试图描述 IoC 容器的哪些功能真正有用以及如何使用它。
什么是 IoC 容器
IoC 容器是一种软件,它负责为您创建对象。您无需调用对象的构造函数,而是调用容器上的方法,通常传递您想要获取的对象的 `Type`,也许还有一些其他参数,它将返回请求类型的对象给您。
使用示例
递归解析接口类型
此时,为了举例,我假设我们有一个通用容器,但这里描述的方法与 IoCy 的方法名称相同。
例如,假设您有一个接口 `IOrg`,该接口由多个类实现,包括类 `Organization`。假设您已以某种方式预先配置了您的 `Container`,将接口 `IOrg` 映射到类 `Organization`(稍后将描述创建此类映射的具体方法)。然后,您可以调用 `IoCContainer.Resolve(...)` 方法来获取对象。
IOrg organization = IoCContainer.Resolve<IOrg>();
请注意,很多时候,您希望在每次调用 `IoCContainer.Resolve()` 方法时创建一个新对象——所以它将替代构造函数。
然而,有时您可能希望该方法在每次调用时都返回同一个对象。例如,如果您要记录一些消息,您可能希望在每个地方都使用同一个记录器,所以调用
ILog log = IoCContainer.Resolve<ilog>();
每次调用它时都应该返回同一个对象。这样的对象称为单例——每个容器只有一个这样的对象实例。您可以在构建容器的配置(映射)阶段指定是否希望每次都将某个类型解析为单例。本节将在文章稍后介绍。
因此,容器指定了您想要解析的类型与某种对象创建策略之间的映射。
请注意,当您解析对象时,容器还会尝试解析其可组合的部分(属性)。例如,假设我们的 `IOrg` 接口有一个 `public` 属性 `Manager`,其类型为接口 `IPerson`,映射到具体类 `Person`,以及另一个类型为 `ILog` 的属性。假设这些属性也以某种方式标记为“可组合”。当您调用
IPerson person = IoCContainer.Resolve<iperson>();
容器将尝试解析这些属性。如果这些接口(或实现它们的类型)也有一些可组合的属性——这些属性也应该由容器递归解析——依此类推。
请注意,某些容器,例如 Unity,假定所有属性都是可组合的。这种假设会导致更长的组合算法,因为必须检查每个属性。
插件架构
IoC 容器的另一个主要用途是简化插件架构。
例如,您有多个团队为您的应用程序开发各种小部件。插件架构有潜力将它们全部集成在一起,作为 IoC 容器的一部分。在这种情况下,我们应该使用类似于 MEF 的 `ImportMany` 的多对象解析容器功能。
每个插件都可以实现一个特定的接口,例如 `IPlugin`。插件可以复制到一个特定的文件夹(应与应用程序文件夹不同),然后从那里动态加载到容器中。然后应用程序将从容器中获取它们并进行排列,例如作为选项卡或基于它们的配置。
IoC 容器的目的
基于以上,IoC 容器的主要用途如下:
- 简化注入并允许轻松更改对象实现。这很重要,原因有很多,最主要的是能够模拟测试对象并注入它们而不是真实对象。
- 简化插件架构,允许多个开发人员甚至多个团队几乎独立地开发各种插件。
以上两个 IoC 功能都作为 IoCy 容器的一部分实现。
许多容器实现了其他功能,但并不经常使用。允许容器创建您的对象还可以做一些疯狂的事情,例如代码生成或即时代码织入以及其他可以促进创建代理、在函数或属性设置器中插入预或后代码以及面向方面的功能的工具。对于这些,您将不得不等到我将 IoCy 和 Roxy 结合起来。
代码示例
引言
在本节中,我将介绍展示如何使用 IoCy 容器的示例。
代码样本位置
您可以从文章上方的链接下载代码,也可以从 GITHUB 获取,地址是 NP.IoCy。
只有一个解决方案 - `NP.IoCy.IoCContainerAndTests`。包含 IoC 容器代码的项目是 `NP.IoCy`。在 *TESTS* 解决方案文件夹下有几个测试项目。每当您想运行测试时,都必须将相应的项目设置为启动项目。
代码概述
如上所述,IoCy 代码位于 `NP.IoCy` 项目下。 `IoCContainer` 类代表容器,包含大部分代码。
`NP.IoCy` 项目还包含一个 `Attributes` 文件夹,其中定义了许多要由 `IoCContainer` 使用的 `Attributes`,以及一个 `Utils` 文件夹,其中包含两个非常简单的扩展类 `ObjUtils` 和 `ReflectionUtils`。
引导程序示例
要运行示例,请将 `BootstrappingTest` 项目设置为启动项目。该示例由 `Program.cs` 文件和两个文件夹组成:*Interfaces* 和 *Implementations*,它们包含相应的接口和类。
这是 `IOrg` 接口的代码。
public interface IOrg
{
string OrgName { get; set; }
IPerson Manager { get; set; }
ILog Log { get; set; }
void LogOrgInfo();
}
这是实现 `IOrg` 接口的 `Org` 类的代码。
public class Org : IOrg
{
public string OrgName { get; set; }
[Part]
public IPerson Manager { get; set; }
[Part]
public ILog Log { get; set; }
public void LogOrgInfo()
{
Log.WriteLog($"OrgName: {OrgName}");
Log.WriteLog($"Manager: {Manager.PersonName}");
Log.WriteLog($"Manager's Address: {Manager.Address.City}, {Manager.Address.ZipCode}");
}
}
请注意,`Part` 属性是 `IoCContainer` 的一个提示,表明它需要解析该属性。
您可以看到 Organization 包含类型为 `IManager` 的属性 `Manager`,类型为 `ILog` 的属性 `Log`。查看 `IPerson` 接口,您可以看到它包含类型为 `IAddress` 的属性 `Address`。
对于涉及的其余接口/类对,我将仅展示类。
这是 `Person` 类的实现。
public class Person : IPerson
{
public string PersonName { get; set; }
[Part]
public IAddress Address { get; set; }
}
这是 `Address` 类的代码。
public class Address : IAddress
{
public string City { get; set; }
public string ZipCode { get; set; }
}
最后,我们有两个 `ILog` 接口的简单实现。
- `FileLog` 将在可执行文件所在的同一文件夹中创建一个“*MyLogFile.txt*”文件,并将日志写入其中。
- `ConsoleLog` 将日志写入控制台。
public class FileLog : ILog
{
const string FileName = "MyLogFile.txt";
public FileLog()
{
if (File.Exists(FileName))
{
File.Delete(FileName);
}
}
public void WriteLog(string info)
{
using(StreamWriter writer = new StreamWriter(FileName, true))
{
writer.WriteLine(info);
}
}
}
public class ConsoleLog : ILog
{
public void WriteLog(string info)
{
Console.WriteLine(info);
}
}
现在我们已经了解了对象的结构以及对象之间的关系,让我们看一下*Program.cs*文件中的 `Program.Main()` 方法。
static void Main(string[] args)
{
// create container
IoCContainer container = new IoCContainer();
#region BOOTSTRAPPING
// bootstrap container
// (map the types)
container.Map<IPerson, Person>();
container.Map<IAddress, Address>();
container.Map<IOrg, Org>();
container.MapSingleton<ILog, FileLog>();
#endregion BOOTSTRAPPING
// after CompleteConfiguration
// you cannot bootstrap any new types in the container.
// before CompleteConfiguration call
// you cannot resolve container types.
container.CompleteConfiguration();
// resolve and compose organization
// all its 'Parts' will be added at
// this stage.
IOrg org = container.Resolve<IOrg>();
#region Set Org Data
org.OrgName = "Nicks Department Store";
org.Manager.PersonName = "Nick Polyak";
org.Manager.Address.City = "Miami";
org.Manager.Address.ZipCode = "33162";
#endregion Set Org Data
// Create file MyLogFile.txt in the same folder as the executable
// and write department store info in it;
org.LogOrgInfo();
// replace mapping to ILog to ConsoleLog in the child container.
IoCContainer childContainer = container.CreateChild();
// change the mapping of ILog to ConsoleLog (instead of FileLog)
childContainer.Map<ILog, ConsoleLog>();
// complete child container configuration
childContainer.CompleteConfiguration();
// resolve org from the childContainer.
IOrg orgWithConsoleLog = childContainer.Resolve<IOrg>();
#region Set Child Org Data
orgWithConsoleLog.OrgName = "Nicks Department Store";
orgWithConsoleLog.Manager.PersonName = "Nick Polyak";
orgWithConsoleLog.Manager.Address.City = "Miami";
orgWithConsoleLog.Manager.Address.ZipCode = "33162";
#endregion Set Child Org Data
// send org data to console instead of a file.
orgWithConsoleLog.LogOrgInfo();
}
我们创建容器,然后引导它,即在代码中定义类和接口之间的映射。
// create container
IoCContainer container = new IoCContainer();
#region BOOTSTRAPPING
// bootstrap container
// (map the types)
container.Map<IPerson, Person>();
container.Map<IAddress, Address>();
container.Map<IOrg, Org>();
container.MapSingleton<ILog, FileLog>();
#endregion BOOTSTRAPPING
引导完成后,我们通过调用 `container.ConfigurationCompleted()` 方法来表示。
调用此方法后,我们就无法再修改容器类了。这样做是为了避免在引导阶段之后出现线程锁定。
现在我们创建并组合 `IOrg` 对象。
// resolve and compose organization
// all its 'Parts' will be added at
// this stage.
IOrg org = container.Resolve<IOrg>();
返回的对象类型为 `Org`,它的部分(以及其部分的各部分)是从容器中解析的,并基于在引导阶段定义的映射。
接下来,我们设置组织及其部分的的数据。
#region Set Org Data
org.OrgName = "Nicks Department Store";
org.Manager.PersonName = "Nick Polyak";
org.Manager.Address.City = "Miami";
org.Manager.Address.ZipCode = "33162";
#endregion Set Org Data
然后,我们调用 `org.LogOrgInfo()` 方法将数据写入 `ILog` 实现。请注意,我们在引导阶段将 `ILog` 映射到 `FileLog`,因此文本将被写入项目“*bin/debug*”文件夹中的“*MyLogFile.txt*”文件。
现在是最有趣的部分,我将演示使用 IoCy 替换实现有多么容易。
请记住,我们无法在引导阶段结束后修改容器,但我们可以创建一个子容器。
// replace mapping to ILog to ConsoleLog in the child container.
IoCContainer childContainer = container.CreateChild();
然后我们可以在子容器中设置任何我们想要更改的映射。在我们的例子中,我们想将 `ILog` 映射到 `ConsoleLog` 而不是 `FileLog`。
// change the mapping of ILog to ConsoleLog (instead of FileLog)
childContainer.Map<ILog, ConsoleLog>();
// complete child container configuration
childContainer.CompleteConfiguration();
从子容器解析以责任链方式工作——当它找不到映射时,它会向上检查其父容器。在我们的例子中,只有 `ILog` 将从子容器中解析,其他所有内容都将从父容器中解析。
// resolve org from the childContainer.
IOrg orgWithConsoleLog = childContainer.Resolve<IOrg>();
现在我们为新对象设置数据并调用 `LogOrgInfo()` 方法。
#region Set Child Org Data
orgWithConsoleLog.OrgName = "Nicks Department Store";
orgWithConsoleLog.Manager.PersonName = "Nick Polyak";
orgWithConsoleLog.Manager.Address.City = "Miami";
orgWithConsoleLog.Manager.Address.ZipCode = "33162";
#endregion Set Child Org Data
// send org data to console instead of a file.
orgWithConsoleLog.LogOrgInfo();
信息将被打印到控制台而不是文件中。
程序集加载示例
此示例位于*TESTS/AssemblyLoadingTest*文件夹下。它包含 3 个项目。
- `AssemblyLoadingTest` 是运行此测试的启动项目。
- Interfaces 项目包含接口(与上一个示例相同,只是这里它们被提取到一个单独的项目中)。
- Implementations 项目包含实现(与上一个示例完全相同)。
Implementations 项目依赖于 Interfaces 项目,而 `AssemblyLoadingTest` 项目依赖于 Implementation 和 Interfaces。
请注意,实现现在具有 `Implements` 类属性,例如:
[Implements(typeof(IOrg))]
public class Org : IOrg
{
...
}
或
[Implements]
public class Person : IPerson
{
...
}
`Implements` 属性指示 `IoCContainer`,应将此实现映射到另一个更抽象的类型(超类或接口)。此类要么作为参数显式指定给 `Implements` 属性(如 `[Implements(typeof(IOrg))]`),要么隐式假定为非平凡的基类或实现的第一个接口(在 `Person` 类的情况下,它将是 `IPerson` 接口)。
现在看一下 `AssemblyLoadingTest` 项目中的 `Program.Main` 方法。它与前一个示例完全相同,除了整个引导部分被以下行替换:
container.InjectAssembly(typeof(Implementations.Org).Assembly);
所有必需的引导都通过使用程序集中 `Implements` 和 `Part` 属性的信息自动完成。
动态加载程序集示例
此示例位于*TEST/DynamicAssemblyLoadingTest*文件夹下。其主项目是 `DynamicAssemblyLoadingTest`。
除了一个非常重要的细节外,几乎所有内容都与前一个示例相同——主项目 `DynamicAssemblyLoadingTest` 不依赖于 `Implementations` 项目。相反,*Implementations* 项目有一个生成后事件,它将程序集文件复制到“*DynamicAssemblyLoadingTest/bin/Debug/Plugins*”文件夹。
然后,对于程序集注入,我们使用 `InjectDynamicAssemblyByFullPath(...)` 方法。
container.InjectDynamicAssemblyByFullPath("Plugins\\Implementations.dll");
此方法通过指定路径动态加载程序集。结果与前两个示例完全相同。
多部分引导示例
此示例位于 `MultiPartBootstrappingTest` 项目中(需要将此项目设置为启动项目)。
此示例与第一个示例非常相似(所有接口和实现都位于同一个项目中)。
但是,有一个重要的区别:`Org` 类(以及相应的 `IOrg` 接口)有一个 `ILogs` 对象枚举,而不是单个 `ILog` 对象。
[MultiPart]
public IEnumerable<ilog> Logs { get; set; }
</ilog>
该枚举由 `MultiPart` 属性(而不是 `Part` 属性)标记。
`Org.LogOrgInfo()` 方法的实现也有所不同,因为我们遍历日志枚举中的每个 `ILog` 并将信息发送给其中每一个。
public void LogOrgInfo()
{
foreach(ILog log in Logs)
{
log.WriteLog($"OrgName: {OrgName}");
log.WriteLog($"Manager: {Manager.PersonName}");
log.WriteLog($"Manager's Address: {Manager.Address.City}, {Manager.Address.ZipCode}");
}
}
`Program.Main` 方法与第一个示例中的方法非常相似,除了引导 `ILog`。
container.MapMultiType<ilog, filelog="">();
container.MapMultiType<ilog, consolelog="">();
</ilog,></ilog,>
`IoCContainer.MapMultiType()` 方法将实现添加到与 `ILog` 接口对应的实现集合中。
运行应用程序时,您将看到文件和控制台都接收到相同的文本。
插件示例
最后一个示例展示了如何使用 IoCy 来实现插件架构,其中多个插件被动态加载到系统中。
该示例位于*PluginsTest*文件夹下。它包含 4 个项目。
- `Interfaces` - 包含 `IPlugin` 接口。
- `Plugin1` - 包含一个实现 `IPlugin` 接口的 `PluginOne` 类。
- `Plugin2` - 包含一个实现 `IPlugin` 接口的 `PluginTwo` 类。
- `PluginTest` - 包含主程序。
这是 `IPlugin` 接口和插件实现的非常简单的代码。
public interface IPlugin
{
void PrintMessage();
}
[MultiImplements]
public class PluginOne : IPlugin
{
public void PrintMessage()
{
Console.WriteLine("I am PluginOne");
}
}
[MultiImplements(typeof(IPlugin))]
public class PluginTwo : IPlugin
{
public void PrintMessage()
{
Console.WriteLine("I am PluginTwo");
}
}
请注意,主项目 `PluginsTest` 不依赖于 `Plugin1` 和 `Plugin2` 项目。相反,`Plugin1` 和 `Plugin2` 项目有一个 `PostBuild` 事件,将它们的 DLL 复制到 *PluginTest/bin/Debug/Plugins* 文件夹。
这是 `Program.Main` 方法的代码。
static void Main(string[] args)
{
IoCContainer container = new IoCContainer();
container.InjectPluginsFromFolder("Plugins");
container.CompleteConfiguration();
IEnumerable<iplugin> plugins = container.MultiResolve<iplugin>();
foreach(IPlugin plugin in plugins)
{
plugin.PrintMessage();
}
}
结果是,消息将从每个插件中打印到控制台。
摘要
在本篇文章中,我演示了如何用不到 1000 行代码来实现有用的 IoC 功能。核心 `IoCy` 容器功能就在这里,尽管您可能需要根据需要进行一些轻微的调整。
如上所述,在不久的将来,我计划将 `IoCy` 和 `Roxy` 结合起来,提供一个具有代码生成功能的完整 IoC 容器。