没有接口的C#插件模式

时间:2011-09-11 11:16:12

标签: c# oop extensibility plugin-pattern

我遇到了实现插件模式的需要,这种模式不适合我在其他地方看到的任何东西,我只是想知道我是否以错误的方式看待它或者是否有其他人遇到过相同的问题问题,可能有解决方案。

基本上,我们有一个系统,它包含一个核心组件,以及许多插入其中的模块。一些模块依赖于其他模块,但是需要不时地删除或替换其中的一些依赖项,我希望尽可能避免重新编译。

系统是一个定制的CMS,模块是插件,提供CMS内的功能。例如,我们有一个评论模块和几个内容模块,如新闻模块,博客模块等,可以包括评论功能。我的问题是有些客户可能不会购买评论模块,所以我要么找到一种方法来防止依赖模块依赖于评论模块的存在,在某些情况下,可能需要满足修改版本的评论模块。

我们正在运行时加载模块,目前,为了避免模块之间的相互依赖性,我们使用核心CMS程序集中保存的接口来处理它。我担心的是,为了避免每次创建可能存在依赖关系的新模块时都必须修改核心CMS程序集,我需要使用比这些接口的接口和实现更松散的东西。

我正在考虑以下事项:

  • 核心程序集包含一个允许注册和取消注册共享输入/输出消息的对象(例如“Comments.AddComment”或“Comments.ListComments”)
  • 加载模块时,会宣传他们需要的服务和服务 它们提供(例如,新闻模块将需要“Comments.AddComment”消息,并且评论模块的任何变体将提供“Comments.AddComment”消息)。
  • 传递给这些消息的任何对象或数据都将从非常松散的基类继承,或者实现一个接口,该接口公开核心程序集中包含的IDictionary类型的属性。或者,消息合同只需要一个object类型的参数,我将匿名对象从提供者/消费者传递给它们。

缺点显然是失去了强类型,但优点是我不依赖严格的接口实现或者需要包含运行时可能不存在的模块。

通过Reflection加载插件,检查引用的程序集并查找实现给定接口的类。 MEF和动态类型不是一个选项,因为我只限于.NET 3.5。

任何人都可以提出更好的建议,或者可能采用不同的方式来思考这个问题吗?

3 个答案:

答案 0 :(得分:2)

好的,做了一些挖掘,找到了我要找的东西。

注意:这是旧代码,它没有使用任何模式或类似的东西。哎呀它甚至不是它自己的对象,但它有效:-)你需要调整这个想法以你想要的方式工作。

首先,首先是一个循环,它获取在特定目录中找到的所有DLL文件,在我的情况下,这是在apps安装文件夹下的一个名为“plugins”的文件夹中。

private void findPlugins(String path)
{
  // Loop over a list of DLL's in the plugin dll path defined previously.
  foreach (String fileName in Directory.GetFiles(path, "*.dll"))
  {
    if (!loadPlugin(fileName))
    {
      writeToLogFile("Failed to Add driver plugin (" + fileName + ")");
    }
    else
    {
      writeToLogFile("Added driver plugin (" + fileName + ")");
    }
  }// End DLL file loop

}// End find plugins

正如您将看到对'loadPlugin'的调用,这是执行识别和加载单个dll作为系统插件的实际例程。

private Boolean loadPlugin(String pluginFile)
{
  // Default to a successfull result, this will be changed if needed
  Boolean result = true;
  Boolean interfaceFound = false;

  // Default plugin type is unknown
  pluginType plType = pluginType.unknown;

  // Check the file still exists
  if (!File.Exists(pluginFile))
  {
    result = false;
    return result;
  }

  // Standard try/catch block
  try
  {
    // Attempt to load the assembly using .NET reflection
    Assembly asm = Assembly.LoadFile(pluginFile);

    // loop over a list of types found in the assembly
    foreach (Type asmType in asm.GetTypes())
    {
      // If it's a standard abstract, IE Just the interface but no code, ignore it
      // and continue onto the next iteration of the loop
      if (asmType.IsAbstract) continue;

      // Check if the found interface is of the same type as our plugin interface specification
      if (asmType.GetInterface("IPluginInterface") != null)
      {
        // Set our result to true
        result = true;

        // If we've found our plugin interface, cast the type to our plugin interface and
        // attempt to activate an instance of it.
        IPluginInterface plugin = (IPluginInterface)Activator.CreateInstance(asmType);

        // If we managed to create an instance, then attempt to get the plugin type
        if (plugin != null)
        {
          // Get a list of custom attributes from the assembly
          object[] attributes = asmType.GetCustomAttributes(typeof(pluginTypeAttribute), true);

          // If custom attributes are found....
          if (attributes.Length > 0)
          {
            // Loop over them until we cast one to our plug in type
            foreach (pluginTypeAttribute pta in attributes)
              plType = pta.type;

          }// End if attributes present

          // Finally add our new plugin to the list of plugins avvailable for use
          pluginList.Add(new pluginListItem() { thePlugin = plugin, theType = plType });
          plugin.startup(this);
          result = true;
          interfaceFound = true;

        }// End if plugin != null
        else
        {
          // If plugin could not be activated, set result to false.
          result = false;
        }
      }// End if interface type not plugin
      else
      {
        // If type is not our plugin interface, set the result to false.
        result = false;
      }
    }// End for each type in assembly
  }
  catch (Exception ex)
  {
    // Take no action if loading the plugin causes a fault, we simply
    // just don't load it.
    writeToLogFile("Exception occured while loading plugin DLL " + ex.Message);
    result = false;
  }

  if (interfaceFound)
    result = true;

  return result;
}// End loadDriverPlugin

正如您将在上面看到的,有一个结构可以保存插件条目的信息,这被定义为:

    public struct pluginListItem
    {
      /// <summary>
      /// Interface pointer to the loaded plugin, use this to gain access to the plugins
      /// methods and properties.
      /// </summary>
      public IPluginInterface thePlugin;

      /// <summary>
      /// pluginType value from the valid enumerated values of plugin types defined in
      /// the plugin interface specification, use this to determine the type of hardware
      /// this plugin driver represents.
      /// </summary>
      public pluginType theType;
    }

以及将加载器绑定到所述结构的变量:

    // String holding path to examine to load hardware plugins from
    String hardwarePluginsPath = "";

    // Generic list holding details of any hardware driver plugins found by the service.
    List<pluginListItem> pluginList = new List<pluginListItem>();

使用接口'IPlugininterface'定义实际的插件DLL,并使用Enumeration定义插件类型:

      public enum pluginType
      {
        /// <summary>
        /// Plugin is an unknown type (Default), plugins set to this will NOT be loaded
        /// </summary>
        unknown = -1,

        /// <summary>
        /// Plugin is a printer driver
        /// </summary>
        printer,

        /// <summary>
        /// Plugin is a scanner driver
        /// </summary>
        scanner,

        /// <summary>
        /// Plugin is a digital camera driver
        /// </summary>
        digitalCamera,

      }

        [AttributeUsage(AttributeTargets.Class)]
        public sealed class pluginTypeAttribute : Attribute
        {
          private pluginType _type;

          /// <summary>
          /// Initializes a new instance of the attribute.
          /// </summary>
          /// <param name="T">Value from the plugin types enumeration.</param>
          public pluginTypeAttribute(pluginType T) { _type = T; }

          /// <summary>
          /// Publicly accessible read only property field to get the value of the type.
          /// </summary>
          /// <value>The plugin type assigned to the attribute.</value>
          public pluginType type { get { return _type; } }
        }

我们在插件中搜索的自定义属性知道它是我们的

          public interface IPluginInterface
          {
            /// <summary>
            /// Defines the name for the plugin to use.
            /// </summary>
            /// <value>The name.</value>
            String name { get; }

            /// <summary>
            /// Defines the version string for the plugin to use.
            /// </summary>
            /// <value>The version.</value>
            String version { get; }

            /// <summary>
            /// Defines the name of the author of the plugin.
            /// </summary>
            /// <value>The author.</value>
            String author { get; }

            /// <summary>
            /// Defines the name of the root of xml packets destined
            /// the plugin to recognise as it's own.
            /// </summary>
            /// <value>The name of the XML root.</value>
            String xmlRootName { get; }

            /// <summary>
            /// Defines the method that is used by the host service shell to pass request data
            /// in XML to the plugin for processing.
            /// </summary>
            /// <param name="XMLData">String containing XML data containing the request.</param>
            /// <returns>String holding XML data containing the reply to the request.</returns>
            String processRequest(String XMLData);

            /// <summary>
            /// Defines the method used at shell startup to provide any one time initialisation
            /// the client will call this once, and once only passing to it a host interface pointing to itself
            /// that the plug shall use when calling methods in the IPluginHost interface.
            /// </summary>
            /// <param name="theHost">The IPluginHost interface relating to the parent shell program.</param>
            /// <returns><c>true</c> if startup was successfull, otherwise <c>false</c></returns>
            Boolean startup(IPluginHost theHost);

            /// <summary>
            /// Called by the shell service at shutdown to allow to close any resources used.
            /// </summary>
            /// <returns><c>true</c> if shutdown was successfull, otherwise <c>false</c></returns>
            Boolean shutdown();

          }

对于实际的插件界面。这需要由客户端应用程序和任何使用它的插件引用。

你会看到另外一个提到的接口,这是插件回调的主机接口,如果你不需要将它用于双向通信,那么你可以将其剥离,但万一需要它:

            public interface IPluginHost
            {
              /// <summary>
              /// Defines a method to be called by plugins of the client in order that they can 
              /// inform the service of any events it may need to be aware of.
              /// </summary>
              /// <param name="xmlData">String containing XML data the shell should act on.</param>
              void eventCallback(String xmlData);
            }

最后,要创建一个充当插件的DLL,使用单独的DLL项目,并在需要时引用接口,您可以使用以下命令:

            using System;
            using System.Collections.Generic;
            using System.Linq;
            using System.Text;
            using pluginInterfaces;
            using System.IO;
            using System.Xml.Linq;

            namespace pluginSkeleton
            {
              /// <summary>
              /// Main plugin class, the actual class name can be anything you like, but it MUST
              /// inherit IPluginInterface in order that the shell accepts it as a hardware driver
              /// module. The [PluginType] line is the custom attribute as defined in pluginInterfaces
              /// used to define this plugins purpose to the shell app.
              /// </summary>
              [pluginType(pluginType.printer)]
              public class thePlugin : IPluginInterface
              {
                private String _name = "Printer Plugin"; // Plugins name
                private String _version = "V1.0";        // Plugins version
                private String _author = "Shawty";       // Plugins author
                private String _xmlRootName = "printer"; // Plugins XML root node

                public string name { get { return _name; } }
                public string version { get { return _version; } }
                public string author { get { return _author; } }
                public string xmlRootName { get { return _xmlRootName; } }

                public string processRequest(string XMLData)
                {
                  XDocument request = XDocument.Parse(XMLData);

                  // Use Linq here to pick apart the XML data and isolate anything in our root name space
                  // this will isolate any XML in the tags  <printer>...</printer>
                  var myData = from data in request.Elements(this._xmlRootName)
                               select data;

                  // Dummy return, just return the data passed to us, format of this message must be passed
                  // back acording to Shell XML communication specification.
                  return request.ToString();
                }

                public bool startup(IPluginHost theHost)
                {
                  bool result = true;

                  try
                  {
                    // Implement any startup code here
                  }
                  catch (Exception ex)
                  {
                    result = false;
                  }

                  return result;
                }

                public bool shutdown()
                {
                  bool result = true;

                  try
                  {
                    // Implement any shutdown code here
                  }
                  catch (Exception ex)
                  {
                    result = false;
                  }

                  return result;
                }

              }// End class
            }// End namespace

通过一些工作,您应该能够调整所有这些以满足您的需要,最初编写的项目是针对dot net 3.5编写的,我们确实让它在Windows服务中工作。

答案 1 :(得分:2)

如果您在核心应用程序中使用基类或接口,那么您需要重建应用程序以及使用该类/接口的所有插件(如果更改)。所以你对此能做些什么?这里有一些想法(不一定是好的,但它们可能引发一些想法),你可以混合和放大匹配...

  • 将接口放在单独的共享程序集中,因此如果接口发生更改,您至少不需要重新编译核心应用程序。

  • 请勿更改任何界面 - 将它们固定在一起。而不是“版本”它们,所以如果你想改变界面,你就留下旧界面,只是暴露一个扩展或替换旧API的全新界面。这允许您逐渐弃用旧插件,而不是强制立即进行全局重建。这确实束缚了你的手,因为它需要对所有旧接口的完全向后兼容性支持,至少在你知道所有客户已经转移到所有组件的较新版本之前。但是你可以将它与不太频繁的“重新安装一切”版本结合起来,在这种情况下你可以打破向后兼容性,清除已经失效的界面并升级所有客户端程序集。

  • 查找所有插件不需要接口某些部分的接口,并将一些接口拆分为几个更简单的接口,以减少每个接口的依赖性/流失。

  • 正如您所建议的那样,将接口转换为运行时注册/发现方法,以最大限度地减少接口流失。接口越灵活和通用,扩展它们就越容易,而不会引入重大变化。例如,将数据/命令序列化为字符串格式,字典或XML,并以该形式传递,而不是调用显式接口。像XML或名称+值对的字典这样的数据驱动方法比接口更容易扩展,因此您可以开始支持新元素/属性,同时轻松保留向您传递旧格式的客户端的向后兼容性。您可以将接口设置为采用类型参数的单个方法,而不是PostMessage(msg)+ PostComment(msg):PostData(“Message”,msg)和PostData(“Comment”,msg) - 这样很容易支持新的类型,无需定义新接口。

  • 如果可能,请尝试定义预期未来预期功能的接口。因此,如果您认为有一天可能会添加RSS功能,那么请考虑一下如何工作,在界面中查看,但不提供任何支持。然后,如果你最终开始添加一个RSS插件,它已经有一个定义的API插入。当然,这仅适用于您定义足够灵活的接口,以便系统在实现时实际可用的接口!

  • 或者在某些情况下,您可以将依赖插件发送给所有客户,并使用许可系统启用或禁用其功能。然后你的插件可以相互依赖,但你的客户除非他们已经购买了它们,否则无法使用它们。

答案 2 :(得分:0)

如果你想要更通用,恕我直言,你应该在pugins上抽象UI层。 因此,用户与UI(如果其中有Plugin)公开的UI实际互动,与Comments一样,必须是Plugin的一部分定义。 Host容器必须提供一个空间,任何插件都可以推送任何他想要的东西。空间要求也可以是插件描述性清单的一部分。在这种情况下Host,基本上是:

  • 找到一个插件
  • 将其加载到内存中
  • 读取它需要多少和多少空间
  • 检查是否可以在此时刻提供指定的空间,如果是,则允许插件使用插件UI数据填充其界面。

之后或插件/用户交互是由插件本身完成的。

您可以或多或少地在Web开发或移动开发中找到横幅概念,例如在Android上定义您的应用UI布局。

希望这有帮助。