C#插件架构问题

5

我正在使用C#开发类似Nagios的系统监控应用程序。我定义了一个插件接口:

public interface IPlugin
{
    PluginResult Execute();
}

每个插件根据其功能不同,都会有一个可变数量的参数。例如,ping插件可能需要主机名、数据包数量、超时值等参数。我希望用户能够在我的用户界面中为每个服务定义这些参数,但显然在应用程序发现可用的插件之前,这些参数是未知的。我很好奇其他人如何设计插件,使得应用程序可以发现这些可变参数。
例如,我现在有一个ping插件:
public class PingPlugin : IPlugin
{
    private const string RESULT_MESSAGE = "Average ms: {0}; Packet loss: {1}";

    private string _hostname;
    private int _packets;
    private int _timeout;
    private int _warningTimeThreshold;
    private int _warningLossThreshold;
    private int _errorTimeThreshold;
    private int _errorLossThreshold;

    public PingPlugin(
        string hostname,
        int packets,
        int timeout,
        int warningTimeThreshold,
        int warningLossThreshold,
        int errorTimeThreshold,
        int errorLossThreshold)
    {
        _hostname = hostname;
        _packets = packets;
        _timeout = timeout;
        _warningTimeThreshold = warningTimeThreshold;
        _warningLossThreshold = warningLossThreshold;
        _errorTimeThreshold = errorTimeThreshold;
        _errorLossThreshold = errorLossThreshold;
    }

    public PluginResult Execute()
    {
        // execute the plugin
    }
}

我想通过反射来发现构造函数参数,并使用属性网格向用户呈现以允许配置插件,但我不确定在这种设计中提供一组默认值的最佳方法是什么。还有哪些替代方案?

5个回答

18

3
投赞成票,MEF非常棒。这里不需要重新发明轮子,说真的。 - Dan Abramov
真的太支持这个观点了!为什么每个人都要在MEF之后,还要坚持发明另一个插件系统呢?简直难以置信! - marc_s
3
MEF可以实现插件的发现和注入,但它并不能回答一个基本问题:如何动态地暴露和连接参数到插件对象中。MEF本身并不是一个插件系统。在决定哪些服务应该被暴露以及用户将如何与插件进行交互方面,仍需要进行架构工作。 - Dan Bryant
3
插件系统不必过于复杂。如果我不需要MEF提供的全部功能,为什么要使用它呢?如果我可以将插件代码保持在几个较小的类文件中,那么另一个开发人员以后接手维护该应用程序时就更容易了,而无需了解MEF或理解我如何使用它。我理解这个建议,并感激它,但我真的无法理解其中所伴随的敌意... - Chris
1
@Chris:这总是从这样开始的。问题在于功能蔓延几乎是不可避免的。因此,下一个开发人员可能不得不浏览30个类文件,并想到“为什么他不使用MEF?” - Guvante
@Chris:过去我开发了比我记得还要多的插件架构。刚刚看了一下MEF计算器示例,它再也不能更简单了……终于不用实现YAPA了。 - Padu Merloti

4

与其让插件构造函数确定参数,你可以考虑像这样:

public interface IPlugin
{
    PluginResult Execute(Object parameters);
}

public class PingParameters
{
    //Various parameters here, including [Description] and [DisplayName] attributes if you wish
}

public class ParametersTypeAttribute : Attribute
{
    public Type Type { get; private set; }

    public ParametersTypeAttribute(Type type)
    {
        Type = type;
    }
}

[ParametersType(typeof(PingParameters))]
public class PingPlugin : IPlugin
{
    public PluginResult Execute(Object parameters)
    {
        return Execute((PingParameters) parameters);
    }

    private PluginResult Execute(PingParameters parameters)
    {
        //Your execution code here
    }
}

这样可以使参数更加灵活,因为您可以添加属性、提供设置器验证甚至为属性网格指定设计/转换器集成。属性网格直接连接到参数对象。

我非常喜欢这个想法,会进一步探索它。 - Chris
这是一个非常简单的想法,我喜欢那个。我曾经看过MEF,但它对于一个简单的插件架构来说太过复杂了。 - NotMe
@Chris,MEF绝对值得一试,因为它非常擅长处理发现和实例化/注入。此外,它现在已经包含在框架中,有很大的机会成为标准的做法。如果您的插件要求变得更加复杂,插件希望通过服务接口与主机环境进行交互,那么MEF是一个不错的选择。当然,即使您使用MEF来进行连接,您仍然需要像这样的东西来封装您的参数。 - Dan Bryant
@Dan:真正的问题在于主机应用程序需要了解插件的哪些内容。对我来说,通常并不需要很多。我的插件会向asp.net应用程序添加页面。在这种情况下,主机只是一个简单的外壳,仅维护安全检查。因此,我的主机只需要知道哪些插件已经被授权以及要加载的程序集名称。这由数据库调用处理。插件处理其他所有内容。主机传递的参数只是定义良好的安全令牌。 - NotMe
我根本不需要个别插件通过服务接口相互通信。因为插件部分只是用户界面(UI)。我已经将它们的所有代码分离成一个通用的库集合,任何模块都可以直接引用。从这个角度来看,MEF 太过于杂糅了。 - NotMe
@Chris,主要的好处在于开始执行共享发现服务的依赖注入,以实现更松散的耦合,这就是MEF的优点所在。随着你的重心转向使代码支持单元测试,你自然会倾向于注入依赖项,除了插件发现之外,此时拥有一个标准基础设施非常有帮助。听起来它现在还没有用,但值得考虑。MEF实际上并不是很复杂,但它鼓励一种基本的视角转变,需要一些时间来整合。 - Dan Bryant

1

您可以将[DefaultValue]属性应用于参数。

在C#中,您可以使用新的语法:int warningLossThreshold = 30,


0

我也为MEF答案投了+1的票,它可以解决你的许多问题。

然而,如果你想不使用MEF来完成它,那么我觉得你可能缺少了一些方式,让插件通过元数据告诉你的应用程序需要哪些参数。

一个可能的设计是:定义一个IPluginProvider接口,让你的应用程序可以发现它。这个接口应该有一个无参构造函数,所以你可以很容易地实例化一个对象。它应该有返回任何所需元数据的方法(比如参数的"漂亮名称"、哪些是必需的、哪些是一些合理的默认值等等)。然后它应该包括一个CreateInstance方法,这个方法将实际参数作为IDictionary<string,object>,并返回实际的IPlugin实例。


0

我还没有看过MEF(现在会了)。

我曾经有一个和你几乎一模一样的问题,但是我用属性解决了它。
我有一个用户界面,调用BL并使用反射来显示所有可用的“服务”(仅是适当装饰的类)。

当用户选择“服务”时,进一步的属性驱动UI。属性“schema”相当简单,允许任意数量的具有任何名称的参数。通过引入常量(与属性定义一起),您可以标准化常见的事物,如“名称”,以使您的服务保持一致。

然后将所有数据存储在键-值对表中。

这个伟大之处在于,您可以只需在bin目录中放置新的/修改的“服务”程序集——无需额外工作。唯一的依赖关系是属性定义程序集-因此请让它保持精简。

如果您想要“窃取”一些,源代码位于CodePlex


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接