アリスはプラグインで強化する(Assembly.LoadFrom を使う)

オブジェクト指向の肝で、それは【継承】を使うのが良いのか、それとも【委譲】にしたほうがいいのか、という話があります。結論を言えば、ケースバイケースなのですが、どうしても委譲でしか解決できないものもあります。

プラグインのパターンがそうで、とあるクラスの機能を強化しようとする場合、とあるクラスに手を加えずに強化する方法が【委譲】のパターン、インターフェースを作っておいて、それを動かすというパターンになります。

さて、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 プロトコルが使えるそうです。へぇッ!!! 試していないのですが、これって結構アレな機能ですよね。インターフェースをうまく使うと、一度インストールしてしまえば、アップデート無しで機能を追加してしまうことが可能なのです。勿論、オフラインの時にも動作するようにするためには、ちょっと工夫が必要ですが。

カテゴリー: 開発, C# パーマリンク