オブジェクト指向の肝で、それは【継承】を使うのが良いのか、それとも【委譲】にしたほうがいいのか、という話があります。結論を言えば、ケースバイケースなのですが、どうしても委譲でしか解決できないものもあります。
プラグインのパターンがそうで、とあるクラスの機能を強化しようとする場合、とあるクラスに手を加えずに強化する方法が【委譲】のパターン、インターフェースを作っておいて、それを動かすというパターンになります。
さて、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 プロトコルが使えるそうです。へぇッ!!! 試していないのですが、これって結構アレな機能ですよね。インターフェースをうまく使うと、一度インストールしてしまえば、アップデート無しで機能を追加してしまうことが可能なのです。勿論、オフラインの時にも動作するようにするためには、ちょっと工夫が必要ですが。
