次の方法で共有


Microsoft patterns & practices の内部

ライブラリでの依存関係の挿入

Chris Tavares

Microsoft Enterprise Library をリファクターする

依存関係の挿入 (DI) は、.NET 開発者の間でここ数年の間に影響力を増しているパターンです。著名なブロガーたちは、ずいぶん前から、DI のメリットについて話題にしていました。MSDN マガジンには、このトピックに関する記事がいくつか掲載されてきました。.NET 4.0 には DI に似た機能が少し含まれる予定で、将来は、この機能は完全な DI システムへと発展する予定です。

私は、DI に関するブログ投稿や記事を読んでいくうちに、このトピックの取り上げられ方に関するちょっとした、しかし重大な偏りに気付きました。こうしたブログ投稿や記事では、アプリケーション全体との関連において DI の使用について説明されています。しかし、DI を使用するライブラリやフレームワークを作成する必要がある場合はどうなるでしょうか。焦点がこのように変わると、パターンの使い方にどのような影響があるでしょうか。これは、私たち (patterns & practices Enterprise Library チーム) が数か月前に Enterprise Library 5.0 のアーキテクチャに取り組んでいたときに直面した疑問です。

背景

Microsoft Enterprise Library (Entlib) は、Microsoft patterns & practices グループが提供する非常に有名なリリースです。Entlib は今までに 200 万回以上ダウンロードされており、想像し得るほぼすべてのニッチで (金融機関や政府機関からレストランや医療機器メーカーに至るまで) 使用されています。Entlib は、その名が示すとおり、多くのエンタープライズ開発者が共有している一般的な問題に対処するのに役立つライブラリです。Entlib についてあまりご存じない場合は、詳細については patterns & practices デベロッパー センター (英語) のサイトを参照してください。

Entlib は非常に構成主導型です。Entlib のコードの大部分は、構成を読み取り、その構成に基づいてオブジェクト グラフを作成することに費やされています。Entlib オブジェクトは非常に複雑になる場合があります。ほとんどのブロックには、多くのオプション機能が含まれています。また、インストルメンテーションなどをサポートするための多くの基になるインフラストラクチャも存在し、こうしたインフラストラクチャも構成する必要があります。ユーザーが Entlib を使用するためだけに手動でインストルメンテーション プロバイダーを作成したり構成を読み取ったりしなくても済むように、オブジェクトの作成はファクトリ オブジェクトや静的ファサードの背後にカプセル化されています。

Entlib Version 2 ~ 4 の中核は、ObjectBuilder と呼ばれる小規模なフレームワークです。作成者の説明によると、ObjectBuilder は "依存関係挿入コンテナーを構築するためのフレームワーク" です。Enterprise Library は、ObjectBuilder を使用する patterns & practices プロジェクトの 1 つに過ぎません。このようなプロジェクトには、他にも、Composite UI Application Block、Smart Client Software Factory、Web Client Software Factory などがあります。Entlib では説明の中の "フレームワーク" の部分が特に重視され、ObjectBuilder に対する大規模なカスタマイズが構築されました。こうしたカスタマイズは、Entlib の構成を読み取ってオブジェクト グラフを作成するのに必要な機能を提供しました。多くの場合、こうしたカスタマイズは、古い ObjectBuilder 実装よりもパフォーマンスを向上させるためにも必要でした。

デメリットは、ObjectBuilder 自体 (設計が非常に抽象的であるうえにドキュメントがまったく存在しないため、ObjectBuilder は、複雑であるというもっともな評価を受けていました) と Entlib のカスタマイズの両方を理解するにはかなり時間がかかることでした。そのため、Entlib のオブジェクト作成戦略を利用したカスタム ブロックを作成しようとしたユーザーは、初歩的なことでも習得が困難なことから、挫折してしまうことがよくありました。

さらに厄介なことに、Entlib 4.0 では、Unity という依存関係挿入コンテナーがリリースされました。DI には多くのメリットがあるので、私たちは、(理由は何であれ) 多くの優れたオープン ソース コンテナーのいずれかを使用することができなかったユーザーに、マイクロソフトが提供する DI の優れた選択肢を提供したいと考えました。また、当然、Unity を使用する場合に Entlib オブジェクトを簡単に機能させることができるようにしたいとも考えました。Entlib 4.0 では、Unity が統合されたことにより、既存の ObjectBuilder インフラストラクチャと並ぶ類似したオブジェクト作成システムが存在することになってしまいました。ブロック作成者は、ObjectBuilder と Entlib 拡張機能だけでなく、Unity の内部、および Unity 内のいくつかの Entlib 拡張機能についても知っておかなくてはならなくなりました。これでは、向かっている方向が正しくありません。

簡潔さに向かう

私たちは、2009 年 4 月に Entlib 5.0 に取り組み始めました。このリリースの主要なテーマは、"成功のための簡潔さ" でした。これには、エンド ユーザー (Entlib を呼び出す開発者) にとっての簡潔さだけでなく、Entlib 自体のコードの簡潔さも含まれていました。簡潔さが向上すると、Entlib の開発を進める際に Entlib を保守するのがより容易になり、ユーザーが Entlib を理解し、カスタマイズし、拡張するのもより容易になります。

対処が必要であることがとわかっていた主な領域の 1 つは、オブジェクト作成パイプラインでした。同じ機能用に 2 つの類似しているが異なるコードのセットを保持することは、大きな失敗のもとです。なんらかの手を打つ必要がありました。

私たちは、リファクタリングに関して以下の目標を設定しました。

  • アーキテクチャ上の変更のみを理由として、既存のクライアント コードの変更が必要になってはならない。再コンパイルが必要になるのはかまわないが、ソース コードの変更が必要になるのは問題である (もちろん、他の理由でクライアント API が変更される可能性はある)。内部 API や拡張 API は変更してもかまわない。
  • 重複するオブジェクト作成パイプラインを削除する。オブジェクトの作成方法は 2 つ (以上) ではなく 1 つだけでなければならない。
  • DI に関心のないユーザーが内部で Entlib を使用する場合、Entlib の影響を受けないようにする。
  • DI に関心のあるユーザーは、使用するコンテナーを選択し、独自のオブジェクトと Entlib オブジェクトの両方をコンテナーから抽出することができる。

これらの目標は、単独でも組み合わさっても、多くの意味合いを持っていました。"1 つのオブジェクト作成パイプライン" という目標は、表面的には、非常に単純でした。私たちは、ObjectBuilder ベースのシステムを完全に削除して、DI コンテナーをオブジェクト作成エンジンとして内部で使用することに決めました。しかし、ここで "既存のクライアント コードを変更してはならない" という目標に直面します。従来の Entlib API は、静的なファサードとファクトリのセットです。たとえば、Logging ブロックを使用してメッセージをログ記録するには、次のようなコードを使用します。

Logger.Write("My Message");

内部では、Logger ファサードが LogWriter オブジェクトのインスタンスを使用して実際の処理を行います。では、Logger ファサードはどのようにして LogWriter を取得するのでしょうか。LogWriter は多数の依存関係を持つかなり複雑なクラスなので、単にインスタンス化しただけでは構成は適切に行われません。私たちは、Logger および API 内の他のすべての静的クラス用のグローバル コンテナー インスタンスが必要だという結論に達しました。単にグローバルな Unity コンテナーを保持することも可能ですが、そうすると、"ユーザーは使用するコンテナーを選択することができる" という目標に直面します。

私たちは、Unity と Entlib の組み合わせを非常に使い勝手のよいものにしたいと考えています。また、この使い勝手のよさを他のコンテナーにも提供したいと考えています。DI コンテナーの全般的な機能は全体にわたって共通ですが、こうした機能へのアクセス方法は大きく異なります。実際、多くのコンテナー作成者は、自分たちの独自の構成 API が競争上の主要な強みであると考えています。では、Entlib の構成を大幅に異なるコンテナー API にマップするにはどうすればよいのでしょうか。

コンピューター サイエンスの典型的な問題解決手法

コンピューター サイエンスの決まり文句に、"コンピューター サイエンスの問題はすべて、間接層を追加することによって解決することができる" というものがあります。私たちがコンテナーの独立性の問題を解決した方法も、まさにこれです。この場合、間接層となるのは、コンテナー コンフィギュレーターと呼ばれるものです。コンフィギュレーターの根本的な役割は、Entlib の構成を読み取り、適合するコンテナーを構成することです。

残念ながら、構成自体を読み取るだけでは不十分です。Entlib の構成ファイルの形式は、非常にエンド ユーザー中心型です。ユーザーがログ記録カテゴリ、例外ポリシー、およびキャッシュのバッキング ストアを構成します。Entlib の構成ファイルでは、ある機能を実装するために実際に必要となるオブジェクトも、コンストラクターに渡す必要がある値や、設定する必要があるプロパティも、まったく示されません。一方、DI コンテナーの構成は、"このインターフェイスをこの型にマップする"、"このコンストラクターを呼び出す"、"このプロパティを設定する" というような内容です。ブロックを実装するために、ブロックの構成を実際に必要となるオブジェクトにマップするもう 1 つの間接層が必要でした。各コンフィギュレーター (コンテナー 1 つにつき 1 つのコンフィギュレーターが必要です) がそれぞれのブロックの詳細を把握するようにするという方法もありましたが、これは言うまでもなく不適切な方法です。この方法を使用すると、ブロックのコードへのすべての変更がすべての構成に影響を及ぼします。このような場合、カスタム ブロックが作成されるとどうなってしまうでしょうか。

結局、TypeRegistration と呼ばれる一連のオブジェクトを使用することになりました。"型登録モデル" (一連の TypeRegistration オブジェクト) の生成には、さまざまな構成セクションが関与しています。TypeRegistration のインターフェイスを図 1 に示します。

図 1 TypeRegistration クラス

public class TypeRegistration
    {

        public TypeRegistration(LambdaExpression expression);
        public TypeRegistration(LambdaExpression expression, Type serviceType);

        public Type ImplementationType { get; }
        public NewExpression NewExpressionBody { get; }
        public Type ServiceType { get; private set; }
        public string Name { get; set; }

        public static string DefaultName(Type serviceType);
        public static string DefaultName<TServiceType>();

        public LambdaExpression LambdaExpression { get; private set; }

         public bool IsDefault { get; set; }

         public TypeRegistrationLifetime Lifetime { get; set; }

         public IEnumerable<ParameterValue> ConstructorParameters { get; }

         public IEnumerable<InjectedProperty> InjectedProperties { get; }
    }

これはかなりの量のコードですが、基本的な構造は非常に単純です。このクラスは、1 つの型用に必要な構成を表しています。ServiceType はユーザーがコンテナーから要求するインターフェイスで、ImplementationType は実際にインターフェイスを実装する型です。Name は、サービスをどのような名前で登録するかを示します。Lifetime では、作成動作がシングルトン (毎回同じインスタンスを返す) であるか一時的 (毎回新しいインスタンスを作成する) であるかを決定します。この他にも、さまざまなメンバが用意されています。ラムダ式を使用すると、こうした情報すべてを 1 か所で簡潔に指定するのが非常に容易になるので、ラムダ式を使用して TypeRegistration オブジェクトを作成することにしました。Data Access ブロックから型登録を作成する例を以下に示します。

yield return new TypeRegistration<Database>(
       () => new SqlDatabase(
           ConnectionString,
           Container.Resolved<IDataInstrumentationProvider>(Name)))
       {
           Name = Name,
           Lifetime = TypeRegistrationLifetime.Transient
       };

この型登録の内容は、"Name という名前の Database を要求する際は、ConnectionString と IDataInstrumentationProvider を使用して構築された新しい SqlDatabase オブジェクトを返す" というものです。ここでラムダを使用するとよい点は、ブロックを作成する際、オブジェクトを直接インスタンス化しているかのようにこうした式を構築できることです。コンパイラによって式の型がチェックされるので、存在しないコンストラクターを間違って呼び出そうとしてしまうことはなくなります。プロパティを設定するには、単に、ラムダ式内で C# のオブジェクト初期化構文を使用します。ラムダを調べてコンストラクターのシグネチャ、パラメーター、型などを抽出するという処理の詳細は TypeRegistration クラスが担当するので、構成の作成者が対処する必要はありません。

上記のコードで使用した 1 つの便利な技は、"Container.Resolved" の呼び出しです。このメソッドは実際には何も行いません。現に、このメソッドの実装は次のとおりです。

public static T Resolved<T>(string name)
        {
            return default(T);
        }

なぜこのメソッドを使用しているのでしょうか。このラムダ式は実際には実行されないことを思い出してください。代わりに、実行時に式の構造を調べて、登録情報を抽出します。このメソッドは単に、既知のマーカーです。Container.Resolved の呼び出しがパラメーターとして渡された場合、それは "コンテナーを通じてこのパラメーターを解決してください" という意味に解釈されます。このマーカー メソッド手法は、式ツリーに対して高度な処理を行う場合にさまざまな場所で役に立ちます。

結局、構成ファイルから構成されたコンテナーに至るまでの流れは、図 2 のようになります。

図 2 コンテナーの構成

設計上のある決断をここでお伝えしておく必要があります。TypeRegistration システムは、あらゆる DI コンテナー向けの、汎用的であらゆるものを構成できる抽象化ではなく、そうなることはありません。このシステムは、Enterprise Library プロジェクトのニーズ専用に設計されたものです。patterns & practices チームは、これをコード ベースのガイダンスとして位置付けてはいません。基本的な概念 (構成を抽出して抽象的なモデルを作り上げる) は一般的に適用可能ですが、ここでの具体的な実装は Entlib 専用です。

コンテナーからオブジェクトを抽出する

ここまでのところで、コンテナーの構成を行いました。これでほとんど成功したも同然です。ですが、今度はオブジェクトを抽出するにはどうすればよいでしょうか。コンテナー インターフェイスはこの方法に関してもさまざまですが、さいわい、構成インターフェイスほどではありません。

さいわい、ここで新しい抽象化を考え出す必要はありませんでした。2008 年夏の Jeremy Miller によるブログ投稿 (英語) に触発され、patterns & practices グループ、マイクロソフトの MEF チーム、および多くの異なる DI コンテナーの作成者が協力して、コンテナーからオブジェクトを抽出するための最大公約数を定義しました。これは、Common Service Locator プロジェクトとして CodePlex と MSDN に掲載されました。このインターフェイスを使用すると、まさに必要としていた処理を行うことができました。コンテナーからオブジェクトを抽出する必要がある場合はいつでも、Enterprise Library 内からこのインターフェイスを通じて呼び出しを行えば、使用中の特定のコンテナーからオブジェクトを分離することができました。次に問題となるのはもちろん、"コンテナーはどこにあるのか" です。

Enterprise Library は、ブートストラップに関して何も要求しません。静的ファサードを使用する場合、初期化関数をどこかで呼び出す必要はありません。元のライブラリは、最初に必要になったときに構成を抽出することによって機能していました。ライブラリが呼び出されたときにライブラリの準備が整った状態になっているようにするため、この動作を再現する必要がありました。

適切に構成されたコンテナーを取得できることがわかっている基準が必要でした。実は、Common Service Locator ライブラリにその 1 つである ServiceLocator.Current プロパティが用意されていますが、いくつかの理由により、私たちはこれを使用しないことにしました。第 1 の理由は、ServiceLocator.Current は他のライブラリで (さらには、アプリケーション自体でも) 使用できることでした。なんらかの Entlib オブジェクトによる最初のアクセス時にコンテナーを設定できる必要がありました。それ以外のときに設定すると、ユーザーが、慎重に構築したコンテナーがなぜ姿を消したのかや、Entlib がなぜ最初の呼び出しでは機能したのにその後は機能しなくなったのかを突き止めようとした際に、問題のもととなりました。第 2 の理由は、インターフェイス自体の欠点に関するものです。プロパティを確認して、プロパティが設定されているかどうかを調べる方法はありません。そのため、コンテナーをいつ設定するべきかを判断するのは困難でした。

そこで、私たちは EnterpriseLibraryContainer.Current という独自の静的プロパティを構築しました。ユーザー コードからこのプロパティを設定することもできますが、これは明確に Enterprise Library の一部なので、他のライブラリやメインのアプリケーションと競合する可能性は低くなります。静的ファサードに対する最初の呼び出しで、EnterpriseLibraryContainer.Current をチェックします。このプロパティが設定されている場合は、設定されている値を使用します。設定されていない場合は、UnityContainer オブジェクトを作成し、コンフィギュレーターを使用してこれを構成し、これを Current プロパティの値として設定します。

これで、Enterprise Library の機能にアクセスする方法は 3 とおりになりました。従来の API を使用すると、すべてが機能します。内部では Unity コンテナーが作成され、使用されます。アプリケーション内で別の DI コンテナーを使用しているので Unity はプロセスに必要ないが、依然として従来の API を使用している場合は、コンフィギュレーターを使用してコンテナーを構成し、それを IServiceLocator 内にラップし、EnterpriseLibraryContainer.Current に設定すれば、ファサードは機能し続けます。こうすることによって初めて、選択したコンテナーが内部でファサードによって使用されるようになります。実際のところ、メインの Entlib プロジェクトには、Unity 用の構成以外にはコンテナーの構成はまったく用意されていません。私たちは、他のコンテナーに関してはコミュニティの皆さんが実装してくださることを願っています。

2 つ目の方法は、EnterpriseLibraryContainer.Current を直接使用するというものです。GetInstance<T>() を呼び出して、どんな Enterprise Library オブジェクトでも取得することができます。ここでも、必要に応じて別のコンテナーを背後に設定することもできます。

3 つ目の方法として、選択したコンテナーを直接使用することもできます。コンフィギュレーターを使用して Entlib の構成をコンテナー内にブートストラップで読み込む必要がありますが、コンテナーを使用している場合はいずれにしろそのコンテナーを設定する必要があるので、これは新たな要求事項ではありません。その後は、任意の Entlib オブジェクトを依存関係として挿入すればよいだけです。

目標が満たされているかどうか

一連の目標を振り返り、この設計が目標をどの程度満たしているかを確認しましょう。

  1. アーキテクチャ上の変更のみを理由として、既存のクライアント コードの変更が必要になってはならない。再コンパイルが必要になるのはかまわないが、ソース コードの変更が必要になるのは問題である (もちろん、他の理由でクライアント API が変更される可能性はある)。内部 API や拡張 API は変更してもかまわない。

    満たされている。 元の API は依然として変更なしで機能します。依存関係の挿入に関心がない場合は、オブジェクトが内部でどのように構成されているかを知る必要も気にする必要もありません。

  2. 重複するオブジェクト作成パイプラインを削除する。オブジェクトの作成方法は 2 つ (以上) ではなく 1 つだけでなければならない。

    満たされている。 ObjectBuilder スタックはコード ベースから削除され、すべては TypeRegistration とコンフィギュレーター メカニズムを使用して構築されるようになりました。コンテナー 1 つにつき 1 つのコンフィギュレーターが必要です。

  3. DI に関心のないユーザーが内部で Entlib を使用する場合、Entlib の影響を受けないようにする。

    満たされている。 DI は、ユーザーが望まない限り、表面に現れません。

  4. DI に関心のあるユーザーは、使用するコンテナーを選択し、独自のオブジェクトと Entlib オブジェクトの両方をコンテナーから抽出することができる。

    満たされている。 選択した DI コンテナーを直接使用することも、静的ファサードの背後で内部的に使用されるようにすることもできます。

付加的なメリットもいくつか得られました。Entlib のコード ベースがより単純になりました。結局、約 200 個のクラスが元の実装から削除されました。型登録関連のコードを追加しても、リファクタリングが完了するとクラス数は合計約 80 個減っていました。また、追加されたクラスは削除されたクラスよりも単純で、流動的な部分や特殊なケースが減って全体的な構造の一貫性は大幅に向上しました。

もう 1 つのメリットは、リファクターされたバージョンは元のバージョンよりも少し高速だったことです (いくつかの初期の非公式な測定は 10% のパフォーマンス向上を示していました)。実のところ、私たちは、こうした数値を見て納得しました。元のコードの複雑さの多くは、ObjectBuilder の低速な実装に対処する一連のパフォーマンス最適化によるものでした。ほとんどの DI コンテナーでは、全般的なパフォーマンスに関して効果的な取り組みが行われました。コンテナーをベースとして Entlib を構築し直すことにより、このパフォーマンスに関する取り組みを活用することができ、自分たちでそれほど多くの作業を行わなくて済みます。Unity や他のコンテナーが進化し、さらに最適化されるにつれて、私たち自身がたくさんの労力を費やさなくても Entlib はより高速になるでしょう。

他のライブラリに生かせる教訓

Enterprise Library は、1 つに固く結合されることなく依存関係挿入コンテナーを真に活用するライブラリの良い例です。DI コンテナーを使用するが開発者が選択したコンテナーの使用をユーザーに強制しないライブラリを作成しようとしている方は、この例から設計のヒントを得ることができるでしょう。私たちが変更に関して設定した目標 (特に以下の 2 つ) は、Entlib だけでなく、ライブラリを作成するすべての方にとって適切な目標だと思われます。

  • DI に関心のないユーザーが内部で Entlib を使用する場合、Entlib の影響を受けないようにする。
  • DI に関心のあるユーザーは、使用するコンテナーを選択し、独自のオブジェクトと Entlib オブジェクトの両方をコンテナーから抽出することができる。

ライブラリを設計する際は、考える必要がある問題がいくつかあります。以下の問題を検討するようにしてください。

  • ライブラリはどのようにブートストラップされるか。コードの準備を整えるためにクライアントが特定の処理を行う必要があるのか、何も行わなくても機能する静的なエントリ ポイントがあるのか。
  • コンテナーの呼び出しをハードコーディングすることなくそのコンテナーを構成できるようにするには、オブジェクト グラフをどのようにモデル化すればよいか (ヒントを得るために、TypeRegistration システムをご覧ください)。
  • 使用するコンテナーはどのように管理されるか (内部で処理されるか、呼び出し元が管理するか)。呼び出し元は、使用するコンテナーをどのようにして通知するか。

Enterprise Library プロジェクトでは、これらの問題に対する適切な答えを見つけることができました。この例が、皆さんがプロジェクトを設計するうえでのヒントをご提供できることを願っています。

Chris Tavares は Microsoft patterns & practices チームの開発者であり、このチームで Enterprise Library と Unity の開発リーダーを務めています。マイクロソフトに勤務する前は、コンサルティング、市販ソフトウェア、および組み込みシステムの分野で働いていました。ブログ (tavaresstudios.com、英語) で、Entlib、patterns & practices、および開発に関する一般的なトピックについて書いています。