静态定义派生类型的选项 class

Statically defining options for a type of a derived class

我正在开发一个框架,其中 class 继承自框架的抽象 class 需要能够为调用 DoStuff 时可以接受的选项指定模式().

我从像这样的抽象 GetOptionsSchema() 方法开始:

public abstract class Widget
{
    public abstract OptionsSchema GetOptionsSchema();
    public abstract void DoStuff(Options options);
}

其他开发人员随后会通过创建自定义 Widget 类型来扩展我的框架:

public abstract class FooWidget: Widget
{
    public overide DoStuff(Options options)
    {
        //Do some FooWidget stuff
    }

    public overide OptionsSchema GetOptionsSchema()
    {
        //Return options for FooWidget
    }
}

这可行,但需要框架创建每个 Widget 类型的实例以确定它们接受的选项模式,即使它实际上不需要对这些类型中的任何一个执行 DoStuff()。

最终,我希望能够直接从 System.Type 确定特定 Widget 类型的选项架构。我会创建一个自定义的 OptionsSchema 属性,但是构建这些模式比在属性的构造函数中更有意义。它需要在一个方法中发生。

我见过其他框架通过创建一个自定义属性来解决类似的问题,该属性可以按名称识别静态方法或 属性。例如 NUnit 中的 TestCaseSource 属性。

此选项可能如下所示:

public abstract class Widget
{
    public abstract void DoStuff(Options options);
}

[OptionsSchemaSource(nameof(GetOptionsSchema))]
public abstract class FooWidget: Widget
{
    public overide DoStuff(Options options)
    {
        //Do some FooWidget stuff
    }

    public static OptionSchema GetOptionsSchema()
    {
        //Return options for FooWidget
    }
}

我喜欢 OptionsSchemaSource 属性如何使直接从 System.Type 获取选项架构成为可能,但这对于创建自定义 Widget 类型的其他开发人员来说似乎也不太容易发现。

使用抽象方法,另一个 Widget 开发人员知道他们必须覆盖 GetOptionSchema(),否则他们的代码将无法编译。使用 OptionsSchemaSource 属性,我能做的最好的事情就是希望人们阅读我的文档并让框架在遇到没有 OptionsSchemaSource 属性的 Widget 时在 运行 时抛出异常。

是否有 alternative/better/recommended 方法?

目前无法在编译时强制执行该属性;那将是您的用例的理想选择。也不可能有 abstract static 方法,或者在接口中指定静态方法;因此没有办法确保该方法在编译时确实存在,除非通过抽象 class 或接口(这将需要该类型的实例才能访问)强制执行实例方法。

我赞成属性的想法——期望开发人员阅读文档并不是不合理的;即使覆盖了抽象方法,开发人员也需要知道如何在覆盖的方法中构造 OptionSchema - 返回文档!

您几乎已经知道了判断什么是最佳方法所需的一切。

如前所述,您不能在类型上定义静态接口,因此无法确保新开发人员强制添加该属性。

所以,您确定的两个备选方案是我唯一能想到的两个。 现在,让我们做一个利弊,并尝试磨砺它们。

属性

您可以减轻确保开发人员将属性放在 classes 上并带有有意义的错误消息的痛苦。我会说你应该管理 classes 的发现完全基于属性,而不是继承。 如果你用Attributes来管理一切,你就不需要继承Widget。 这是一个专业,因为现在每个人都可以根据需要继承,如果需要则重新实现。

缺点是可发现性的实现会更复杂:您需要在启动时使用反射,获取 MethodInfo,检查方法是否具有正确的签名,给出适当的错误以防万一并根据需要调用对结果进行拆箱的方法。

想一想:你想要一个静态方法,因为你不需要实例化一个单一类型的 Widget 实例,但实际上实例化一个新的 Widget 可能不是什么大问题。

摘要class

好吧,您对开发人员实施了一个继承链,这可能是好的、必要的或完全可选的(您判断),但您会获得自我记录的体验。

明显的缺点是在启动时您需要为您发现的每个派生类型实例化一个 Widget,但与程序集扫描和类型检查以及方法信息发现和通过反射进行的方法调用相比,这很可能是小菜一碟。 丑陋的?有点儿。效率低下?没那么多。它的代码对您的最终用户是不可见的。

恕我直言

我发现在设计框架时,将一些 "ugly" 代码 放在 框架中是一个很好的权衡,如果这意味着每个单独的实现都使用该库会更好一点。

总而言之,如果您要设计一个灵活且易于发现的库,您应该期望开发人员至少阅读快速入门指南。如果他们可以在 5 分钟内阅读一点信息("extend a base class" 或 "add a single or a couple attributes"),并且这一点为他们提供了发现小部件注册的各个方面的方向,我会没问题的:你可以真的没有比这更好的了。

我的决定:我会走抽象的 class 路线,但要注意一点。我真的不喜欢强制基地 class。因此,我将在启动时根据界面 IWidget 组织发现,其中包含 GetOptionsSchema 方法和使用小部件所需的一切(可能是 DoStuff 方法,但很可能是其他东西)。在启动时,您搜索非抽象接口的实现,然后就可以开始了。

如果,并且仅当您真正预先需要的唯一位是字符串或其他类似的简单类型时,我会需要一个额外的属性。

[OptionsSchemaName("http://something")]
public class MyWidget : WidgetBase
{
    public overide DoStuff(Options options)
    {
        //Do some FooWidget stuff
    }

    public static OptionSchema GetOptionsSchema()
    {
        //Return options for FooWidget
    }
}

然后,您的类型发现基础架构可以搜索非抽象 IWidget 并在启动时立即抛出一个有意义的错误,例如 the type MyWidget is lacking an OptionsSchemaName attribute. Every implementation of IWidget must define one. See http://mydocs for information

砰!搞定了!