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

Introducing Roxy:强大的代理生成和 IoC 容器包

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.57/5 (7投票s)

2018 年 1 月 26 日

Apache

9分钟阅读

viewsIcon

16128

downloadIcon

83

Roxy 是一个基于 Roslyn 的强大新代理生成包,它有助于实现关注点分离,创建适配器和智能混入。

引言

名称解释

包名“Roxy”是“Roslyn”和“Proxy”两个词的混合(尽管我打算让这个包不仅仅是一个代理生成器)。

Roxy 的目的和功能

Roxy 的主要目的是引入更好的关注点分离,从而简化代码。

以下是 Roxy 包目前和将来要解决的任务。

现在

  1. 使用内置转换或易于创建的自定义转换机制将接口转换为类。
  2. 创建适配器,将多个类适配到接口或抽象类或两者。
  3. 通过将行为与它们修改的对象混合,实现更大的关注点分离。
  4. 轻松访问第三方组件的非公共属性和方法。
  5. 创建智能混入,并允许轻松切换包装部分的实现。这大大增强了测试——例如,您将能够轻松地将真实的后端连接替换为模拟连接。

未来,我计划将 Roxy 打造成一个功能完善的 IoC 容器。特别是,它将允许

  1. 将接口和抽象类解析为具体的预指定(或生成)类型
  2. 生成单例对象
  3. 轻松将接口或抽象类实现替换为不同的实现

此外,将来我计划消除 Roxy 目前的一些限制

  1. 允许为泛型接口生成泛型(未完全解析)类
  2. 允许处理使用方法重载的类和接口

背景

在多年的软件开发中,我提出了许多与基本编程概念相关的想法,例如接口和实现继承、混入、适配器,以及整体与其部分(例如类与其包含的部分)之间的关系。

有一段时间,我认为为了实现这些想法,编译器需要改变。我仍然认为,通过引入新的编译器功能和对语言(无论是 C#、Java 还是 C++)进行更改,其中许多想法可以在编译器中得到更好、更高效的实现。然而,C# 中 Roslyn 的出现为在程序本身或通过 Visual Studio 插件生成和编译代码创造了巨大的潜力。相应地,代码生成有两种方法:创建 IoC 容器,该容器还将根据传递给它的参数负责生成代码;或者创建单个文件生成器工具 VS 插件,该插件将根据某些类和方法属性生成部分类的某一部分。

Roxy 是基于第一种策略构建的:它允许在使用它的同一程序中动态创建新类型。然而,我确实计划也创建自定义的单个文件生成器工具,由于 Roxy 完全基于 Roslyn(而非反射),我应该能够重用其大部分代码用于 VS 自定义插件。

在未来的文章中,我计划提出 C# 语言更改,将 Roxy 的功能添加到语言本身中(这也将更具类型安全性和效率)。

Roxy 与 Castle

Roxy 的构建视角与 Castle 完全不同。它生成代码,然后在运行时编译它,而不仅仅是修改已编译的类型。在这方面,它功能更强大,但通常在初始化上花费更多时间。一旦初始化完成,类型生成并编译——它就和任何编译代码一样快。对于大多数应用程序来说,由于 Roxy 初始化而产生的额外初始化时间与整个应用程序初始化时间相比可以忽略不计。

此外,与 Castle 不同的是,它将提供非常详细和清晰的文档,这样开发人员就不必自己发现每个功能。

本文的目的和范围

本文介绍了几个 Roxy 用法的演示示例,这些示例大大简化了我的软件开发经验(希望也能简化许多其他软件工程师的开发经验)。

更多深入讨论 Roxy 用法,特别是关注点分离的文章即将推出。

Roxy 项目的现状和代码位置

Roxy 是一个正在进行中的代码。我计划在改进它的同时,将其用于我的其他项目(自用)。

目前,Roxy 还不能被称为一个完整的 IoC 容器(只是一个代理生成器),但我计划很快添加 IoC 功能。

目前在代码生成方面存在一些限制。两个最突出的限制是

  1. 目前,Roxy 不处理方法重载:Roxy 使用的类和接口不应有任何重载方法。
  2. Roxy 可以使用泛型类,但不能生成泛型类——Roxy 生成的结果应该始终是具体的(所有泛型参数都已解析)。

Roxy 是一个位于 Github 上的开源项目 Roxy

如果您有任何希望成为 Roxy 一部分的想法,或者发现任何 bug,请在 Github 上提出问题:Roxy Issues

Roxy 示例

示例代码位置和用法

Roxy 示例也位于 Github 上的 Roxy Demos 仓库中。

要运行示例,您必须从 nuget.org 安装 nuget NP.Roxy 包。

接口默认实现示例

这是一个非常简单的示例,位于 NP.Roxy.Demos.InterfaceImpl 解决方案下。

这是主要代码

// get default implementation of IPerson
// interface containing only propertys
// the default property implementation
// is the auto property
IPerson person = Core.Concretize<IPerson>();

person.FirstName = "Joe";

person.LastName = "Doe";

person.Age = 35;

person.Profession = "Astronaut";

// test that the properties have indeed been assigned. 
Console.WriteLine($"Name='{person.FirstName} {person.LastName}'; 
                  Age='{person.Age}'; Profession='{person.Profession}'"); 

运行示例将在控制台打印以下字符串:"Name='Joe Doe'; Age='35'; Profession='Astronaut'"

请注意,当您运行应用程序时,会有大约 2-3 秒的延迟。这种延迟主要来自

  1. Roslyn 初始化——这是一次性延迟
  2. 动态程序集生成和加载——这将在每次 Roslyn 项目重新编译和程序集重新加载时发生。

当您第一次请求生成的类型或在 Core 上调用 RegenerateAssembly 方法时,会发生程序集重新加载。正确的策略是在应用程序初始化期间创建应用程序中所需的所有类型,并且只在初始化阶段生成和加载动态程序集一次。

Concretize 方法为接口 IPerson 中的所有属性提供了默认实现(即自动属性实现)。这是生成的 Person

public class Person_Concretization : NoClass, IPerson, NoInterface
{
    public static Core TheCore { get; set; }
    #region Default Constructor
    public Person_Concretization ()
    {
    }
    #endregion Default Constructor

    #region Generated Properties
    public string FirstName
    {
        get;
        set;
    }
    public string LastName
    {
        get;
        set;
    }
    public int Age
    {
        get;
        set;
    }
    public string Profession
    {
        get;
        set;
    }
    #endregion Generated Properties
}

现在看看 Program.Main 方法的顶部

// if there is a compiler error
// all the generated code will be dumped
// into "GeneratedCode" folder located within 
// the directory containing the executable
Core.SetSaveOnErrorPath("GeneratedCode");      

如注释中所述,这将在动态程序集编译错误时触发生成的代码转储。

底部还有一行代码会触发代码转储(在这种情况下,当程序成功完成时会发生代码转储)

// dump all the generated code into 
// "GeneratedCode" folder located within 
// the directory containing the executable
Core.Save("GeneratedCode");    

包装器生成

此示例 NP.Rosy.Demos.Wrappers 演示了如何将类适配到它未实现的接口。此外,它还展示了如何通过适配到接口来公开类的非公共属性和方法。

在使用第三方库(例如 Telerik、DevExpress 或 Roslyn)时,我经常遇到我需要访问的功能不是 public 的情况。我不得不使用 C# 反射来获取或设置非公共值或调用非公共方法。如下所示,Roxy 使得访问非公共功能变得极其容易。

重要提示: 访问非公共功能不应轻易为之——只有当您真正了解自己在做什么时才这样做。 

我们希望实现的 IPerson 接口与上一个示例非常相似,但在属性之外还有一个方法 string GetFullNameAndProfession()

public interface IPerson
{
    string FirstName { get; set; }

    string LastName { get; set; }

    int Age { get; set; }

    string Profession { get; set; }

    string GetFullNameAndProfession();
}      

我们想将 PersonImpl 类包装(适配)到此接口

public class PersonImpl
{
    public string FirstName { get; set; }

    private string LastName { get; set; }

    private int Age { get; set; }

    private string TheProfession { get; set; }

    private string GetFullNameAndProfession()
    {
        return $"{FirstName} {LastName} - {TheProfession}";
    }
}  

请注意,除了 FirstName 属性之外,该类的所有成员都是 private 的。我不想创建另一个项目来演示适配非公共第三方功能,所以我在同一个项目中创建了要适配的类型,但将其大部分成员设为 private

另请注意,它的所有属性和方法名称都与 IPerson 接口的相应成员名称匹配,除了 TheProfession 属性(其在 IPerson 接口中的对应项名为 Profession,没有前缀“The”)。

这是 Program.Main(...) 方法代码的主要部分

#region create the generated type configuration object
// get the type configuration object. The class that it is going to generate
// will be called "MyPersonImplementation"
ITypeConfig typeConfig =
    Core.FindOrCreateTypeConfig<IPerson, PersonImplementationWrapperInterface>("MyPersonImplementation");

// allow access to non-public members of 
// PersonImplementationWrapperInterface.ThePersonImplementation object.
typeConfig.SetAllowNonPublicForAllMembers
(nameof(PersonImplementationWrapperInterface.ThePersonImplementation));

// map TheProfession property of the wrapped object
// into Profession property of the IPerson interface.
typeConfig.SetMemberMap
(
    nameof(PersonImplementationWrapperInterface.ThePersonImplementation),
    "TheProfession",
    nameof(IPerson.Profession)
);

// Signal that the configuration is completed, 
// after ConfigurationCompleted() method is called
// TypeConfig object for this class cannot be modified.
typeConfig.ConfigurationCompleted();
#endregion create the generated type configuration object

// get the instance of the generated type "MyPersonImplementation"
IPerson person = 
    Core.GetInstanceOfGeneratedType<IPerson>("MyPersonImplementation");


// set the properties
person.FirstName = "Joe";

person.LastName = "Doe";

person.Age = 35;

person.Profession = "Astronaut";

// test that the wrapped properties and the method work
Console.WriteLine($"Name/Profession='{person.GetFullNameAndProfession()}'; Age='{person.Age}'");

让我们看看方法的顶部(“创建生成类型配置对象”区域中的部分)。

行...

ITypeConfig typeConfig =
    Core.FindOrCreateTypeConfig<IPerson, 
    PersonImplementationWrapperInterface>("MyPersonImplementation");   

...创建生成的类型配置对象。此对象将负责生成名为“MyPersonImplementation”的适配器类)。

查看 Core.FindOrCreateTypeConfig 方法的 Type 参数。第一个参数指定我们想要实现的 interface。第二个参数 PersonImplementationWrapperInterface 更复杂,需要详细解释。

PersonImplementationWrapperInterface 接口定义在 Program.cs 文件的顶部

public interface PersonImplementationWrapperInterface
{
    PersonImpl ThePersonImplementation { get; }
}  

它只包含一个只读属性 ThePersonImplementation。此属性是我们要包装的类型。

我为包装类引入此类接口的原因是 Roxy 能够包装多个对象(而不仅仅是这个简单示例中的单个对象)。此外,几个包装类可以是相同类型(它们的成员可以映射到我们实现的接口的不同属性),因此类型可能无法唯一标识此类包装类。因此,我引入了这个非常简单的包装器接口,它列出了不同名称下的所有包装对象。这些更复杂的用例将在未来的文章中讨论。

行...

typeConfig.SetAllowNonPublicForAllMembers
(
    nameof(PersonImplementationWrapperInterface.ThePersonImplementation)
);

...表示包装对象的所有成员都应该可访问,即使它们不是 public

语句...

typeConfig.SetMemberMap
(
    nameof(PersonImplementationWrapperInterface.ThePersonImplementation),
    "TheProfession",
    nameof(IPerson.Profession)
);  

...将包装对象的 TheProfession 属性映射到 interfaceProfession 属性。

请注意,如果不是因为 Profession 属性名称不匹配,我们可以使用以下快捷方法而不是“创建生成类型配置对象”区域中的整个代码块

IPerson person = 
    Core.CreateWrapperWithNonPublicMembers<IPerson, PersonImplementationWrapperInterface>
    ("MyPersonImplementation");  

运行代码将在控制台打印以下行:"Name/Profession='Joe Doe - Astronaut'; Age='35'"

将枚举映射到接口

此示例位于 NP.Roxy.Demos.EnumToInterface 解决方案下。Program.Main 代码非常简单

// we create an adaptor adapting ProductKind enumeration
// to IProduct interface using extension methods from the static 
// ProductKindExtensions class
Core.CreateEnumerationAdapter<IProduct, ProductKind>(typeof(ProductKindExtensions));

// enumeration value ProductKind.FinancialInstrument is converted into
// IProduct interface
IProduct product =
    Core.CreateEnumWrapper<IProduct, ProductKind>(ProductKind.FinancialInstrument);

// we test the methods of the resulting object that implements IProduct interface.
Console.WriteLine($"product: {product.GetDisplayName()}; Description: {product.GetDescription()}");

ProductKind 是一个枚举,它有一个 static 扩展类 ProductKindExtensions

public enum ProductKind
{
    Grocery,
    FinancialInstrument,
    Information
}

public static class ProductKindExtensions
{
    // returns a displayable short name for the ProductKind
    public static string GetDisplayName(this ProductKind productKind)
    {
        switch(productKind)
        {
            case ProductKind.Grocery:
                return "Grocery";
            case ProductKind.FinancialInstrument:
                return "Financial Instrument";
            case ProductKind.Information:
                return "Information";
        }

        return null;
    }

    // returns the full description of the ProductKind
    // note that the method is private
    private static string GetDescription(this ProductKind productKind)
    {
        switch (productKind)
        {
            case ProductKind.Grocery:
                return "Products you can buy in a grocery store";
            case ProductKind.FinancialInstrument:
                return "Products you can buy on a stock exchange";
            case ProductKind.Information:
                return "Products you can get on the Internet";
        }

        return null;
    }
}  

接口 IProduct 只有两个方法

public interface IProduct
{
    string GetDisplayName();

    string GetDescription();
}  

请注意,IProduct 方法的名称与 ProductKindExtensions 类的名称匹配。如果不是这样,我们将不得不做一些额外的名称映射工作。

还要注意,其中一个扩展方法(即 ProductKindExtensions.GetDescription(...)private 的。我故意将其设为 private,以表明非公共扩展方法(例如来自第三方库的内部方法)仍然可以使用 Roxy 进行包装。

摘要

在这里,我引入了一个新的基于 Roslyn 的运行时代码生成 Roxy 包,我计划将其发展为一个成熟的 IoC 容器。

本文预览了一些可以大大增强软件开发人员能力的功能。

Roxy 更复杂的功能和能力将在未来的文章中讨论。特别是,将介绍以下功能

  1. Roxy 的关注点分离
  2. 事件实现
  3. 多个包装(适配)类
  4. 超类代替并与接口一起使用
  5. 智能混入

历史

删除了第二个许可证 - 设置正确的许可证 (Apache) 2018 年 1 月 29 日

© . All rights reserved.