オブジェクト指向の肝で、それは【継承】を使うのが良いのか、それとも【委譲】にしたほうがいいのか、という話があります。結論を言えば、ケースバイケースなのですが、どうしても委譲でしか解決できないものもあります。
プラグインのパターンがそうで、とあるクラスの機能を強化しようとする場合、とあるクラスに手を加えずに強化する方法が【委譲】のパターン、インターフェースを作っておいて、それを動かすというパターンになります。
さて、C++ の場合は、プラグイン作りは、インターフェースとなる関数を定義しておいて、外部のDLLで定義しておけば良いので、結構簡単にできます。結構簡単に、と言うのは DLL 作りに慣れていればの話であって、非常に敷居の高いものでもあります。DLL の import/export の対象となる関数の型の変換やスタックの使い方の問題があって、ややこしいのですね。
これを統一化させるために、COM がある訳ですが、いちいち登録が必要なのと、COM を扱うのが相当手間(少なくとも生のC++でやるのは手間です)なので、避けたいプラグインです。VB6 あたりだと CreateObject 関数で作成すればよいのですが、このあたりは variant 型を扱える言語だからですね~、とかなんとか。
と、C# ではどうやるの?と思って調べて作ってみたのが以下です。
ネタ元は、先日買った「プログラミング .NET Framework 第3版」なのです。
まずはメイン関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | 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 インターフェースは、次のコードです。
1 2 3 4 5 6 7 | namespace Sample { public interface IPerson { void Say( string val ); } } |
実に単純ですね。単純に委譲をするためだけのインターフェースです。
実際に動作するところの Person クラスは以下のコードです。
1 2 3 4 5 6 7 8 9 10 11 12 | using System; namespace Sample { public class Person : IPerson { public void Say( string val ) { Console.WriteLine( "in Person: {0}" , val ); } } } |
IPerson インターフェースを継承して Person クラスを作ります。メイン関数で IPerson::Say メソッドを呼び出したときには、この Say メソッドが呼び出されます。
さて、これをコンパイルするのはどうするかというと、こんな風です。
1 2 3 | 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 は別々に開発することができるのです。
実行したのがこれです。
1 2 3 4 | C:\masuda\alice>plugin plugin test class: Sample.Person in Person: hello |
さて、person.cs とは別の personOther.cs を作ります。
1 2 3 4 5 6 7 8 9 10 11 12 | using System; namespace Sample { public class PersonOther : IPerson { public void Say( string val ) { Console.WriteLine( "in PersonOther: {0}" , val ); } } } |
のように PersonOther というクラスを作ります。そして、これをコンパイルする時に、person.dll が出力されるようにします。
1 | csc /t:library /r:iperson.dll /out:Person.dll PersonOther.cs |
いわゆる、DLL 名はそのままにして、中身をすげかえてしまうわけです。
そして、main.exe はそのままにして、実行すると、
1 2 3 4 | C:\masuda\alice>plugin plugin test class: Sample.PersonOther in PersonOther: hello |
な風にメッセージ(動作)が変わります。
まあ、本来は main 関数で使っている Person.dll の名前を変えるのが普通なんですけどね。さくっとアセンブリを変えるだけで、クラス名までも変えられてしまう(インターフェースが合っているのだから、これは妥当なんだけど)のが不思議なところです。
さて、書籍に載っていたのですが、この Assembly.LoadFrom メソッドの引数、ローカルファイルだけでなくて WEB サイトに置いてあるファイルも参照できます。つまり
1 | Assembly asse = Assembly.LoadFrom( "http://servername/assemblries/Person.dll" ); |
のように、http プロトコルが使えるそうです。へぇッ!!! 試していないのですが、これって結構アレな機能ですよね。インターフェースをうまく使うと、一度インストールしてしまえば、アップデート無しで機能を追加してしまうことが可能なのです。勿論、オフラインの時にも動作するようにするためには、ちょっと工夫が必要ですが。