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

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

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

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

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