オブジェクト指向の肝で、それは【継承】を使うのが良いのか、それとも【委譲】にしたほうがいいのか、という話があります。結論を言えば、ケースバイケースなのですが、どうしても委譲でしか解決できないものもあります。
プラグインのパターンがそうで、とあるクラスの機能を強化しようとする場合、とあるクラスに手を加えずに強化する方法が【委譲】のパターン、インターフェースを作っておいて、それを動かすというパターンになります。
さて、C++ の場合は、プラグイン作りは、インターフェースとなる関数を定義しておいて、外部のDLLで定義しておけば良いので、結構簡単にできます。結構簡単に、と言うのは DLL 作りに慣れていればの話であって、非常に敷居の高いものでもあります。DLL の import/export の対象となる関数の型の変換やスタックの使い方の問題があって、ややこしいのですね。
これを統一化させるために、COM がある訳ですが、いちいち登録が必要なのと、COM を扱うのが相当手間(少なくとも生のC++でやるのは手間です)なので、避けたいプラグインです。VB6 あたりだと CreateObject 関数で作成すればよいのですが、このあたりは variant 型を扱える言語だからですね~、とかなんとか。
と、C# ではどうやるの?と思って調べて作ってみたのが以下です。
ネタ元は、先日買った「プログラミング .NET Framework 第3版」なのです。
まずはメイン関数です。
using System; using System.Reflection; using Sample; public class Program { public static void Main(string[] args ) { Console.WriteLine("plugin test"); Type per = null; // 動的に DLL をロードする Assembly asse = Assembly.LoadFrom("Person.dll"); foreach ( Type t in asse.GetExportedTypes() ) { Console.WriteLine("class: {0}", t.ToString()); // 内部で公開されているクラスで IPerson なものを探す if ( t.IsClass && typeof(IPerson).IsAssignableFrom(t) ) { per = t; break; } } if ( per == null ) { Console.WriteLine("Error: no interface"); return; } // IPerson のコンストラクタを使ってオブジェクトを作成 IPerson p = (IPerson)Activator.CreateInstance(per); p.Say("hello"); } }
何をやっているかわかり辛いですが、Person.dll というアセンブリを動的にロードしています。そして、この中にある IPerson というインターフェースを探して、見つかったら、IPerson::Say メソッドを実行しています。
このあたり、インターフェースを使わない場合はリフレクションを使うのですが、インターフェースを使ったほうが楽ですし、コンパイル時にチェックができるので、お奨めです。
この IPerson インターフェースは、次のコードです。
namespace Sample { public interface IPerson { void Say( string val ); } }
実に単純ですね。単純に委譲をするためだけのインターフェースです。
実際に動作するところの Person クラスは以下のコードです。
using System; namespace Sample { public class Person : IPerson { public void Say( string val ) { Console.WriteLine("in Person: {0}", val ); } } }
IPerson インターフェースを継承して Person クラスを作ります。メイン関数で IPerson::Say メソッドを呼び出したときには、この Say メソッドが呼び出されます。
さて、これをコンパイルするのはどうするかというと、こんな風です。
csc /t:library IPerson.cs csc /t:library /r:iperson.dll Person.cs csc /t:exe /r:iperson.dll main.cs
まず、iperson.dll だけを作ります。このアセンブリを、Person.cs と main.cs が読み込む訳です。当然なことですが、person.cs と main.cs の直接的な関係はありません。関係がないので、main.exe と person.dll は別々に開発することができるのです。
実行したのがこれです。
C:\masuda\alice>plugin plugin test class: Sample.Person in Person: hello
さて、person.cs とは別の personOther.cs を作ります。
using System; namespace Sample { public class PersonOther : IPerson { public void Say( string val ) { Console.WriteLine("in PersonOther: {0}", val ); } } }
のように PersonOther というクラスを作ります。そして、これをコンパイルする時に、person.dll が出力されるようにします。
csc /t:library /r:iperson.dll /out:Person.dll PersonOther.cs
いわゆる、DLL 名はそのままにして、中身をすげかえてしまうわけです。
そして、main.exe はそのままにして、実行すると、
C:\masuda\alice>plugin plugin test class: Sample.PersonOther in PersonOther: hello
な風にメッセージ(動作)が変わります。
まあ、本来は main 関数で使っている Person.dll の名前を変えるのが普通なんですけどね。さくっとアセンブリを変えるだけで、クラス名までも変えられてしまう(インターフェースが合っているのだから、これは妥当なんだけど)のが不思議なところです。
さて、書籍に載っていたのですが、この Assembly.LoadFrom メソッドの引数、ローカルファイルだけでなくて WEB サイトに置いてあるファイルも参照できます。つまり
Assembly asse = Assembly.LoadFrom("http://servername/assemblries/Person.dll");
のように、http プロトコルが使えるそうです。へぇッ!!! 試していないのですが、これって結構アレな機能ですよね。インターフェースをうまく使うと、一度インストールしてしまえば、アップデート無しで機能を追加してしまうことが可能なのです。勿論、オフラインの時にも動作するようにするためには、ちょっと工夫が必要ですが。