4

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

本质上,我们有一个由核心组件和许多插入其中的模块组成的系统。一些模块依赖于其他模块,但需要不时删除或替换其中一些依赖项,我想尽可能避免重新编译。

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

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

我正在考虑以下几点:

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

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

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

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

4

3 回答 3

4

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

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

  • 不要更改您的任何界面 - 将它们固定在石头上。取而代之的是“版本”它们,因此如果您想更改接口,请将旧接口保留在原处,只需公开一个扩展或替换旧 API 的全新接口。这使您可以逐渐弃用旧插件,而不是强制要求立即进行全局重建。这确实有点束缚你的手,因为它需要对所有旧接口的完全向后兼容性支持,至少在你知道你的所有客户已经转移到他们所有程序集的更新版本之前。但是您可以将其与不太频繁的“重新安装所有内容”版本结合起来,在该版本中破坏向后兼容性,清除失效的接口并升级所有客户端程序集。

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

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

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

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

于 2011-09-13T22:30:37.790 回答
2

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

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

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

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>();

实际的插件 DLL 是使用接口“IPlugininterface”和定义插件类型的枚举定义的:

      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();

          }

对于实际的插件接口。客户端应用程序和使用它的任何插件都需要引用它。

您会看到提到的另一个接口,这是插件回调的主机接口,如果您不需要将其用于 2 路通信,那么您可以将其删除,但以防万一:

            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 服务中工作。

于 2011-09-13T21:07:40.067 回答
0

如果您想尽可能地通用,恕我直言,您应该在 pugins 上抽象 UI 层。UI因此,用户与暴露的实际交互Plugin(如果有的UI话),就像 forComments必须是Plugin定义的一部分。Host容器必须提供一个空间,任何插件都可以推送他想要的任何东西。空间要求也可以是插件描述清单的一部分。在这种情况下主机,基本上:

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

之后或事件泵送/用户交互由插件本身进行。

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

希望这可以帮助。

于 2011-09-11T11:41:52.663 回答