没有接口的c#插件模式

本文关键字:插件 模式 接口 | 更新日期: 2023-09-27 18:08:08

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

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

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

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

我正在考虑以下事项:

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

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

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

有谁能提出更好的建议,或者可能是考虑这个问题的不同方式吗?

没有接口的c#插件模式

你是对的,如果你在核心应用中使用基类或接口,那么如果它发生变化,你需要重建应用和所有使用该类/接口的插件。那么你能做些什么呢?这里有一些想法(不一定是好的,但它们可能会激发你的一些想法),你可以混合使用。比赛…

  • 把接口放在单独的共享程序集中,这样如果接口发生变化,你至少不需要重新编译核心应用。

  • 不要改变你的任何接口——保持它们固定不变。相反,"版本化"它们,所以如果您想更改接口,您可以保留旧的接口,而只是公开一个扩展或替换旧API的全新接口。这允许您逐渐弃用旧插件,而不是强制立即进行全局重建。这在一定程度上束缚了您的手脚,因为它要求对所有旧接口提供完全的向后兼容性支持,至少在您知道所有客户端都已迁移到所有程序集的新版本之前是这样。但是你可以把它和一个不那么频繁的"重新安装一切"的版本结合起来,在那里你打破了向后兼容性,清除了失效的接口,升级了所有的客户端程序集。

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

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

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

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

好的,我做了一些调查,找到了我要找的东西。

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

首先,是一个循环,得到在一个特定目录中找到的所有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>();

实际的插件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();
          }

实际的插件接口。这需要被客户端应用和任何使用它的插件引用。

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

            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

通过一些工作,你应该能够适应所有这一切来做你需要的,最初的项目是为。net 3.5编写的,我们确实让它在windows服务中工作。

如果你想要更通用,我认为你应该在插件上抽象UI层。因此,用户与Plugin暴露的UI的实际交互(如果其中有任何UI),就像Comments一样,必须是Plugin定义的一部分。Host容器必须提供一个空间,任何插件都可以推他想要的任何东西。空间需求也可以是插件描述清单的一部分。在本例中,主机基本上是:

  • 找到插件
  • 将其加载到内存
  • 读取它需要多少和什么样的空间
  • 检查是否可以提供指定的空间,如果是,允许插件用插件的UI数据填充其接口。

事件泵送/用户交互由插件本身完成。

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