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

让我们聊聊 MEF – 第 1 部分

2018年11月13日

CPOL

11分钟阅读

viewsIcon

8666

让我们聊聊 MEF – 第 1 部分

引言

我们讨论了什么是依赖倒置和依赖注入(请参阅此处),上次我们研究了如何制作我们自己的 IoC 容器

我还承诺我们将开始研究成熟的 IoC 容器及其工作方式,因此第一个将是托管可扩展性框架 (MEF)。

我承认我对这个框架有点偏爱,原因有很多,我希望在讨论之后你会理解为什么。另外,请注意,MEF 的大量信息将使其成为一个关于如何使用它的迷你系列。否则,对于这第一篇文章,我们将介绍基础知识,以便我们有一个与其他框架进行比较的基础。

什么是MEF?

MEF 是 Managed Extensibility Framework 的缩写,自 .NET Framework 4.0 版以来,它就一直是 .NET Framework 的一部分,这对我来说意义重大,因为它不仅无需引入大量第三方框架即可随时使用(并不是说它们本身有什么问题),还意味着它在幕后被使用。我不知道你有没有注意到,但在较新版本的 Visual Studio 中(我认为从 2013 年开始),当添加插件时,Visual Studio 实际上会显示一个进度条,说明插件正在通过 MEF 加载(并可能创建)。

有一点需要注意,根据网上的一些研究,MEF 并不真正属于 IoC 框架类别,因为它真正的目的是通过插件使应用程序可扩展。话虽如此,我仍然将其用作 IoC 容器,当应用程序成熟时,我甚至可能会将其用于其实际目的。

因此,为了更好地理解 MEF,它与其他我遇到的框架有不同的术语,我们将基于一个比喻来制作一个项目,以便更容易理解其原因和术语。

使用 MEF 启动项目

编写蓝图

假设你拥有一家汽车工厂,可以制造你想要的任何汽车,但有一个问题,你的工厂要工作,它需要有一个你想要它制造的蓝图。所以我们先写这个

namespace BlogPlayground
{
    internal class Car
    {

    }
}

我知道,有点虎头蛇尾,但让我们从小处着手,然后逐步扩展。我们的汽车,像其他任何汽车一样,需要一些基本部件,例如车轮,所以我们将其设为一项要求

namespace BlogPlayground
{
    internal class Car
    {
        private const int WheelCount = 4;

        internal Car(WheelType wheelTypeType)
        {
            WheelType = wheelTypeType;
        }

        internal WheelType WheelType { get; }
    }
}

我们将假设我们所有 4 个车轮都使用相同类型的车轮,我们也将为此创建一个额外的类

namespace BlogPlayground
{
    internal class WheelType
    {
    }
}

从技术角度讲,这之所以是一个类而不是一个枚举(如果有人问起),是因为我们可能想要为车轮添加额外的功能,而且它也很适合我们的例子:)

所以现在,我们拥有了在最基本层面让我们的工厂运作所需的一切。但首先,我们将在不使用 MEF 的情况下编写它,然后再进行更新。

建造工厂

不使用 MEF 的代码如下所示

namespace BlogPlayground
{
    internal class Factory
    {
        internal Factory()
        {

        }

        internal Car CreateCar()
        {
            return new Car(new WheelType());
        }
    }
}

现在我们已经写好了工厂,接下来我们来测试一下,确保我们走在正确的轨道上。像往常一样,我们将使用 NUnit 完成这项任务

namespace BlogPlayground
{
    using NUnit.Framework;

    [TestFixture]
    public class FactoryTests
    {
        [Test]
        public void CarShouldHaveWheels()
        {
            Factory sut = new Factory();

            Car car = sut.CreateCar();

            Assert.That(car, Is.Not.Null, "car instance was not returned from the factory");
            Assert.That(car.WheelType, Is.Not.Null, 
                        "car instance should have an instance of wheel types");
        }
    }
}

我们编写测试,确保它通过,然后我们就可以继续,而不必担心犯错。

请注意,汽车及其车轮类型的创建是硬编码的,这无法帮助我们制造任何种类的汽车,对吗?所以首要任务是制作我们的容器。

为了让 MEF 工作,我们需要添加对 System.ComponentModel.Composition 程序集的引用,可以通过添加新引用并在 Assemblies 部分查找来找到它。

namespace BlogPlayground
{
    using System.ComponentModel.Composition.Hosting;
    using System.Reflection;

    internal class Factory
    {
        private readonly CompositionContainer _container;

        internal Factory()
        {
            AssemblyCatalog catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
            _container = new CompositionContainer(catalog);
        }

        internal Car CreateCar()
        {
            return _container.GetExportedValue<Car>();
        }
    }
}

现在让我们看看我们在这里做了什么

  • MEF 位于 System.ComponentModel.Composition.Hosting 命名空间中,这就是为什么我们在第 3 行添加它的原因。
  • MEF 基于“目录”概念工作,基本上,目录告诉 MEF 在哪里查找它需要的蓝图和部件。我们在第 12 行创建的 AssemblyCatalog 告诉 MEF 检查本地程序集中的所有类型(当然,如果愿意,我们也可以提供另一个程序集,稍后会详细介绍)。
  • 在第 13 行,我们创建了一个接收目录作为参数的 CompositionContainer,这是工厂的核心,我们将在下一点中看到它的工作原理。
  • 在第 18 行,我们告诉容器我们想要返回一个 Car 类型的对象,这将使容器在其目录中查找该类型的蓝图和部件,并为我们创建一辆 car

不过,如果现在运行测试,我们会得到以下错误

No exports were found that match the constraint: \
ContractName BlogPlayground.Car\
RequiredTypeIdentity BlogPlayground.Car

好吧,至少我们现在确认了容器正在工作,并尝试查找类型为 BlogPlayground.Car 的对象。我们需要帮助它找到那个契约(在我们的类比中是蓝图)。

namespace BlogPlayground
{
    using System.ComponentModel.Composition;

    [Export]
    internal class Car
    {
        private const int WheelCount = 4;

        internal Car(WheelType wheelTypeType)
        {
            WheelType = wheelTypeType;
        }

        internal WheelType WheelType { get; }
    }
}

所以现在,我们添加了 MEF 的 using 语句,并在第 5 行添加了一个名为 [Export] 的属性,它将告诉 MEF 这个对象在目录中是可用于创建的。

现在如果运行我们的测试,我们会得到以下错误

System.ComponentModel.Composition.CompositionException : The composition produced 
a single composition error. The root cause is provided below. 
Review the CompositionException.Errors property for more detailed information.

1) Cannot create an instance of type ‘BlogPlayground.Car’ because a constructor 
could not be selected for construction. Ensure that the type either has a 
default constructor, or a single constructor marked with the 
‘System.ComponentModel.Composition.ImportingConstructorAttribute’.

Resulting in: Cannot activate part ‘BlogPlayground.Car’.\
Element: BlogPlayground.Car –> BlogPlayground.Car –> AssemblyCatalog 
(Assembly=”BlogPlayground, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null”)

现在它失败了,因为 MEF 要求当我们创建的类型没有默认或无参数构造函数时,构造函数应该标记为 [ImportingConstructor],并且因为构造函数需要一些额外的部分,所以我们还需要将参数标记为 [Import]。所以,让我们按照错误提示进行操作并修复它

namespace BlogPlayground
{
    using System.ComponentModel.Composition;

    [Export]
    internal class Car
    {
        private const int WheelCount = 4;

        [ImportingConstructor]
        internal Car([Import]WheelType wheelTypeType)
        {
            WheelType = wheelTypeType;
        }

        internal WheelType WheelType { get; }
    }
}

但是,如果现在尝试运行测试,它会给出与之前完全相同的错误,即找不到汽车的契约。这就是 MEF 承认有点烦人的地方,它现在应该告诉我们它找不到 WheelType 的契约,所以要解决这个错误,我们也将更新该类

namespace BlogPlayground
{
    using System.ComponentModel.Composition;

    [Export]
    internal class WheelType
    {
    }
}

现在如果运行我们的测试,它将通过。但是,当我们只需要用硬编码行解决问题时,为什么要费力做所有这些呢?而且我知道它与我们使用具体类时并没有那么不同。好吧,如果你问了这个问题,你是对的,但现在让我们看看如何真正发挥 MEF 的强大功能。

MEF 的魔力

首先,我们将 Car 类改为 abstract,因为我们希望使用许多不同的模型

namespace BlogPlayground
{
    internal abstract class Car
    {
        private const int WheelCount = 4;

        internal Car(WheelType wheelTypeType)
        {
            WheelType = wheelTypeType;
        }

        internal WheelType WheelType { get; }
    }
}

由于我们无法实例化一个 abstract 类,我们移除了 MEF 的属性。接下来,我们将创建一个跑车

namespace BlogPlayground
{
    using System.ComponentModel.Composition;

    [Export]
    class SportCar : Car
    {
        [ImportingConstructor]
        public SportCar([Import]WheelType wheelTypeType)
            : base(wheelTypeType)
        {
        }
    }
}

一切都很好,但这行不通,因为我们想创建一辆 Car,但现在我们有一份 SportCar 的蓝图。为了让它工作,我们需要告诉 MEF 这也将作为一辆 Car 导出,为此,我们只需在属性中指定类型,如下所示

namespace BlogPlayground
{
    using System.ComponentModel.Composition;

    [Export(typeof(Car))]
    internal class SportCar : Car
    {
        [ImportingConstructor]
        internal SportCar([Import]WheelType wheelTypeType)
            : base(wheelTypeType)
        {
        }
    }
}

但为了确保万无一失,我们还需要添加另一个测试来确认

[Test]
public void CarShouldBeASportCar()
{
    Factory sut = new Factory();

    Car car = sut.CreateCar();

    Assert.That(car, Is.Not.Null, "car instance was not returned from the factory");
    Assert.That(car, Is.TypeOf<SportCar>(), "the instance was not of type SportsCar");
    Assert.That(car.WheelType, Is.Not.Null, "car instance should have an instance of wheel types");
}

我们编写这个测试,它第一次就通过了,而且我们也没有对工厂进行任何更改。对于如此小的代码库,这看起来并不那么令人印象深刻,但考虑在服务和大型应用程序层面使用它,仅通过一个属性就能改变整个行为的力量。

让我们看看如何为汽车添加引擎,但这次我们将使用接口。 😉

创建引擎

首先,我们将为我们的 Engine 创建一个接口。我们还希望在不明确声明导出的情况下导出任何此类类型。接口将如下所示

namespace BlogPlayground
{
    using System.ComponentModel.Composition;

    [InheritedExport]
    internal interface IEngine
    {
    }
}

[InheritedExport] 属性会将继承此类的任何内容作为 IEngine 导出。

现在让我们用新的先决条件更新我们的 Car

namespace BlogPlayground
{
    internal abstract class Car
    {
        private const int WheelCount = 4;

        internal Car(WheelType wheelTypeType, IEngine engine)
        {
            WheelType = wheelTypeType;
            Engine = engine;
        }

        internal WheelType WheelType { get; }

        internal IEngine Engine { get; }
    }
}

既然这是强制性的,我们也需要更新 SportCar

namespace BlogPlayground
{
    using System.ComponentModel.Composition;

    [Export(typeof(Car))]
    internal class SportCar : Car
    {
        [ImportingConstructor]
        internal SportCar([Import]WheelType wheelTypeType, [Import]IEngine engine)
            : base(wheelTypeType, engine)
        {
        }
    }
}

由于 Engine 只是一个接口,我们还需要实现它,没什么花哨的

namespace BlogPlayground
{
    class SportEngine : IEngine
    {
    }
}

请注意,一旦添加了这个类,所有测试又都通过了。请注意,一个导入和一个导出只能一对一匹配,因此如果我们要添加另一个引擎,我们会收到一个错误,告诉我们有多个引擎,容器不知道如何处理它们。

不过,这是一个很好的地方来展示 MEF 如果您愿意,是如何超越竞争对手的,而且由于一辆 Car 不能有多个引擎,我们将转到功能部分,所以让我们为我们的 car 添加一些功能。

添加功能

首先,让我们为功能创建一个接口

namespace BlogPlayground
{
    using System.ComponentModel.Composition;

    [InheritedExport]
    public interface IFeature
    {

    }
}

这里我们将对 [InheritedExport] 属性做同样的事情,以便将来可以轻松扩展功能列表,所以让我们先创建一些功能

namespace BlogPlayground
{
    using System.ComponentModel.Composition;

    [InheritedExport]
    public interface IFeature
    {

    }

    class USB : IFeature
    {
    }

    class GPS : IFeature
    {
    }

    class Radio : IFeature
    {
    }
}

所以让我们更新我们的汽车以适应这些功能

namespace BlogPlayground
{
    using System.Collections.Generic;

    internal abstract class Car
    {
        private const int WheelCount = 4;

        internal Car(WheelType wheelTypeType, IEngine engine, IEnumerable<IFeature> features)
        {
            WheelType = wheelTypeType;
            Engine = engine;
            Features = features;
        }

        internal WheelType WheelType { get; }

        internal IEngine Engine { get; }

        public IEnumerable<IFeature> Features { get; }
    }
}

现在进行实现,请仔细观察参数,[Import] 参数可以由单个 [Export] 满足,而 [ImportMany] 参数可以由多个 [Export] 满足

namespace BlogPlayground
{
    using System.Collections.Generic;
    using System.ComponentModel.Composition;

    [Export(typeof(Car))]
    internal class SportCar : Car
    {
        [ImportingConstructor]
        internal SportCar([Import]WheelType wheelTypeType, 
        [Import]IEngine engine, [ImportMany] IEnumerable<IFeature> features)
            : base(wheelTypeType, engine, features)
        {
        }
    }
}

再次,测试已经通过了,但让我们确保它们都在那里,让我们添加另一个测试

[Test]
public void CarShouldHaveThreeFeatures()
{
    Factory sut = new Factory();

    Car car = sut.CreateCar();

    Assert.That(car, Is.Not.Null, "car instance was not returned from the factory");
    Assert.That(car.Features, Is.Not.Null, "car instance should have a collection of features");
    Assert.That(car.Features, Has.Length.EqualTo(3), "the car should have 3 features");
    Assert.That(car.Features.ElementAt(0), Is.TypeOf<USB>());
    Assert.That(car.Features.ElementAt(1), Is.TypeOf<GPS>());
    Assert.That(car.Features.ElementAt(2), Is.TypeOf<Radio>());
}

当我们运行这个测试时,我们会看到它也通过了。我们现在已经创建了一种在不修改汽车的情况下向我们的汽车添加功能的方法,我们所需要做的就是创建另一个功能并实现 IFeature 接口。

这只是 MEF 提供的众多功能之一,除了它也具有可扩展性之外,在我结束这篇文章之前,我想向你展示另一件事,因为 MEF 还有很多内容需要涵盖,而不仅仅是这些,这些只是基础知识。

多辆汽车?

假设你非常喜欢你的新汽车,你想也为你的朋友创造一辆,让我们通过一个新测试来举例说明

[Test]
public void ShouldBeAbleToCreateMultipleCars()
{
    Factory sut = new Factory();

    Car car1 = sut.CreateCar();
    Car car2 = sut.CreateCar();

    Assert.That(car1, Is.Not.Null, "car instance was not returned from the factory");
    Assert.That(car2, Is.Not.Null, "car instance was not returned from the factory");
    Assert.That(car1, Is.Not.EqualTo(car2), "the two instances should be different");
}

如果运行此测试,它将失败,主要是因为 MEF 和依赖注入通常被认为是具有可重用可交换部件的,因此当我们调用工厂创建第二个实例时,它将返回相同的实例,但我们可以更改它,我们所需要做的就是以下操作

namespace BlogPlayground
{
    using System.Collections.Generic;
    using System.ComponentModel.Composition;

    [Export(typeof(Car))]
    [PartCreationPolicy(CreationPolicy.NonShared)]
    internal class SportCar : Car
    {
        [ImportingConstructor]
        internal SportCar([Import]WheelType wheelTypeType, 
                          [Import]IEngine engine, [ImportMany] IEnumerable<IFeature> features)
            : base(wheelTypeType, engine, features)
        {
        }
    }
}

通过添加带有 CreationPolicy.NonShared[PartCreationPolicy] 属性,我们告诉 MEF,每当它创建此部件时,它都应该每次创建新实例,回到我们的类比,两个车轮之间的轴或汽车车架应该共享,但每个车轮不应该共享,因为我们将有 4 个车轮。

[PartCreationPolicy] 可以应用于 [Import][Export] 属性,它可以有 3 个值:SharedNotSharedAny(默认情况下,如果未指定,则视为 Any),因此它们之间的组合需要匹配,并且匹配方式如下

导入 导出 实例
共享 共享 单例实例
共享 非共享 不匹配
共享 任意 单例实例
非共享 共享 不匹配
非共享 非共享 独立实例
非共享 任意 独立实例
任意 共享 单例实例
任意 非共享 独立实例
任意 任意 单例实例

结论

我希望您喜欢这里介绍的 MEF 一小部分,将来,我们将研究其他巧妙的功能,例如(但不限于)元数据、程序集发现、函数导出(是的,我们甚至可以只导出字符串和函数)、契约和惰性初始化、自定义导出,这些都是开箱即用的,根本无需扩展 MEF。

以下是我(以及我曾工作过的团队中的其他人)过去使用 MEF 的几个例子,我也很好奇你会有什么想法

  • 在不同的程序集中制作桌面应用程序模块,这些模块会在启动时加载。
  • 制作可以实时从服务器下载模块并无需重启应用程序即可更新的应用程序。
  • 用于根据角色或其他条件启用和禁用对应用程序模块的访问
  • 制作无需修改核心代码即可插入应用程序生命周期的函数。

请注意,这不是一个连载系列,但会有承诺的补充内容。我之所以提到这一点,是因为 MEF 可能不是您或其他人正在寻找的东西,而且它只会延迟其他框架的展示。

谢谢,下次再见。

© . All rights reserved.