Шаблон плагина C # без интерфейсов - PullRequest
2 голосов
/ 11 сентября 2011

Я столкнулся с необходимостью реализации шаблона плагина, который не подходит ни к чему, что я видел в другом месте, и мне просто интересно, смотрю ли я на него неправильно или кто-то еще сталкивался с тем жепроблема и может иметь решение.

По сути, у нас есть система, которая состоит из сборки ядра и нескольких модулей, подключаемых к ней.Некоторые модули полагаются на другие модули, но возникла необходимость периодически удалять или заменять некоторые из этих зависимостей, и я хотел бы по возможности избегать перекомпиляции.

Система являетсяCMS на заказ и модули являются плагинами, обеспечивающими функции в CMS.Например, у нас есть модуль комментариев и несколько модулей контента, таких как модуль новостей, модуль блогов и т. Д., Которые могут включать функции комментирования.Моя проблема в том, что некоторые клиенты могут не приобрести модуль комментариев, поэтому мне нужно либо найти способ предотвратить зависимость зависимых модулей от наличия модуля комментариев, а в некоторых случаях, возможно, придется обслуживать модифицированную версиюмодуль комментариев.

Мы загружаем модули во время выполнения и в настоящее время, чтобы избежать взаимозависимостей между модулями, мы обрабатываем это с помощью интерфейсов, которые содержатся в базовой сборке CMS.Я обеспокоен тем, что для того, чтобы избежать необходимости изменять базовую сборку CMS каждый раз, когда мы создаем новые модули, в которых может существовать зависимость, мне нужно использовать что-то гораздо более свободное, чем интерфейсы и реализации этих интерфейсов.

Яс учетом следующего:

  • Базовая сборка содержит объект, который позволяет регистрировать и отменять регистрацию общих сообщений ввода / вывода (например, «Comments.AddComment» или «Comments.ListComments»)
  • Когда модули загружены, они объявляют об услугах, которые им требуются, и об услугах, которые они предоставляют (например, новостному модулю потребуется сообщение «Comments.AddComment», а любой вариант модуля комментариев будет содержать сообщение «Comments.AddComment»).,
  • Любые объекты или данные, которые передаются в эти сообщения, будут наследоваться от очень свободного базового класса или реализовывать интерфейс, который предоставляет свойство типа IDictionary, которое содержится в базовой сборке.В качестве альтернативы, для контракта на сообщение потребуется только параметр типа object, и я передаю в него анонимные объекты от поставщика / потребителя.

Недостатком является очевидная потеря строгой типизации, но плюс в том, чтоЯ не полагаюсь на строгую реализацию интерфейса и не требую включения модулей, которые могут не существовать во время выполнения.

Плагины загружаются через Reflection, проверяют ссылочные сборки и ищут классы, реализующие данный интерфейс.MEF и динамические типы не подходят, так как я ограничен .NET 3.5.

Может кто-нибудь предложить что-нибудь лучше или, возможно, другой способ решения этой проблемы?

Ответы [ 3 ]

2 голосов
/ 14 сентября 2011

Хорошо, немного покопался и нашел то, что искал.

ПРИМЕЧАНИЕ : это старый код, он не использует никаких шаблонов или чего-то в этом роде.Черт, он даже не в своем собственном объекте, но он работает :-) вам нужно адаптировать идею для работы так, как вы хотите.

Во-первых, это цикл, который получает все найденные DLL-файлы.в моем конкретном каталоге, в моем случае это было в папке с именем 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();

          }

Для фактического интерфейса плагина,На это должно ссылаться как клиентское приложение, так и любой плагин, который его использует.

Вы увидите еще один упомянутый интерфейс, это интерфейс хоста, к которому подключается плагин, если вы неВам не нужно использовать его для двухсторонней связи, тогда вы можете удалить его, но в случае, если это необходимо:

            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.

2 голосов
/ 14 сентября 2011

Вы правы, что если вы используете базовый класс или интерфейс в своем основном приложении, то вам нужно перестроить приложение и все плагины, которые используют этот класс / интерфейс, если оно изменится. Так что вы можете с этим поделать? Вот некоторые идеи (не обязательно хорошие, но они могут вызвать некоторые мысли), которые вы можете смешивать и сочетать ...

  • Поместите интерфейсы в отдельные общие сборки, чтобы вам по крайней мере не нужно было перекомпилировать основное приложение в случае изменения интерфейса.

  • Не меняйте ни один из ваших интерфейсов - держите их в камне. Вместо этого «версионируйте» их, поэтому, если вы хотите изменить интерфейс, вы оставляете старый интерфейс на месте и просто предоставляете совершенно новый интерфейс, который расширяет или заменяет старый API. Это позволяет вам постепенно отказаться от старых плагинов, вместо того чтобы требовать немедленной глобальной перестройки. Это несколько связывает вам руки, так как требует полной поддержки обратной совместимости для всех старых интерфейсов, по крайней мере, до тех пор, пока вы не узнаете, что все ваши клиенты перешли на более новые сборки всех своих сборок. Но вы можете комбинировать это с менее частым выпуском «переустановите все», где вы нарушаете обратную совместимость, очищаете устаревшие интерфейсы и обновляете все клиентские сборки.

  • Найдите интерфейсы, в которых некоторые части интерфейса не нужны всем плагинам, и разбейте некоторые интерфейсы на несколько более простых интерфейсов, чтобы уменьшить зависимости / отток на каждом интерфейсе.

  • Как вы предложили, преобразуйте интерфейсы в подход регистрации / обнаружения во время выполнения, чтобы минимизировать отток интерфейсов. Чем более гибкими и универсальными будут ваши интерфейсы, тем легче будет их расширять, не внося существенных изменений. Например, сериализуйте данные / команды в строковый формат, словарь или XML и передайте их в этой форме вместо вызова явных интерфейсов. Управляемый данными подход, такой как XML или словарь пар имя / значение, гораздо проще расширить, чем интерфейс, поэтому вы можете начать поддерживать новые элементы / атрибуты, одновременно сохраняя обратную совместимость для клиентов, которые передают вам более старый формат. Вместо PostMessage (msg) + PostComment (msg) вы можете обобщить интерфейс для одного метода, принимающего параметр типа: PostData («Message», msg) и PostData («Comment», msg) - таким образом, легко поддерживать новый типы без необходимости определять новые интерфейсы.

  • Если возможно, попытайтесь определить интерфейсы, которые ожидают ожидаемые будущие функции. Поэтому, если вы думаете, что однажды добавите возможность RSS, подумайте о том, как это может работать, добавьте интерфейс, но не предоставляйте никакой поддержки. Затем, если вы наконец-то добавите плагин RSS, у него уже есть определенный API для подключения. Конечно, это работает только в том случае, если вы определяете достаточно гибкие интерфейсы, чтобы они фактически могли использоваться системой при ее реализации!

  • Или, в некоторых случаях, вы можете отправить плагины для зависимостей всем своим клиентам и использовать систему лицензирования для включения или отключения их возможностей. Тогда ваши плагины могут зависеть друг от друга, но ваши клиенты не могут использовать средства, если они их не купили.

0 голосов
/ 11 сентября 2011

Если вы хотите быть как можно более универсальным, ИМХО, вам следует также абстрагировать слой пользовательского интерфейса поверх pugins.Таким образом, фактическое взаимодействие пользователя с UI, предоставляемым Plugin (если в нем есть UI), например, для Comments, должно быть частью определения Plugin.Контейнер Host должен обеспечивать пространство, куда любой плагин может протолкнуть все, что захочет.Требуемое пространство также может быть частью описания манифеста плагина.В этом случае Host, в основном:

  • находит плагин
  • загружает его в память
  • читает, сколько и какого пространства ему нужно
  • проверяет, может ли быть предоставлено указанное пространство в данный момент, если да, позволяет плагину заполнять свой интерфейс данными пользовательского интерфейса плагинов.

А после или при событии перекачка / взаимодействие с пользователем осуществляется самим плагином.

Эту идею вы можете найти, более или менее, в концепциях баннеров в веб-разработке или в разработке для мобильных устройств, например, для определения макета пользовательского интерфейса вашего приложения на Android.

Надеюсь, это поможет.

...