CLR
MEF の構成に属性を使用しない方法
Managed Extensibility Framework (MEF) は、Microsoft .NET Framework 開発者が疎結合型アプリケーションを簡単に構築できるようにすることを目的に設計されています。MEF バージョン 1 の主目的は拡張性で、アプリケーション開発者は特定の拡張ポイントをサード パーティ開発者に公開し、コンポーネントのアドオンや拡張機能を構築できるようにします。Visual Studio 自体を拡張するための Visual Studio プラグイン モデルが、この優れたユース ケースの 1 つです。詳細については、MSDN ライブラリの「Visual Studio 拡張機能の開発」(bit.ly/IkJQsZ、英語) を参照してください。拡張ポイントを公開してプラグインを定義するこの方法は、いわゆる属性プログラミング モデルを使用しています。開発者は属性を使用してプロパティやクラスだけでなく、メソッドさえも修飾し、特定の型の依存関係の要件、または特定の型の依存関係を満たす機能のいずれかを通知します。
型システムが公開される拡張シナリオにとっては属性が非常に有用であることは事実ですが、型システムを公開しないシナリオではビルド時に把握すべき属性が多すぎます。属性プログラミング モデルが抱える根本的問題には次のようなものがあります。
- 同じようなパーツを多数構成すると、不必要な繰り返しが多くなります。これは「同じ情報を重複させない」(DRY: Don't Repeat Yourself) の原則に反しており、実際に、人為的ミスを引き起こしたり、ソース ファイルが読み取りにくくなることがあります。
- .NET Framework 4 で拡張機能やパーツを作成すると、MEF アセンブリとの依存関係を生じるため、特定の依存関係の挿入 (DI) フレームワークに開発者を縛り付けることになります。
- MEF を考慮に入れて設計されていないパーツでは、アプリケーションで適切に識別されるように、属性を追加する必要があります。これは導入にあたって大きな障壁になる可能性があります。
.NET Framework 4.5 では、構成を一元化して、拡張ポイントとコンポーネントを作成および構成する方法について、一連の規則を記述できるようにしています。これは、System.ComponentModel.Composition.Registration 名前空間にある RegistrationBuilder (bit.ly/HsCLrG、英語) という新しいクラスを使用して実現されます。今回は、まず、MEF のようなシステムを利用する理由について考察します。MEF の上級者の方は、この部分を読み飛ばしてください。次に、開発者に一連の要件が指定されている役割を与え、MEF の属性プログラミング モデルを使用してシンプルなコンソール アプリケーションを作成します。その後、RegistrationBuilder を使用していくつか典型的なシナリオを実装する方法を示しながら、このアプリケーションを規則ベースのモデルに変換します。最後に、規則を中心とする構成がアプリケーション モデルにどのように組み込まれているかを説明し、この構成により、MEF や DI の原理をすぐに使えるようにすることがいかに簡単かを示します。
背景
ソフトウェア プロジェクトのサイズやスケールが大きくなるにつれ、保守性、拡張性、およびテストの容易性が重要な課題になります。ソフトウェア プロジェクトが成熟するにしたがって、コンポーネントを置き換えたり、改良する必要が生じることもあります。プロジェクトのスコープが広がると、要件が変化したり追加されることがよくあります。プロジェクトを進化させるには、大きなプロジェクトに機能を簡単に追加できる能力が非常に重要です。さらに、大半のソフトウェアはサイクルが変わるたびに変更されるのが当たり前になっているため、ソフトウェア製品に含まれるコンポーネントを、他のコンポーネントから切り離して迅速にテストする能力は、特に依存関係のあるコンポーネントを並行開発するような環境では、きわめて重要です。
このような背景から、大規模ソフトウェア開発プロジェクトでは、DI という考え方が普及するようになりました。DI の考え方は、コンポーネントが必要とする依存関係や、コンポーネントが満たす依存関係を、実際にンスタンスを作成しないで公開するコンポーネントを開発することです。依存関係の挿入フレームワークは、依存関係の適切なインスタンスを把握して、その依存関係をコンポーネントに "挿入" します。依存関係の挿入の詳細については、2005 年 9 月号の MSDN マガジンの記事「依存関係の挿入」(msdn.microsoft.com/magazine/cc163739、英語) を参考にしてください。
シナリオ
では、先ほど説明したシナリオに取り掛かります。提示された仕様書を見ている開発者だと考えてください。大まかに言うと、実装を求められているソリューションの目的は、ユーザーの郵便番号に基づいて天気予報を提供することです。以下に、必要な手順を示します。
- ユーザーに郵便番号の入力を求める。
- ユーザーが有効な郵便番号を入力する。
- インターネットの気象サービス プロバイダーに予報の情報を問い合わせる。
- 予測情報の書式を整え、ユーザーに表示する。
要件を見ると、この時点でいくつか不明点があることや、開発サイクルの後半で変化する可能性がある点があることは明らかです。たとえば、使用する気象サービス プロバイダーや、そのプロバイダーからのデータの入手方法がまだわかりません。そこで、アプリケーションの設計にあたり、いくつか独立した機能単位 (WeatherServiceView、IWeatherServiceProvider、および IDataSource) に分割します。これらの各クラスのコードを、それぞれ図 1、図 2、および図 3 に示します。
図 1 WeatherServiceView (結果を表示するクラス)
[Export]
public class WeatherServiceView
{
private IWeatherServiceProvider _provider;
[ImportingConstructor]
public WeatherServiceView(IWeatherServiceProvider providers)
{
_providers = providers;
}
public void GetWeatherForecast(int zipCode)
{
var result=_provider.GetWeatherForecast(zipCode);
// Some display logic
}
}
図 2 IWeatherServiceProvider (WeatherUnderground) データ解析サービス
[Export(typeof(IWeatherServiceProvider))]
class WeatherUndergroundServiceProvider:IWeatherServiceProvider
{ private IDataSource _source;
[ImportingConstructor]
public WeatherUndergroundServiceProvider(IDataSource source)
{
_source = source;
}
public string GetWeatherForecast(int zipCode)
{
string val = _source.GetData(GetResourcePath(zipCode));
// Some parsing logic here
return result;
}
private string GetResourcePath(int zipCode)
{
// Some logic to get the resource location
}
}
図 3 IDataSource (WeatherFileSource)
[Export(typeof(IDataSource))]
class WeatherFileSource :IDataSource
{
public string GetData(string resourceLocation)
{
Console.WriteLine("Opened ----> File Weather Source ");
StringBuilder builder = new StringBuilder();
using (var reader = new StreamReader(resourceLocation))
{
string line;
while((line=reader.ReadLine())!=null)
{
builder.Append(line);
}
}
return builder.ToString();
}
}
最後に、この各パーツの階層を作成するためにカタログを使用します。このカタログからアプリケーションに含まれるすべてのパーツを見つけ、CompositionContainer を使用して WeatherServiceView のインスタンスを取得してからこのインスタンスを操作します。
class Program
{
static void Main(string[] args)
{
AssemblyCatalog cat =
new AssemblyCatalog(typeof(Program).Assembly);
CompositionContainer container =
new CompositionContainer(cat);
WeatherServiceView forecaster =
container.GetExportedValue<WeatherServiceView>();
// Accept a ZIP code and call the viewer
forecaster.GetWeatherForecast(zipCode);
}
}
ここに示したすべてのコードはかなり基本的な MEF セマンティクスです。このコードのしくみがよくわからない場合は、MSDN ライブラリの「Managed Extensibility Framework の概要」(bit.ly/JLJl8y) を参照してください。ここでは、MEF の属性プログラミング モデルについて詳しく説明しています。
規則主体の構成
これで属性を使用するバージョンをコーディングしたので、RegistrationBuilder を使用して、これらのコードを規則主体のモデルに変換する方法を説明します。まず、MEF の属性が追加されているクラスをすべて削除します。一例として、図 4 のコードは、図 2 に示した WeatherUnderground データ解析サービスを変更したものです。
図 4 シンプルな C# クラスに変換した WeatherUnderground データ解析クラス
class WeatherUndergroundServiceProvider:IWeatherServiceProvider
{
private IDataSource _source;
public WeatherUndergroundServiceProvider(IDataSource source)
{
_source = source;
}
public string GetWeatherForecast(int zipCode)
{
string val = _source.GetData(GetResourcePath(zipCode));
// Some parsing logic here
return result;
}
...
}
図 1 と図 3 のコードも、図 4 同じ方法で変更します。
次に、属性を使用して指定したことを表現するために、RegistrationBuilder を使用して一定の規則を定義します。図 5 に、このコードを示します。
図 5 規則のセットアップ
RegistrationBuilder builder = new RegistrationBuilder();
builder.ForType<WeatherServiceView>()
.Export()
.SelectConstructor(cinfos => cinfos[0]);
builder.ForTypesDerivedFrom<IWeatherServiceProvider>()
.Export<IWeatherServiceProvider>()
.SelectConstructor(cinfo => cinfo[0]);
builder.ForTypesDerivedFrom<IDataSource>()
.Export<IDataSource>();
規則の各宣言には、2 つの異なるパーツがあります。1 つは、操作する 1 つのクラスまたはクラスのセットを識別するパーツ、もう 1 つは、選択したクラス、クラスのプロパティ、またはクラスのコンストラクターに適用する属性、メタデータ、および共有ポリシーを指定するパーツです。2、5、および 8 行目では、3 つの規則を定義しています。各規則の最初の部分が型を識別し、残りの規則をその型に適用しています。たとえば、5 行目では、IWeatherServiceProvider から派生したすべての型に規則を適用します。
この規則を見て、図 1、図 2、および図 3 の元の属性付きコードにマップします。WeatherFileSource (図 3) は単に IDataSource をエクスポートしていました。図 5 の 8 行目と 9 行目の規則は、IDataSource から派生するすべての型を選択して、IDataSource のコントラクトとしてエクスポートするよう指定します。図 2 のコードは、IWeatherServiceProvider 型をエクスポートし、コンストラクターで IDataSource をインポートするよう求めています。つまり、コンストラクターが、ImportingConstructor 属性で修飾されています。これに対応する規則は、図 5 の 5、6、および 7 行目で指定しています。ここで追加されているのは、Func<ConstructorInfo[], ConstructorInfo> を受け取る SelectConstructor メソッドです。このようにして、コンストラクターを指定できます。つまり、最小数または最大数の引数を持つコンストラクターは常に ImportingConstructor になる、といった規則を指定できます。今回の例では、コンストラクターが 1 つしかないため、最初の (そして唯一の) コンストラクターを選択するという簡単なケースを使用できます。図 1 のコードには、図 5 の 2、3、および 4 行目の規則が指定されます。これは今説明した規則に似ています。
規則を決めたら、アプリケーション内に存在する型に規則を当てはめる必要があります。そのためには、RegistrationBuilder をパラメーターとして受け取るオーバーロードをすべてのカタログに含めることになります。そこで、前述の CompositionContainer コードを変更します (図 6 参照)。
図 6 規則の使用
class Program
{
static void Main(string[] args)
{
// Put the code to build the RegistrationBuilder here
AssemblyCatalog cat =
new AssemblyCatalog(typeof(Program).Assembly,builder);
CompositionContainer container = new CompositionContainer(cat);
WeatherServiceView forecaster =
container.GetExportedValue<WeatherServiceView>();
// Accept a ZIP code and call the viewer
forecaster.GetWeatherForecast(zipCode);
}
}
コレクション
これで終わりです。シンプルな MEF アプリケーションが属性なしで稼働します。しかし、そんな単純にはいきません。ここで複数の気象サービスをサポートし、すべての気象サービスからの予報を表示できなければならなくなったとします。さいわい、MEF を使用しているので、パニックになることはありません。単に、インターフェイスを複数実装して、これらを反復処理する必要があるだけです。今回の場合は、IWeatherServiceProvider の実装を複数用意し、これらのすべての気象エンジンからの結果を表示します。必要な変更を確認してみましょう (図 7 参照)。
図 7 複数の IWeatherServiceProviders を有効にする
public class WeatherServiceView
{
private IEnumerable<IWeatherServiceProvider> _providers;
public WeatherServiceView(IEnumerable<IWeatherServiceProvider> providers)
{
_providers = providers;
}
public void GetWeatherForecast(int zipCode)
{
foreach (var _provider in _providers)
{
Console.WriteLine("Weather Forecast");
Console.WriteLine(_provider.GetWeatherForecast(zipCode));
}
}
}
これで作業は完了です。1 つ以上の IWeatherServiceProvider 実装を受け取るように WeatherServiceView クラスを変更し、コレクションを反復処理するようロジックを変更しています。先ほど決めた規則により、IWeatherServiceProvider のすべての実装を取得し、エクスポートするようになります。でも、何か足りないように感じます。WeatherServiceView を構成しているときに、ImportMany 属性やそれに相当する規則をどこにも追加しませんでした。これは、RegistrationBuilder のちょっとしたメリットで、パラメーターに IEnumerable<T> が指定されているかどうかを判断して、ImportMany を明示的に指定していなくても、ImportMany が必要であることを決定します。そのため、MEF を使用してアプリケーションを拡張する作業が単純化され、RegistrationBuilder を使用することで、新しいバージョンでも IWeaterServiceProvider を実装している限り、何も行う必要はありません。すばらしいことです。
メタデータ
MEF のもう 1 つの非常に便利な機能は、メタデータをパーツに追加できることです。説明のために、今回の例で、GetResourcePath メソッドから返される値 (図 2 参照) は、使用する IDataSource と IWeatherServiceProvider の具象型によって決まるものとします。そこで、名前付け規則を 1 つ定義し、気象サービス プロバイダーとデータ ソースをアンダースコア ("_") で区切って組み合わせた名前をリソースに付けるようにします。この名前付け規則から、Weather Underground サービス プロバイダーと Web データ ソースの場合は、WeatherUnderground_Web_ResourceString という名前になります。このコードを図 8 に示します。
図 8 リソース記述定義
public class ResourceInformation
{
public string Google_Web_ResourceString
{
get { return "http://www.google.com/ig/api?weather="; }
}
public string Google_File_ResourceString
{
get { return @".\GoogleWeather.txt"; }
}
public string WeatherUnderground_Web_ResourceString
{
get { return
"http://api.wunderground.com/api/96863c0d67baa805/conditions/q/"; }
}
}
この名前付け規則により、WeatherUnderground と Google の気象サービス プロバイダーのプロパティを作成して、すべてのリソース文字列をインポートし、現在の構成に基づいて適切なリソース文字列を選択できるようになります。まず、RegistrationBuilder 規則を記述して、ResourceInformation を Export として構成する方法について説明します (図 9 参照)。
図 9 プロパティをエクスポートしメタデータを追加する規則
builder.ForType<ResourceInformation>()
.ExportProperties(pinfo =>
pinfo.Name.Contains("ResourceString"),
(pinfo, eb) =>
{
eb.AsContractName("ResourceInfo");
string[] arr = pinfo.Name.Split(new char[] { '_' },
StringSplitOptions.RemoveEmptyEntries);
eb.AddMetadata("ResourceAffiliation", arr[0]);
eb.AddMetadata("ResourceLocation", arr[1]);
});
1 行目は、単にクラスを識別します。2 行目は、このクラスのプロパティの中で ResourceString を含むプロパティを選択する述語を定義します。ExportProperties の最後の引数は、Action<PropertyInfo,ExportBuilder> です。ここでは、2 行目で指定した述語に一致するすべてのプロパティを、ResourceInfo という名前付きコントラクトとしてエクスポートすること、ResourceAffiliation と ResourceLocation をキーに使用して、そのプロパティの名前を解析した結果に基づいてメタデータを追加することを指定します。この規則を使用する側では、次のように、IWeatherServiceProvider のすべての実装にプロパティを追加することが必要になります。
public IEnumerable<Lazy<string, IServiceDescription>> WeatherDataSources { get; set; }
次に、厳密に型指定したメタデータを使用するように以下のインターフェイスを追加します。
public interface IServiceDescription
{
string ResourceAffiliation { get; }
string ResourceLocation { get; }
}
メタデータおよび厳密に型指定したメタデータの詳細については、bit.ly/HAOwwW (英語) のチュートリアルを参照してください。
ここで、RegistrationBuilder で規則を追加して、ResourceInfo というコントラクト名を含むすべてのパーツをインポートします。このため、図 5 (5 ~ 7 行目) の既存の規則を使用して、以下の句を追加します。
builder.ForTypesDerivedFrom<IWeatherServiceProvider>()
.Export<IWeatherServiceProvider>()
.SelectConstructor(cinfo => cinfo[0]);
.ImportProperties<string>(pinfo => true,
(pinfo, ib) =>
ib.AsContractName("ResourceInfo"))
8 行目と 9 行目は、IWeatherServiceProvider から派生するすべての型は、string 型のすべてのプロパティに Import を適用する必要があることと、その Import を ResourceInfo コントラクト名に基づいて作成する必要があることを指定しています。この規則を実行すると、名前が ResourceInfo のすべてのコントラクトについて、前記で追加したプロパティが Import になります。その後、列挙値をクエリして、メタデータに基づいて正しいリソース文字列をフィルター選択できます。
属性は不要になるのか
ここまで説明してきたサンプルを考えると、属性はもはや実際には必要なくなったように思われます。属性プログラミング モデルで実行できることは、規則ベースのモデルを使用して実現できるようになります。RegistrationBuilder が役に立つ、いくつか一般的なユース ケースについて述べましたが、RegistrationBuilder の詳細については、Nicholas Blumhardt のすばらしい記事 (bit.ly/tVQA1J、英語) をご覧ください。ただし、MEF の使い方が規則主体になっても、依然として属性は重要な役割を果たします。規則に関する 1 つの大きな問題は、規則に従っている場合に限り、威力を発揮する点です。規則に例外が生じるや否や、その規則をメンテナンスするオーバーヘッドがきわめて高くなる可能性があります。ただし、属性を使って規則をオーバーライドすることができます。新しいリソースが ResourceInformation クラスに追加され、その名前が規則に従っていないとします (図 10 参照)。
図 10 属性を使用した規則のオーバーライド
public class ResourceInformation
{
public string Google_Web_ResourceString
{
get { return "http://www.google.com/ig/api?weather="; }
}
public string Google_File_ResourceString
{
get { return @".\GoogleWeather.txt"; }
}
public string WeatherUnderground_Web_ResourceString
{
get { return "http://api.wunderground.com/api/96863c0d67baa805/conditions/q/"; }
}
[Export("ResourceInfo")]
[ExportMetadata("ResourceAffiliation", "WeatherUnderground")]
[ExportMetadata("ResourceLocation", "File")]
public string WunderGround_File_ResourceString
{
get { return @".\Wunder.txt"; }
}
}
図 10 では、これまでの名前付け規則から、規則の最初の部分が正しくないことがわかります。しかし、正しいコントラクト名とメタデータを把握して明示的に追加することで、RegistrationBuilder によって検出されたパーツをオーバーライドしたり、パーツに追加できるため、MEF の属性は、RegistrationBuilder で定義された規則の例外を指定する効果的なツールになります。
シームレスな開発
今回は、規則を中心とした構成について説明しました。これは、RegistrationBuilder クラスで公開される MEF の新機能で、MEF 関連の開発効率を大幅に向上します。これらのライブラリのベータ版は、mef.codeplex.com (英語) にあります。.NET Framework 4.5 をまだお持ちでなければ、CodePlex サイトからダウンロードできます。
皮肉なことに、RegistrationBuilder により、日常の開発作業で MEF について思いをめぐらすことが少なくなるため、プロジェクトでの MEF の使用が非常にシームレスになります。これに関する優れた例は、MEF 用のモデル - ビュー - コントローラー (MVC: Model-View-Controller) に組み込まれている統合パッケージです。これについては、BCL チームのブログ (bit.ly/ysWbdL、英語) を参照してください。短いバージョンでは、パッケージが MVC アプリケーションにダウンロードされ、これにより、プロジェクトで MEF を使用するようにセットアップされます。このエクスペリエンスでは、すべてコードは "ただ機能するだけ" ですが、指定した規則に従うと、MEF のコード行を自身で記述することなく、アプリケーションで MEF を使用するメリットが得られます。詳細については、BCL チームのブログ (bit.ly/ukksfe、英語) を参照してください。
Alok Shriram は、マイクロソフトの Microsoft .NET Framework チームのプログラム マネージャーを努めており、基本クラス ライブラリ チームに所属しています。以前は、後に Office 365 チームとなる、Office Live チームの開発者として働いていました。ノースカロライナ大学チャペルヒル校の大学院を経て、現在はシアトルに在住しています。余暇には、太平洋岸北西部で与えられるすべてのものを、妻の Mangal と一緒に探しています。彼は MEF CodePlex サイトで作業したり、Twitter (twitter.com/alokshriram、英語) で発言したり、.NET ブログに投稿することもあります。
この記事のレビューに協力してくれた技術スタッフの Glenn Block、Nicholas Blumhardt、および Immo Landwerth に心より感謝いたします。