C#でのPlugin機能の実装
はじめに
前回の「Plugin Architecture」って何だろうか?でまとめを書いたが、それだけでは分かった気になってしまうので、実際に手を動かしてみる。
今回はプログラミング言語の機能としてPluginの実装をするのではなく、アプリケーションレベルで『Plugin機能』を実装していく。
前置き
何はともあれ、コードを見せろ!という方はこちらから。
動作確認環境
- Windows 10 Pro
- .NET Core 2.1
アプリケーションの構成
『参考』に載っているアプリケーションはGUIだが、手っ取り早く実装を試してみたかったので、CLIを作成。
読み込んだプラグイン(.dll)の内容に応じて、メニューを表示。対象のメニュー(プラグイン)を選択すると、それに応じた処理を画面に表示する簡単なもの。
インターフェース
拡張点になるインフェースはCLIということで非常にシンプル。
namespace PluginComponent.Plugin { /// <summary> /// Plugin Interface /// </summary> public interface IPlugin { /// <summary> Plugin name </summary> string Name { get; } /// <summary> Execute </summary> void DoAction(); } }
Name
は画面に表示されるメニューで、DoAction()
がメニュー選択時の実処理を表している。
プラグイン作成時は、このインターフェースを継承する。
(今回は何も考えずにvoid
かつ引数なしにしているが、コアシステムからの入力とコアシステムへの返却のIFも考えておいた方がよい。)
プラグイン
プラグインの実装もかなりシンプル。
名前と実処理を書いてしまえば、実装自体は終了。
プラグインを書いていて思ったのは、拡張点の細分化はやっぱり必須だなということ。
同じインターフェースを使って、『AのプラグインではDBアクセス、Bのプラグインでは..』みたいにやってしまうとコアシステム側で前処理や後処理が煩雑化してしまう。
using static System.Console; using PluginComponent.Plugin; namespace FirstPlugin { /// <summary> /// FirstPlugin implements IPlugin. /// </summary> public class FirstPlugin : IPlugin { /// <summary> Plugin Name </summary> public string Name { get; } = "First"; /// <summary> Execute </summary> public void DoAction() { WriteLine("I`m First Plugin."); } } }
コアシステム
最も重要になるのは、SamplePlugin/Main/PluginLoader.cs。
こいつが動的に.dll(プラグイン)を読み込み、インスタンスを作成している。
Pluginsフォルダに存在するファイル(拡張子が.dll)のモノを読み込んで、アセンブリをロードする。
アセンブリがロード出来たら、アセンブリのTypeを取得して、interfaceとabstractを除外していく。
そして、plugins
リストにAddする前にIPlugin
を継承しているか確認を行う。
(ここで上記を除外しておかないと、インスタンス生成時にErrorが発生する。)
対象クラスの選別が出来たら、Activator
を使って、Typeからインスタンスを動的に生成する。
後は生成されたインスタンスをアップキャストして、呼び出し元に返す。
using PluginComponent.Plugin; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.Loader; namespace SamplePlugin.Main { /// <summary> /// Plugin Loader /// </summary> public static class PluginLoader { /// <summary> /// Load Plugin For Plugin directory /// </summary> /// <param name="path">Plugin directory path</param> /// <returns>load assembly</returns> public static ICollection<IPlugin> LoadPlugins(string path) { var plugins = new List<IPlugin>(); if (Directory.Exists(path)) { var dlls = Directory.GetFiles(path, "*.dll") .Select(p => Path.GetFullPath(p)).ToArray(); var pluginTypes = new List<Type>(); foreach(var dll in dlls) { var assm = AssemblyLoadContext.Default.LoadFromAssemblyPath(dll); if (assm != null) { var types = assm.GetTypes(); foreach(var type in types) { if (type.IsInterface || type.IsAbstract) { continue; } else { // implements IPlugin interface if (type.GetInterface(typeof(IPlugin).FullName) != null) { pluginTypes.Add(type); } } } } } pluginTypes.ForEach(pType => { var plugin = (IPlugin)Activator.CreateInstance(pType); plugins.Add(plugin); }); return plugins; } return plugins; } } }
さいごに
この程度であれば、『プラグイン簡単じゃん!』ってなりそうだが、前処理・後処理を考慮した拡張点の細分化、プラグイン入出力のIFの設計はかなり骨が折れそう。
要件を詰めてかつ、実装レベルの設計もある程度理解してないと、実装者が痛い目を見る典型だなという印象。