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

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2018年5月14日

CPOL

11分钟阅读

viewsIcon

27736

downloadIcon

115

新的简单而强大的 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 容器的主要用途如下:

  1. 简化注入并允许轻松更改对象实现。这很重要,原因有很多,最主要的是能够模拟测试对象并注入它们而不是真实对象。
  2. 简化插件架构,允许多个开发人员甚至多个团队几乎独立地开发各种插件。

以上两个 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` 接口的简单实现。

  1. `FileLog` 将在可执行文件所在的同一文件夹中创建一个“*MyLogFile.txt*”文件,并将日志写入其中。
  2. `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 个项目。

  1. `AssemblyLoadingTest` 是运行此测试的启动项目。
  2. Interfaces 项目包含接口(与上一个示例相同,只是这里它们被提取到一个单独的项目中)。
  3. 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 个项目。

  1. `Interfaces` - 包含 `IPlugin` 接口。
  2. `Plugin1` - 包含一个实现 `IPlugin` 接口的 `PluginOne` 类。
  3. `Plugin2` - 包含一个实现 `IPlugin` 接口的 `PluginTwo` 类。
  4. `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 容器。

© . All rights reserved.