4

他の場所で見たものに適合しないプラグインパターンを実装する必要があり、それを間違った方法で見ているのか、他の誰かが同じ問題に遭遇した可能性があるのか​​疑問に思っています。解決策があります。

基本的に、コアアセンブリと、それに接続する多数のモジュールで構成されるシステムがあります。一部のモジュールは他のモジュールに依存していますが、これらの依存関係の一部を時々削除または置換する必要が生じているため、可能な限り再コンパイルを避けたいと思います。

システムは特注のCMSであり、モジュールはCMS内の機能を提供するプラグインです。たとえば、コメントモジュールと、コメント機能を含めることができるニュースモジュール、ブログモジュールなどのいくつかのコンテンツモジュールがあります。私の問題は、一部の顧客がコメントモジュールを購入しない可能性があることです。そのため、依存モジュールがコメントモジュールの存在に依存しないようにする方法を見つける必要があり、場合によっては、の変更バージョンに対応する必要があります。コメントモジュール。

実行時にモジュールをロードしています。現在、モジュール間の相互依存を回避するために、コアCMSアセンブリに保持されているインターフェイスを使用してこれを処理しています。私の懸念は、依存関係が存在する可能性のある新しいモジュールを作成するたびにコアCMSアセンブリを変更する必要がないようにするために、インターフェイスやそれらのインターフェイスの実装よりもはるかに緩いものを使用する必要があることです。

私は次のことを考えています:

  • コアアセンブリには、共有入出力メッセージの登録と登録解除を可能にするオブジェクトが含まれています(たとえば、「Comments.AddComment」または「Comments.ListComments」)。
  • モジュールが読み込まれると、必要なサービスと提供するサービスがアドバタイズされます(たとえば、ニュースモジュールには「Comments.AddComment」メッセージが必要であり、コメントモジュールのバリアントには「Comments.AddComment」メッセージが表示されます)。
  • これらのメッセージに渡されるオブジェクトまたはデータは、非常に緩い基本クラスから継承するか、コアアセンブリ内に含まれるタイプIDictionaryのプロパティを公開するインターフェイスを実装します。または、メッセージのコントラクトにはオブジェクト型のパラメーターのみが必要であり、プロバイダー/コンシューマーから匿名オブジェクトを渡します。

欠点は明らかに強い型付けを失うことですが、プラスは、厳密なインターフェイスの実装に依存しないこと、または実行時に存在しない可能性のあるモジュールを含める必要がないことです。

プラグインはReflectionを介してロードされ、参照されるアセンブリをチェックし、特定のインターフェイスを実装するクラスを探します。.NET 3.5に制限されているため、MEFと動的型はオプションではありません。

誰かがこの問題についてもっと良いこと、またはおそらく別の考え方を提案できますか?

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 ファイルを取得するループです。私の場合、これはアプリ インストール フォルダーの下の「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();

          }

実際のプラグイン インターフェイス用。これは、クライアント アプリとそれを使用するプラグインの両方で参照する必要があります。

言及されているもう 1 つのインターフェイスが表示されます。これは、プラグインがコールバックするためのホスト インターフェイスです。双方向通信に使用する必要がない場合は、それを取り除くことができますが、必要な場合に備えて:

            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

できるだけ一般的になりたい場合は、私見では、UIレイヤーもpuginsで抽象化する必要があります。そのため、定義の一部である必要があるなど、ユーザーとの実際のやり取り(存在する場合)UIによって公開されます。コンテナーは、任意のプラグインが必要なものをプッシュできるスペースを提供する必要があります。スペース要件は、プラグイン記述マニフェストの一部にすることもできます。この場合、ホストは基本的に次のようになります。PluginUICommentsPluginHost

  • プラグインを見つける
  • それをメモリにロードします
  • 必要なスペースの量と種類を読み取ります
  • 指定されたスペースをこの瞬間に提供できるかどうかを確認し、提供できる場合は、プラグインがインターフェイスにプラグインの UI データを入力できるようにします。

そして、イベントのポンピング/ユーザーとのやり取りの後、プラグイン自体によって行われます。

このアイデアは、多かれ少なかれ、Web 開発やモバイル開発のバナーの概念 (Android でアプリの UI レイアウトを定義するなど) で見つけることができます。

お役に立てれば。

于 2011-09-11T11:41:52.663 に答える