.NET: はじめての MEF

Managed Extension Framework (MEF) は、いわゆる「プラグイン」とか「アドイン」とか呼ばれてる仕組みを .NET 上で簡単に実現するためのフレームワークで、.NET Framework 4 で新たに追加された。Visual Studio 2010 の「アドイン」は、この MEF を使用している。
同じような仕組みとして .NET Framework 3.5 で導入された Managed Add-in Framework (MAF) がすでにあるが、微妙に機能範囲が違っている。MEF は MAF よりも簡易な記述でプラグイン機構を実装できる一方、MAF にあるような、プラグインを別プロセスで実行したりアンロードしたりする機能はない。ちょっと検索してみたところ「MEF と MAF を併用することもできるよ」と書かれているページがいくつか見つかったが、僕自身 MAF を使ったことがないので詳細はわからない。

Hello World

以前の日記で、プラグイン用のインタフェースを使ってプラグイン機構を実装する方法について調べたことがあった。MEF でも同じようなものからはじめるのが簡単そうなので、ここから始めることにした。

とりあえず Visual Studio 上でソリューションを 1 つ作り、その中にアプリケーションのプロジェクトを 1 つと、ライブラリのプロジェクトを 2 つ作った。アプリケーション (HelloAddins) はプラグインを呼び出すホスト側、ライブラリのうち片方 (HelloAddins.Addin) はプラグインのための SPI/API を定義するためのもの。そして残りの HelloDateAddins は、読み込まれる側のプラグインを実装するものだ。

まずは HelloAddins.Addin プロジェクトで、プラグインが実装すべきインタフェースを書く。そして、ホスト側とプラグイン実装の両方で、「参照設定の追加」を使って HelloAddins.Addin プロジェクトへの参照を追加する。ここまでは前回と代わらない。

namespace HelloAddins.Addin
{
    public interface HelloAddin
    {
        string CreateMessage();
    }
}

次に、ホストアプリケーションの側で、1) 「参照設定の追加」で System.ComponentModel.Composition を追加し、2) プラグインインスタンスを受け取るためのフィールドの宣言と、3) プラグインを読み込ませるためのコードを書く。そこまでできたら、後は 4) 好きなようにプラグインを呼べばいい。

using System;
using System.Collections.Generic;

// ↓この 2 つを使うために参照設定の追加が必要
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;

using System.Windows.Forms;
using HelloAddins.Addin;

namespace HelloAddins
{
    public partial class Form1 : Form
    {
        private DirectoryCatalog catalog;
        private CompositionContainer container;

        // 2) プラグインのインスタンスを受け取るためのフィールドの宣言
        [ImportMany]
        private List<HelloAddin> addins;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            // 3) プラグインを読み込ませる
            catalog = new DirectoryCatalog("plugins");
            container = new CompositionContainer(catalog);
            container.ComposeParts(this);
        }

        private void button1_Click(object sender, EventArgs e)
        {
            this.listBox1.Items.Clear();
            
            // 4) 好きなようにプラグインを呼ぶ
            foreach (var addin in addins)
            {
                this.listBox1.Items.Add(addin.CreateMessage());
            }
        }
    }
}

上の例ではホストがたまたま Form のサブクラスだけど、もちろん実際には何でもいい。

で、上記の手順のうち、MEF の特徴となるのは 3) の部分だ。オブジェクトの定義を格納する「カタログ」のオブジェクトと、それにしたがってインスタンスの生成やフィールドへの注入を行う「コンテナ」を使用している。この 2 つを使うことで、プラグインの定義が取得され、[ImportMany] または [Import] が付けられたフィールドへの注入が行われる。
2 つに分割されているのにはもちろん意味がある。上の例では DirectoryCatalog を使用しているのでカレントディレクトリのサブディレクトリ "plugins" にある DLL からプラグインの実装を検索するようになっているが、この部分を書き換えることで、複数のディレクトリから探すとか、サーバを探しに行くとかいったように、プラグインの検索・取得手段を変更できるようになっている。便利ですね。たぶん。

で、最後に、プラグインの実装を作る。1) 「参照設定の追加」で System.ComponentModel.Composition を追加し、2) インタフェースを実装したクラスを作って外から呼ばれるようにする。

using System;

// ↓これを使うために参照設定の追加が必要
using System.ComponentModel.Composition;

using HelloAddins.Addin;

namespace HelloDateAddin
{
    // 2) インタフェースを実装したクラスを作って外から呼ばれるようにする
    [Export(typeof(HelloAddin))]
    public class HelloDate : HelloAddin
    {
        public string CreateMessage()
        {
            return "Hello " + DateTime.Now;
        }
    }

MEF を使わない場合は、インタフェースを実装するだけでよかったが、今回は [Export] を付けて「これ外から呼んでいいですよー」と明示する必要がある。さらに、引数の typeof(HelloAddin) は、ホスト側で [ImportMany] あるいは [Import] を書いたときの引数と、きっちり同じである必要がある。CompositionContainer がホストとプラグインを結びつける際に、この引数を使うからだ。今回はインタフェースを使っていて、しかもそれが 1 種類しかないからありがたみがないけど。

型で指定するのって、なんか他と衝突しそう……という場合は、自分で名前を付けることもできる。当然、プラグイン側でも同じ文字列を指定することになる(実際にはこれもSPI/APIで定数定義した方がいいかな)。

        // ホスト側
        [ImportMany("HelloAddins.Addin.Message")]
        private List<string> propertyValues;

ここで、デバッグのためにもうひと手間。ホスト側は「カレントディレクトリの下の "plugins"」を探しに行くので、プラグイン実装の DLL をビルドしたら、そこにコピーしないと動かせない。ビルドする度に手動でコピーするのは面倒なので、プラグイン実装のプロジェクトでプロパティを開き、「ビルドイベント」タブの「ビルド後に実行するアクション」のところに次のように書いておく。

mkdir "$(SolutionDir)\HelloAddins\bin\Debug\plugins\"
copy "$(TargetPath)" "$(SolutionDir)\HelloAddins\bin\Debug\plugins\"

プロパティやメソッドでプラグイン

ここからは、前回やってないこと。

クラスまるごと Export する代わりに、プロパティに付けることもできる。この場合、プロパティを宣言したクラスのインスタンスが作られた後に、そのプロパティ値が取得されてホスト側に注入される。

        // プラグイン側
        [Export("HelloAddins.Addin.Message")]
        public string Message { get; set; }

メソッドもプラグインすることができる。まず、API/SPI 用のライブラリに、デリゲートを宣言する。ホスト側ではそのデリゲートで受け取るようにしておき、プラグイン実装側ではデリゲートの宣言通りのメソッドを作ればいい。

    // 宣言
    public delegate string GetMessage(string s);
        // ホスト側
        [ImportMany(typeof(GetMessage))]
        private List<GetMessage> actions;
        // プラグイン側
        [Export(typeof(GetMessage))]
        public string GetMessage(string hello)
        {
            return "はろーあいらびゅーこいをしようよいぇいいぇい";
        }

実装側では、ひとつのクラスで複数のメソッドを Export することもできる。

再読み込み

プラグイン定義の読み込みに使った DirectoryCatalog は、再読み込みに対応している。これにはまず、ImportMany あるいは Import の引数で、AllowRecomposition プロパティを true にする。

        [ImportMany(AllowRecomposition = true)]
        private List<HelloAddin> addins;

そして、再読み込みをしたいタイミングで Refresh() メソッドを呼ぶ。

            catalog.Refresh();

すると、ディレクトリ内を読み直し、プラグインが追加されたり削除されたりしていないかチェックした上で、その結果を反映してくれる。上記の例では List を使っているけど、List の中に追加されたりするのではなく、新しい List が作られて、フィールドにセットしなおされる。ただし、List の中身はできるだけ前と同じにしてくれる。つまり、A、B、C の 3 つのプラグインがリストに入っている状態でディレクトリに新しい DLL が増えた場合、元の List に入っていた A、B、C のインスタンスはそのままで、作り直されたりしない。あくまで新たに D という要素が増えるだけだ。

System.IO.FileSystemWatcher と組み合わせれば、「所定のディレクトリに DLL を放り込むだけで自動で認識してプラグインを読み込む」なんていうアプリケーションも書けるわけだ。

後は……

だいたいわかった。と思うので、今回はこの辺で終わりにしておく。

他にどんな機能があるかドキュメントで見ると、メタデータを使ってもう少し細かい条件付けをするとか、.NET Framework 4 で追加された Lazy を使うことで初期化を遅延させたりすることができるらしい。

Managed Extensibility Framework
http://mef.codeplex.com/

これは何なのか

使ってみての感想というか。

MEF 関連で blog 記事やフォーラムの書き込みを見ると、「まるで DI のようですね」とか、場合によってはもっとはっきり「DI ですね」みたいなことが言われているようだ。確かに、メタデータをうまく使って、フレームワークに依存関係を注入させるという意味では、確かに DI だ。現状で DI が使われている箇所のうち、ある程度の割合は、MEF ですんなり置き換えてしまうこともできそうだし。

ただ、個人的には、これを「DI だ」と言い切ってしまうのは違う気がする。DI って、「オブジェクト」と「オブジェクト間の依存関係」を分離すると、オブジェクトの実装がシンプルになるし、依存関係を後から組み替えてキャッキャウフフできるからいいよね〜という話だったと思った。MEF を使う場合、依存関係をフレームワークが実行時に構築するわけだけど、どういう風に構築するかは、基本的にはホスト側が決めてしまう。つまり、依存関係の定義はアプリケーションの中に残りっぱなしになっている。ある条件に複数のプラグインが該当した場合にどれを使うのか、といった問題も、ホスト側がなんとかするものであって、MEF が何かしてくれるわけではない。動的に連結するだけで DI だというなら、リフレクションも DI だということになってしまう。

やはり、DI っぽさは一旦忘れて、Managed Extension Framework という名前が示すとおり、自分のアプリケーションを簡単に拡張できるようにしたいときに使うのが素直でよさそうに思う。

ちなみに MEF って、Java で言うとおおむね OSGi に相当する部分だと思う。Java が Project Jigsaw と OSGi でモメてる間に .NET Framework が横を追い抜こうとしているの図ですね。OSGi や Jigsaw の機能を MEF が全部カバーしているという意味じゃないですよ。ないですけど、やっぱりこう、Oracle さんガンバッテ!! ね!!