次の方法で共有



March 2011

Volume 26 Number 03

Cutting Edge - アプリケーションの拡張: MEF と IoC の違い

Dino Esposito | March 2011

image: Dino Esposito「アプリケーションを構成するあらゆる部分を実行時に検出できる、拡張可能なアプリケーションを作成する方法はあるだろうか」というのは、終わることのない質問ですが、Microsoft .NET Framework 4 には、この問いに適切な答えを提供するために設計された新しいコンポーネントがあります。

Glenn Block が、2010 年 2 月号の記事「Managed Extensibility Framework による .NET 4 で構成可能なアプリケーションの構築」(msdn.microsoft.com/magazine/ee291628) で解説しているように、Managed Extensibility Framework (MEF) を使用すると、構成可能なプラグイン ベースのアプリケーションの構築を最適化できます。私は、1994 年からこの問題に取り組み始めた開発者として (これは、開発者として初めての本格的な挑戦でした)、この問題領域へのソリューションの提案は心から歓迎します。

MEF は、ライブラリを追加で購入、ダウンロード、参照しなくても使用でき、サードパーティが既存のアプリケーションに行う汎用性のある機能拡張についての問題に特化していることから、簡単なプログラミング インターフェイスが提供されています。Glenn の記事は MEF を知る第一歩としてたいへん優れているため、プラグイン ベースのアプリケーションについて考えている場合は必読です。

今月のコラムでは、アプリケーションの主要本体と外部部品を結びつけるための基盤として MEF を使用し、拡張可能なアプリケーションを構築するのに必要な手順について順を追って見ていきます。

IoC から MEF へ、そしてまた IoC へ

ですが、サンプル アプリケーションに進む前に、MEF と、よく使用されるもう 1 つのフレームワークのファミリ、制御の反転 (IoC: Inversion of Control) について、いくつか見解を共有したいと思います。

ひと言で言えば、MEF の機能と典型的な IoC フレームワークの機能に重なり合う部分があるというのは間違いではありませんが、まったく一致しているわけではありません。ほとんどの IoC フレームワークでは、MEF がサポートしないタスクを実行できます。おそらく、機能豊富な IoC コンテナーを使用したり、少しの労力を費やすことで MEF 固有の機能の一部のエミュレーションを行ったりすることができます。このことを受け、クラスや日常業務で MEF について言及する際によく質問されるのが、MEF と IoC ツールの違いはどこにあり、実際に MEF を必要とするのはどのようなときか、という点です。

MEF は、その中核において、まさに .NET Framework にとって適切になるように構築された IoC フレームワークであるというのが、私の考えです。今日広く使用されている IoC フレームワークの多くほど強力ではありませんが、代表的な IoC コンテナーの基本的なタスクを非常に適切に実行することができます。

現在、IoC フレームワークには、3 つの代表的機能があります。1 つ目に、オブジェクトのグラフのファクトリとして機能して、オブジェクト関係と依存関係のチェーンをたどって操作することで、登録済みの必要な型のインスタンスを作成することできます。2 つ目に、IoC フレームワークは、作成されるインスタンスの有効期間を管理して、キャッシュ機能やプール機能を提供できます。3 つ目に、ほとんどの IoC フレームワークはインターセプトをサポートしており、特定の型のインスタンス周辺に動的なプロキシを作成し、開発者がメソッド実行の事前処理および事後処理を実行できるようにします。私が執筆した 1 月号の記事 (msdn.microsoft.com/magazine/gg535676) では、Unity 2.0 のインターセプトについて扱っています。

ある意味では、MEF はオブジェクトのグラフのファクトリとして機能することができます。つまり、実行時に解決する必要があるクラスのメンバーを認識して処理できます。また MEF は、インスタンスのキャッシュを最小限しかサポートしません。キャッシュ機能があるのは確かですが、いくつかの別の IoC フレームワークほど機能豊富ではありません。最後の 3 つ目に関しては、.NET Framework 4 に同梱されているバージョンの MEF にはインターセプト機能がまったくありません。

では、どのようなときに MEF を使用すればよいのでしょう。IoC フレームワークを使ったことがなく、依存関係の挿入を少し追加してシステムのデザインを単にクリーンアップするだけであれば、MEF が簡単な糸口になります。これによって目的を簡単に達成できるなら、IoC フレームワークではなく MEF をお勧めします。

一方、1 つ以上の IoC フレームワークを何年も操作したことがあり、そこから機能をいくらでも引き出すことができる方は、おそらく MEF が役に立つことはないでしょう。ただ、MEF が持つ、一致する型を見つけるためにさまざまな種類のカタログをスキャンできる機能は役に立つ場合がありますが、StructureMap (structuremap.net/structuremap/ScanningAssemblies.htm、英語) などのいくつかの IoC フレームワークは、あるインターフェイスの特定の型や実装を見つけるためにディレクトリとアセンブリをスキャンするという機能を既に備えています。ただし MEF では、StructureMap (と、その他いくつかのフレームワーク) よりも、おそらく簡単でより直接的にこれを実行できます。

つまり、最初に考えるべき点は、汎用性のある機能拡張を求めているかどうかです。これに該当する場合は、MEF を検討する必要があります (依存関係、シングルトン、およびインターセプトを扱う必要がある場合は IoC ツールに MEF を追加することになります)。汎用性を求めていない場合は、MEF も同様に対処できる基本的なニーズがない限り、IoC フレームワークを使用するのが最適なアプローチです。すべての条件が同じであるなら、MEF は .NET Framework 用に構築されており追加の依存関係を必要としないため、IoC フレームワークよりも適切です。

MEF と拡張可能なアプリケーション

MEF は、拡張可能なアプリケーションを構築する際に便利ですが、この構築において最も難しい部分は、拡張できるようにアプリケーションを設計することです。これは設計の問題であって、MEF や IoC などのテクノロジとはあまり関係がありません。特に、アプリケーションのどの部分でプラグインを利用できるようにするかを考えなければなりません。

プラグインは、表示要素であることがほとんどで、メイン アプリケーションの UI の操作、メニューの追加や拡張、ウィンドウの作成、ダイアログ ボックスの表示、メイン ウィンドウの追加やサイズ変更などが必要です。独自のアプリケーションのプラグインにどのような構想を持っているかによって、プラグインと共有する情報量は、単にビジネス データ (本質的には、アプリケーションの現在状態のセグメント) から構成できる場合もあれば、コンテナー、メニュー、ツール バー、特定のコントロールなどの表示要素への参照から構成できる場合もあります。この情報は、データ構造にグループ化して、初期化時にプラグインに渡します。プラグインは、この情報に基づいて、自身の UI を調整して、固有のカスタム ロジックを追加で実装できる必要があります。

次に、プラグインのインターフェイスについて考える必要があります。このインターフェイスは、メイン アプリケーションで特定した挿入ポイントによって決まります。"挿入ポイント" とは、アプリケーションのコードの中で、プラグインを呼び出し、プラグインに開始および動作する機会を与える場所のことです。

挿入ポイントの例として、エクスプローラーを見てみましょう。ご存知のように、エクスプローラーは、シェル拡張によって UI を拡張できます。これらのプラグインは、ユーザーが右クリックして選択したファイルのプロパティを表示するときなど、非常に具体的な瞬間に呼び出されます。こうした挿入ポイントと、登録済みのプラグインにその時点で渡すデータを特定するのは、アプリケーションのアーキテクトの役割です。

設計のすべての側面が明確になれば、プラグインベースのアプリケーションを構築するタスクを簡略化できるフレームワークに目を向けることができます。

プラグインベースのサンプル アプリケーション

数字を見つけるアプリケーションといった簡単なものでも、プラグインを使用すれば、豊富で魅力的な機能を備えたアプリケーションにすることができます。図 1 は、このアプリケーションの基本的な UI です。アプリケーションの SDK を定義するためには、別個のプロジェクトを作成することになります。プラグインを実装するのに必要なすべてのクラスとインターフェイスを定義する場所はクラス ライブラリです。図 2 に例を示します。

簡単なサンプル アプリケーション

図 1 簡単なサンプル アプリケーション

図 2 アプリケーション SDK の定義

public interface IFindTheNumberPlugin {
  void ShowUserInterface(GuessTheNumberSite site);
  void NumberEntered(Int32 number);
  void GameStarted();
  void GameStopped();
}

public interface IFindTheNumberApi {
  Int32 MostRecentNumber { get; }
  Int32 NumberOfAttempts { get; }
  Boolean IsUserPlaying { get; }
  Int32 CurrentLowerBound { get; }
  Int32 CurrentUpperBound { get; }
  Int32 LowerBound { get; }
  Int32 UpperBound { get; }
  void SetNumber(Int32 number);
}

public class FindTheNumberFormBase : Form, IFindTheNumberApi {
  ...
}

すべてのプラグインは、IFindTheNumberPlugin インターフェイスを実装する必要があります。メイン アプリケーション フォームは、プラグインに情報を渡すのに便利なパブリック ヘルパー メンバーのリストを定義する、指定されたフォーム クラスから継承します。

IFindTheNumberPlugin から推測できるように、登録済みのプラグインは、アプリケーションがその UI を表示したとき、ユーザーが数字の推測を新しく試みたとき、そしてゲームが開始および停止されたときに呼び出されます。GameStarted と GameStopped は単なる通知メソッドで、入力はまったく必要ありません。NumberEntered は、ユーザーが新しい試行のために入力して送信した数値を取り込む通知です。最後に、ShowUserInterface は、プラグインをウィンドウに表示する必要があるときに呼び出されます。この場合、図 3 で定義しているように、サイト オブジェクトを渡します。

図 3 プラグイン用のサイト オブジェクト

public class FindTheNumberSite {
  private readonly FindTheNumberFormBase _mainForm;

  public FindTheNumberSite(FindTheNumberFormBase form) {
    _mainForm = form;
  }

  public T FindElement<T>(String name) where T:class { ... }
  public void AddElement(Control element) { ... }

  public Int32 Height {
    get { return _mainForm.Height; }
    set { _mainForm.Height = value; }
  }

  public Int32 Width { ... }
  public Int32 NumberOfAttempts { ... }
  public Boolean IsUserPlaying { ... }
  public Int32 LowerBound { ... }
  public Int32 UpperBound { ... }
  public void SetNumber(Int32 number) { ... }
}

サイト オブジェクトは、プラグインとホスト アプリケーションとの接点を表します。プラグインは、ホスト状態をいくらか参照したり、ホスト UI を変更したりできる必要がありますが、ホスト内部の詳細情報を認識することはありません。このため、プラグイン プロジェクトが参照する必要がある、中間のサイト オブジェクト (SDK アセンブリの一部) を作成する必要があります。

図 2 では、簡潔さのため、ほとんどのメソッドの実装を省略しましたが、サイト オブジェクトのコンストラクターは、アプリケーションのメイン ウィンドウへの参照を受け取り、(メイン ウィンドウ オブジェクトが公開する) 図 1 のヘルパー メソッドを使用して、アプリケーションの状態と表示要素の読み取りと書き込みが可能です。たとえば、Height メンバーは、プラグインがホスト ウィンドウの高さの読み取りと書き込みを行う方法を示します。

具体的には、FindElement メソッドは、(サンプル アプリケーション内の) プラグインがフォーム内の特定の表示要素を取得できるようにします。開発者は、SDK の一部として、ツール バーやメニューなどの特定のコンテナーにアクセスする方法についての技術的な詳細をいくつか公開する必要があります。このような簡単なアプリケーションでは、物理コントロールの ID を公開します。FindElement の実装を次に示します。

public T FindElement<T>(String name) where T:class {
  var controls = _mainForm.Controls.Find(name, true);
  if (controls.Length == 0)
    return null;
  var elementRef = controls[0] as T;
  return elementRef ?? null;
}
With the design of the application’s extensibility model completed, we’re now ready to introduce the MEF.

プラグインのインポートを定義する

メイン アプリケーションは、現在登録済みのすべてのプラグインを列挙するプロパティを必ず公開します。以下に例を示します。

public partial class FindTheNumberForm : 
  FindTheNumberFormBase {
  public FindTheNumberForm() {
    InitializeMef();
    ...
 }

 [ImportMany(typeof(IFindTheNumberPlugin)]
 public List<IFindTheNumberPlugin> Plugins { 
    get; set; 
  }
  ...
}

MEF の初期化とは、使用予定のカタログと、エクスポート プロバイダー (省略可能) を指定することで、構成のコンテナーを準備することを意味します。プラグインベースのアプリケーションでよく使用されるソリューションは、固定フォルダーからプラグインを読み込む方法です。図 4 は、今回の例における MEF のスタートアップ コードを示しています。

図 4 MEF の初期化

private void InitializeMef() {
  try {
    _pluginCatalog = new DirectoryCatalog(@"\My App\Plugins");
    var filteredCatalog = new FilteredCatalog(_pluginCatalog, 
      cpd => cpd.Metadata.ContainsKey("Level") && 
      !cpd.Metadata["Level"].Equals("Basic")); 

    // Create the CompositionContainer with the parts in the catalog
    _container = new CompositionContainer(filteredCatalog);
    _container.ComposeParts(this);
  }
  catch (CompositionException compositionException) {
    ...
  }
  catch (DirectoryNotFoundException directoryException) { 
    ...
  }
}

使用できるプラグインをグループ化するには DirectoryCatalog を、選択したプラグインの一部をフィルターにかけるには FilteredCatalog クラスを使用します。FilteredCatalog クラスは MEF にはありませんが、bit.ly/gf9xDK (英語) の MEF ドキュメントに例が示されています。具体的には、すべての読み込み可能なプラグインにレベルを示すメタデータ属性を含めることを要求できます。属性がないプラグインは無視します。

ComposeParts を呼び出すと、アプリケーションの Plugins プロパティが設定されます。その後は、さまざまな挿入ポイントからプラグインを呼び出すだけです。次のように、UI を変更する機会を与えるために、プラグインを最初に呼び出すのはアプリケーションの読み込み直後にします。

void FindTheNumberForm_Load(Object sender, EventArgs e) {
  // Set up UI
  UserIsPlaying(false);

  // Stage to invoke plugins
  NotifyPluginsShowInterface();
}

void NotifyPluginsShowInterface() {
  var site = new FindTheNumberSite(this);
  if (Plugins == null)
    return;

  foreach (var p in Plugins) {
    p.ShowUserInterface(site);
  }
}

同様の呼び出しは、ユーザーが新しいゲームを開始したとき、現在実行中のゲームを終了したとき、または未知の数字を新しく推測しようとしたときを通知するイベントのハンドラーで行われます。

サンプルのプラグインを作成する

プラグインは、アプリケーションの拡張性インターフェイスを実装する単なるクラスです。図 1のアプリケーションの興味深いプラグインとして、ユーザーがこれまでに行った推測回数を表示するプラグインについて考えてみましょう。推測回数は、アプリケーションのビジネス ロジックによって追跡され、サイト オブジェクトを通じてプラグインに公開されます。すべてのプラグインに必要なことは、固有の UI を準備して、試行回数にバインドし、メイン ウィンドウにアタッチすることです。

サンプル アプリケーションのプラグインは、メイン ウィンドウの UI に新しいコントロールを作成します。図 5 にサンプル プラグインを示します。

図 5 カウンターのプラグイン

[Export(typeof(IFindTheNumberPlugin))]
[PartMetadata("Level", "Advanced")]
public class AttemptCounterPlugin : IFindTheNumberPlugin {
  private FindTheNumberSite _site;
  private Label _attemptCounterLabel;

  public void ShowUserInterface(FindTheNumberSite site) {
    _site = site;
    var numberToGuessLabelRef = _host.FindElement<Label>("NumberToGuess");
    if (numberToGuessLabelRef == null)
      return;

    // Position of the counter label in the form 
    _attemptCounterLabel = new Label {
      Name = "plugins_AttemptCounter",
      Left = numberToGuessLabelRef.Left,
      Top = numberToGuessLabelRef.Top + 50,
      Font = numberToGuessLabelRef.Font,
      Size = new Size(150, 30),
      BackColor = Color.Yellow,
      Text =  String.Format("{0} attempt(s)", _host.NumberOfAttempts)
    };
    _site.AddElement(_attemptCounterLabel);
  }

  public void NumberEntered(Int32 number = -1) {
    var attempts = _host.NumberOfAttempts;
    _attemptCounterLabel.Text = String.Format("{0} attempt(s)", attempts);
    return;
  }

  public void GameStarted() {
    NumberEntered();
  }

  public void GameStopped() {
  }
}

プラグインは、新しい Label コントロールを作成して、既存の UI 要素の直下に配置します。次に、新しい数字が入力されたという通知を受け取るたびに、ビジネス ロジックの状態に応じて、現在の試行回数を示すカウンターを更新します。図 6 は、動作中のプラグインです。

サンプル アプリケーションといくつかのプラグイン

図 6 サンプル アプリケーションといくつかのプラグイン

コラムのプラグイン

結局、拡張可能なアプリケーションを設計するうえで最も難しいタスクは、ホストとプラグインとのインターフェイスを設計することです。これは純粋な設計タスクで、機能リストやユーザー要件に関係します。

しかし、実装に関して言えば、プラグインの選択、読み込み、検証などの、プラグイン インターフェイスにかかわらず実現する必要がある、実践的なタスクがいくつもあります。この件に関しては、読み込むプラグインのカタログ作成を簡略化したり、IoC フレームワークとほぼ同じようにプラグインを自動的に読み込んだりすることによって、MEF が強力にサポートします。

MEF の開発は継続中で、mef.codeplex.com (英語) から、最新情報、ドキュメント、およびサンプル コードを参照することができます。

Dino Esposito は、『Programming ASP.NET MVC』(Microsoft Press、2010 年) の著者で、『Architecting Applications for the Enterprise』(Microsoft Press、2008 年) の共著者でもあります。Esposito はイタリアに在住し、世界各国で開催される業界のイベントで頻繁に講演しています。ブログは weblogs.asp.net/despos (英語) で読むことができます。

この記事のレビューに協力してくれた技術スタッフの Glenn Block に心より感謝いたします。