Silverlight をインストールするには、ここをクリックします*
Japan変更|すべてのMicrosoft のサイト
MSDN
|MSDN ライブラリ|デベロッパー センター|ダウンロード情報|開発ツール製品|コミュニティ|ご意見・ご要望|サイトマップ
MSDN Magazine  
   MSDN Home >  MSDN マガジン >  2005 年 7 月
内部の概要

.NET Framework で使用されているデザイン パターンを見つける



この記事で取り上げる話題:
  • .NET Framework クラスで使用される一般的なデザイン パターン
  • ASP.NET プログラミング モデルの実装に使用されるパターンとパイプラインの要求に使用されるパターン
  • パターンによってプログラミング作業が迅速化・簡略化されるしくみ
この記事で使用する技術:
.NET Framework と ASP.NET


サンプルコードのダウンロード:
DesignPatterns.exe (英語) (122KB)

目次


最近、マイクロソフトはデザイン パターンへの注力を強めています。パターンに馴染みのない人は、聞き慣れない用語や見慣れない UML 図に戸惑うかもしれません。ただし、パターンが重要視されているとはいえ、新しくなっているのは用語だけで、方法論自体が大きく変わるということではありません。Microsoft .NET Framework の基本クラス ライブラリ (BCL) ではすでにパターンが広く使用されており、実は多くのパターンをすでに目にしているのに、それと気づかないでいるだけのことかもしれないのです。

この記事では、一般的ないくつかのデザイン パターンの基本的な概要と、これらのパターンが BCLや .NET Framework の他の領域でどのように使用されているかを説明します。この記事を読めば、Framework が現在の形に至った設計上の理由のいくつかがわかると同時に、パターンそのものの抽象的な概念をより直感的に理解できるようになるでしょう。

ここで紹介するパターンのほとんどは、その分野の権威ある参考書である『Design Patterns』(Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides 共著、Addison-Wesley, 1995) から引用したものです。この 4 名の著者は「Gang of Four (4 人のギャングたち)」と呼ばれています。Gang of Four (GoF) はパターンを発明したわけではなく、ソフトウェア開発が始まった頃からの先人の知恵を集大成してパターンを形式化しました。

この記事はパターン別に分かれた構成になっているので、自分が知っているパターンは飛ばして、知らないパターンに関する部分だけを読んで頂いても構いません。ASP.NET 関連のパターンに関する項を理解するには、リクエスト パイプラインについての知識が必要になります。リクエスト パイプラインの基本的な概要については同じ項の中で説明しています。

.NET Framework では、パターンがごく一般的に使用されており、単にクラス ライブラリで表すのではなく、プログラミング言語自体に組み込まれてしまっているパターンもあります。最初に取り上げる Observer と Iterator という 2 つのパターンは、C# と Visual Basic .NET の両方でさまざまな言語機能でサポートされています。以下の説明を読めば、これらのパターンが多くの一般的なプログラミング作業にとって不可欠な一部であることがご理解いただけるはずです。


Observer パターン

優れたオブジェクト指向設計では、カプセル化と疎結合が重視されます。言い換えれば、クラスでは内部の詳細を隠蔽し、厳密な依存性を最小限にとどめる必要があります。ほとんどのアプリケーションでは、クラスは単独で機能するのではなく、他の多くのクラスと連動 (相互作用) します。別のクラス (Subject) に変更があったときに 1 つのクラス (Observer) に通知する必要がある場合は、クラスの相互作用が発生します。たとえば、あるボタンがクリックされた場合、複数の Windows Forms コントロールの表示を更新する必要があるとします。この場合の簡単な解決法は、状態に変化があったときにそのつど Subject が Observer の特定のメソッドを呼び出すようにすることです。ただし、この方法では数多くの問題が生じてしまいます。まず、Subject はどのメソッドを呼び出すかを知っている必要があるので、特定の Observer に強固に結合されます。また、複数の Observer を追加する必要がある場合には、メソッドの呼び出しごとに Subject にコードを追加していかなければなりません。Observers の数が動的に変化する場合には、さらに複雑になります。 すぐに収集のつかない状態になることは目に見えています。

この問題は、Observer パターンを適用すると効率的に解決できます。Observer から Subject を結合解除すれば、どんな種類の Observer でも、設計時または実行時に簡単に追加または削除できます。Subject には関連する Observer のリストが保持されます。状態が変化するたびに、Subject は各 Observer の Notify メソッドを呼び出します。図 1 は、実装例です。Observer として機能するように設計されたクラスはすべて ICanonicalObserver インターフェイスを実装します。また、Subject はすべて CanonicalSubjectBase から派生する必要があります。新しい Observer で Subject を監視する必要がある場合でも、Subject クラスのコードを一切変更することなく、Add メソッドだけで簡単に対処できます。また、各 Subject は特定の Observer ではなく、ICanonicalObserver インターフェイスに直接依存しているだけです。

GoF の Observer パターンを使用すれば、一部の問題が解決されるとは言え、Subject は特定の基本クラスから継承する必要があり、Observer は特別なインターフェイスを実装する必要があるため、まだいくつかの障害が残っています。Windows Forms ボタンの例に戻って考えると、ある解決策が浮かんできます。.NET Framework には、これらの問題を解決するためにデリゲートとイベントという概念が導入されています。ASP.NET や Windows Forms 向けのプログラミング経験がある読者なら、イベントやイベント ハンドラを扱ったことがあるでしょう。イベントは Subject、デリゲートは Observer としての役割をそれぞれ果たします。図 2 に、イベントを利用した Observer パターンの例を示します。

Windows Forms Button コントロールは、ボタンがクリックされると発生する Click イベントを公開します。このイベントに対して反応するように設計されたクラスはすべて、そのイベントにデリゲートを登録する必要があります。Button クラスは潜在的な Observer のどれにも依存しておらず、各 Observer はイベントの適切なタイプのデリゲート (この場合は EventHandler) を知る必要があるだけです。EventHandler はデリゲート タイプでインスタンスではないため、Observer ごとに追加のインターフェイスを実装する必要はありません。すでに対応するシグネチャを持つメソッドが含まれていると想定すると、そのメソッドを Subject のイベントに登録するだけで済みます。このように、Observer パターンを使用すれば、デリゲートとイベントを通じて、Subject とその Observer の結合を解除できます。

Back to top

Iterator パターン

プログラミング作業では、オブジェクトのコレクションを操作することが多くあります。コレクションが単純なリストであれ、バイナリ ツリーなどの複雑なものであれ、コレクション内の各オブジェクトにアクセスするための方法が必要になることがしばしばあります。実際、コレクションによっては、前から後ろ、後ろから前、先行順 (行きがけ)、後行順 (帰りがけ) など、複数の方法で各オブジェクトにアクセスする必要がある場合があります。コレクションをシンプルにするために、トラバース コード自体が専用のクラスを持つ場合もあります。

オブジェクトのリストを格納するための最も基本的な方法の 1 つが配列です。配列型は、Visual Basic .NET と C# の両方に組み込まれています。いずれの言語にも、配列での反復処理に便利なループ構造があります。C# の場合は foreach、Visual Basic .NET の場合は For Each です。配列を反復処理する簡単な例を次に示します。

int[] values = new int[] {1, 2, 3, 4, 5};
foreach(int i in values)
{
    Console.Write(i.ToString() + " ");
}
このステートメントでは、配列の背後で反復子を使用しています。ここでは、配列のアイテムごとに 1 回だけにループが実行されることが保証されれば良いわけです。

このステートメントが正常に動作するためには、In 式で参照されるオブジェクトが IEnumerable を実装する必要があります。IEnumerable インターフェイスを実装するオブジェクトのコレクションはすべてトラバース (列挙) できます。このインターフェイスには、IEnumerator を実装するオブジェクトを返す GetEnumerator というメソッドが 1 つ含まれています。IEnumerator クラスには、コレクションを反復処理するために必要なコードが含まれています。現在のオブジェクト (Current) のプロパティと、次のオブジェクトに進むためのメソッド (MoveNext)、および処理を始めからやり直すためのメソッド (Reset) があります。System.Collections 名前空間のすべてのコレクション クラスと配列は、IEnumerable を実装します。したがって、反復処理が可能になります。

foreach を使用するコードを対象に C# コンパイラで生成された Microsoft 中間言語 (MSIL) を見てみると、ほとんどの場合に IEnumerator を使って反復処理が行われていることがわかります (配列や文字列などの型は、コンパイラで特殊な扱いを受けます)。前の例で使用した配列を IEnumerator を使って反復処理する例を次に示します。

int[] values = new int[] {1, 2, 3, 4, 5};
IEnumerator e = ((IEnumerable)values).GetEnumerator();
while(e.MoveNext())
{
    Console.Write(e.Current.ToString() + " ");
}

.NET Framework では IEnumerable インターフェイスと IEnumerator インターフェイスを使用して Iterator パターンを実装します。Iterator パターンを使用すれば、コレクションの内部的な動作を公開することなく、コレクションをトラバースできます。IEnumerator のインプリメンタである Iterator クラスは、IEnumerable を実装するコレクションとは別のクラスです。Iterator クラスは、トラバーサルの状態 (現在のアイテムが何か、トラバース対象のアイテムが他にもあるかどうか) をコレクションの外部に保持します。トラバーサルのアルゴリズムは、Iterator にも含まれています。これにより、コレクション クラス自体を複雑化させることなく、まったく異なる方法で同じコレクションをトラバースする反復子を同時に複数使用することが可能になります。

Back to top

Decorator パターン

便利な実行可能プログラムでは、入力の読み取り、出力の書き込み、またはその両方が処理されます。読み取りまたは書き込みの対象となるデータのソースに関係なく、データは一連のバイトとして抽象的に処理することができます。.NET では、System.IO.Stream クラスを使ってこの抽象化を表します。テキスト ファイルの文字、TCP/IP ネットワーク トラフィック、その他の何がデータに関係していても、Stream を経由すればデータにアクセスできることはほぼ間違いありません。ファイル データを操作するためのクラス (FileStream) とネットワーク トラフィックに対応するためのクラス (NetworkStream) はいずれも Stream を継承するため、ソースに関係なくデータを処理するコードを簡単に記述できます。Stream からコンソールにバイトを書き出す 1 つの方法を次に示します。

public static void PrintBytes(Stream s)
{
    int b;
    while((b = fs.ReadByte()) >= 0)
    {
        Console.Write(b + " ");
    }
}

ストリームにアクセスするのに 1 バイトずつ読み込んでいくのは効率的な方法とは言えません。たとえば、ハード ドライブはディスクから連続するデータ ブロックを一気に読み込む能力があります (また、そのように最適化されています)。読み取る文字が複数あることがわかっている場合には、ディスクからチャンクをすべて一度に読み込んで、バイト単位でメモリからチャンクを消費するようにした方が効率的です。Framework には、この処理を行う BufferedStream クラスが含まれています。BufferedStream のコンストラクタには、バッファ アクセスするストリームをパラメータとして指定できます。BufferedStream は、Read や Write などの Stream の主なメソッドをオーバーライドして、追加の機能を提供します。BufferedStream は基本的に Stream の子クラスであるため、他の Stream と同じ方法で使用できます (FileStream には独自のバッファリング機能があります)。同様に、System.Security.Cryptography.CryptoStream を使用して、その場で Stream を暗号化および復号化できます。この場合、アプリケーションはそれが Stream であるということ以外は何も知る必要はありません。図 3 に、異なる Stream を使用した印刷メソッドの呼び出し例を示します。

図 4 Decorator パターンの使用例
図 4 Decorator パターンの使用例

コンポジションを使用して透過的に、新しい機能をオブジェクトに動的にアタッチする機能は、Decorator パターンの一例です (図 4 を参照)。Stream のどのインスタンスでも、BufferedStream にラップすれば、データへのインターフェイスを変更することなく、バッファ アクセスの機能を追加できます。ここではオブジェクトを構成するだけですから、コンパイル時に行う必要がある継承などのテクニックを使用することなく、実行時に行うことができます。コア機能は、インターフェイスまたはすべての Decorators の派生元となる抽象クラス (Stream など) によって定義されます。Decorator 自体はインターフェイスのメソッドを実装 (またはオーバーライド) して、追加の機能を提供します。たとえば、BufferedStream は Read をオーバーライドして、Stream から直接読み込むのではなく、ラップされた Stream によって書き込まれたバッファから読み込みを行います。図 3 に示すように、Decorator のコンポジションはどんなに複雑なものであっても、単なる基本クラスであるかのように使用することができます。

Back to top

Adapter パターン

.NET Framework の長所の 1 つが後方互換性です。.NET ベースのコードと従来の COM オブジェクトの間で簡単に呼び出しを行うことができます。Visual Studio .NET の [参照の追加] ダイアログを通じて参照を追加するだけで、プロジェクトで COM コンポーネントを使用できます。舞台裏では、Visual Studio .NET が tlbimp.exe ツールを呼び出し、相互運用アセンブリに含まれるメタデータを使用して、ランタイム呼び出し可能ラッパー (RCW: Runtime Callable Wrapper) クラスを作成します。参照を追加すると (かつ相互運用アセンブリが自動的に生成されると)、マネージ コードの他のクラスと同じように COM コンポーネントを使用できるようになります。参照リストを見ずに (かつクラスに関連付けられたメタデータやその実装を調べることなく)、誰かが書いたプログラムを見ても、.NET 向け言語で記述されたクラスと COM コンポーネントの見分けはつきません。

この秘密は RCW にあります。COM コンポーネントでは異なるエラー処理メカニズムを使用しており、使用されるデータ型も異なります。たとえば、.NET Framework の文字列では System.String クラスを使用するのに対して、COM では BSTR を使用することがあります。ただし、.NET ベースのコードから文字列パラメータを使って COM コンポーネントを呼び出すときには、他の同様のマネージ コード メソッドに渡すのとまったく同じように System.String を渡すことができます。RCS 内部では、この System.String は COM コンポーネントで期待される形式 (BSTR など) に変換されてから、COM の呼び出しが行われます。同様に、通常は COM コンポーネントでメソッドを呼び出すと、成功または失敗を示す HRESULT が返されます。COM メソッドを呼び出して、呼び出しが失敗したことを示す HRESULT が返された場合、RCW は他のマネージ コード エラーと同じように処理できるように、これを例外 (既定) に変換します。

マネージ クラスと COM コンポーネントがインターフェイスの違いを乗り越えて対話できるようにすることから、RCW は Adapter パターンの一例と言えます。Adapter パターンを使用すれば、1 つのインターフェイスを別のインターフェイスに適合させることができます。COM は System.String クラスを認識できないため、RCW はそれを COM が理解できるものに適合させます。レガシー コンポーネントの動作の仕組みを変えることはできなくても、対話することはできるわけです。Adapter は、しばしばこのように利用されます。

Adapter クラスそのものは Adaptee をラップし、クライアントからの呼び出しをすべて適切な形式と呼び出しのシーケンスに変換します。まるで Decorator と同じではないかと思われるかも知れませんが、実はいくつかの重要な違いがあります。Decorator の場合、構成するオブジェクトのインターフェイスは同じです。他方、Adapter の主な目的はインターフェイスを変更できるようにすることです。また、Adapter では明確な順序が決められています。つまり、Adapter には必ず Adaptee を含めなければなりません。Decorator クラスのインターフェイスはすべて同じですから、他の 1 つのクラスまたは 500 のクラスでラップされているかどうかを知る必要はありません。つまり、Decorator を使用してもアプリケーションからはわかりませんが、アプリケーションに知られずに Adapter を使用することはできません。

Back to top

Factory パターン

Framework では、コンストラクタを呼び出さなくても、構造体やクラスの新しいインスタンスを取得できる場合が多くあります。System.Convert クラスにはこのような働きをする静的メソッドが多数含まれています。たとえば、整数値をブール値に変換するには、Convert.ToBoolean を呼び出して整数に渡します。このメソッド呼び出しの戻り値は、整数がゼロ以外の場合は "true" に設定された新しいブール値で、それ以外の場合は "false" です。Convert クラスは、適切な値で自動的にブール値を作成します。その他の変換メソッドも同じように動作します。Int32 や Double に対して Parse メソッドを呼び出すと、文字列のみを指定した場合にはこれらのオブジェクトが適切な値に設定された新しいインスタンスが返されます。

新しいオブジェクト インスタンスを作成するためのこの手法は、Factory パターンと呼ばれます。オブジェクトのコンストラクタを呼び出すのではなく、オブジェクト ファクトリにインスタンスの作成を指示することができます。こうすれば、Factory クラスは (文字列から Double 型を解析する方法と同じように) オブジェクト作成の複雑性を隠蔽できます。オブジェクト作成の詳細を変更する場合には、ファクトリ自体を変更する必要があります。つまり、コード内のコンストラクタが呼び出される部分をすべて変更しなければなりません。

このタイプの変換メソッドは、対象のオブジェクトを作成するのにファクトリを使用する必要がないため、このパターンの 1 つのバリアントと言えます。このパターンのもっと純粋な例は、System.Net.WebRequest クラスです。このクラスは、インターネット上のリソースに対して要求を送信し、応答を受信するのに使用されます。FTP、HTTP、およびファイル システム要求が既定でサポートされます。要求を作成するには、Create メソッドを呼び出して URI に渡します。Create メソッドそのものは、要求に対応する適切なプロトコルを判断し、WebRequest の適切なサブクラス (HttpWebRequest、FtpWebRequest (.NET Framework 2.0 で導入)、または FileWebRequest)を返します。呼び出し側はファクトリの呼び出し方法と、返された WebRequest の使用方法がわかっていればよく、各プロトコルの詳細を知る必要はありません。HTTP アドレスから FTP アドレスに URI が変更された場合でも、コードを変更する必要は一切ありません。これは Factory パターンのもう 1 つの一般的な用途です。親クラスはファクトリとして機能し、クライアントから渡されたパラメータに基づいて特定の派生クラスを返します。WebRequest の例と同様に、この場合も、適切な派生クラスの選択に関わる複雑な詳細は呼び出し側からは見えません。

Back to top

Strategy パターン

Array と ArrayList はいずれも、Sort メソッドを通じて、コレクションに含まれているオブジェクトの並び替え機能を提供します。実際、ArrayList.Sort は基になる配列で Sort を呼び出すだけです。これらのメソッドは、QuickSort アルゴリズムを使用します。既定では、Sort メソッドは各要素に IComparable 実装を使用して、ソートに必要な比較を処理します。ただし、同じリストを異なる方法でソートできたら便利な場合もあります。たとえば、文字列の配列を大文字小文字の区別ありと区別なしでソートしたいとします。この場合、パラメータとして IComparer を取る Sort のオーバーロードが存在するため、比較には、IComparer.Compare が使用されます。このオーバーロードにより、クラスのユーザーは、Array、ArrayList、または QuickSort アルゴリズムの実装の詳細を変更したり、調べたりすることなく、組み込まれた IComparer または自分で作成した IComparer を使用することができます。

このようにどの比較アルゴリズムを選ぶかはクラスのユーザーに任せるのが、Strategy パターンの一例です。Strategy を使用すれば、さまざまなアルゴリズムを交互に使用できます。QuickSort 自体に必要なのは、オブジェクト同士を比較する方法だけです。提供されたインターフェイスを通じて Compare を呼び出すことで、呼び出し側はそれぞれのニーズに応じて自由に比較アルゴリズムを入れ替えることができます。QuickSort のコードは変更せずにそのまま使えます。

図 5 実行中の Strategy
図 5 実行中の Strategy

.NET Framework のバージョン 2.0 で導入された新しいジェネリック コレクションの 1 つ、List<T> でも、図 5 に示すように Strategy パターンが多用されています。更新された Sort メソッドに加えて、検索関連のメソッドである BinarySearch なども、呼び出し側のニーズに応じてアルゴリズムの一部を変更できるパラメータを取ります。FindAll<T> メソッドに Predicate<T> デリゲートを使用すると、呼び出し側は、List<T> にフィルタとして (オブジェクト型が適切で、ブール値を返すのであれば) 任意のメソッドを使用できます。匿名メソッド (バージョン 2.0 のもう 1 つの新しい C# 言語機能) と併用すれば、クライアントは、List<T> クラス自体への依存性を生じることなく、リストのオブジェクトのプロパティやメソッドに基づいて、簡単にリストをフィルタ処理できます。Strategy パターンを使用すれば、ソートなどの複雑なプロセスも、呼び出し側の目的に応じて簡単に変更できます。つまり、コードの記述や管理の負担が軽減されます。

Back to top

ASP.NET の Composite パターン

ASP.NET の要求/応答パイプラインは複雑なシステムです。パターンは、パイプライン自体の設計に使用されているだけなく、パフォーマンスと、プログラミングの拡張性および簡略さのバランスを効率的に取ることを目的として、コントロール アーキテクチャにも使用されています。ただし、パイプラインの説明に入る前に、プログラミング モデルに使用されているパターンについて説明しておきます。

オブジェクトのコレクションを操作する場合、1 つのオブジェクトとコレクション全体の両方に適切な操作があります。たとえば、ASP.NET コントロールです。コントロールは、Leteral のような簡単な 1 つのアイテムである場合と、DataGrid のように子コントロールが集まった複雑なコレクションで構成される場合があります。いずれにしても、どちらのコントロールで Render メソッドを呼び出しても、同じような直感的な機能が実行されます。

コレクションの各アイテムに他のオブジェクトのコレクションが含まれている場合には、Composite パターンを使用するのが適切です。Composite を使用すれば、親ノードやリーフ ノードを別個に扱うことなく、コレクションをツリー構造で簡単に表すことができます。

Composite の標準的な例は、抽象基本クラスである Component です。Component クラスには、子を追加・削除するためのメソッドと、親子の間での共通の操作が含まれています。ASP.NET では、この定式化をそのまま System.Web.UI.Control に使用します。Control は Component 基本クラスを表します。また、子を扱うための操作 (子の Control プロパティなど) や、標準の操作、Render や Visible などの標準のプロパティが含まれています。基本オブジェクト (Literal など) または複数のオブジェクトの複合体 (DataGrid など) に関わらず、各オブジェクトはこの基本クラスから継承します。

コントロールの領域は多様であるため、他のコントロールの基本クラスとしての役割を果たす WebControl や BaseDataList などの中間派生クラスがいくつか存在します。これらのクラスは追加のプロパティとメソッドを公開しますが、Control から継承された子の管理機能や主な操作は保持されます。実際、Composite パターンを使用すれば、必要に応じて複雑性を隠蔽できます。コントロールが Literal または DataGrid のどちらであっても、Composite を使えば、Render を呼び出すだけですべてが自動的に分類されます。

Back to top

Template Method パターン

ASP.NET コントロールの標準ライブラリではニーズを満たせない場合に、自分でコントロールを作成するにはいくつかの選択肢があります。1 つのプロジェクトだけで使用する簡単なコントロールが必要な場合には、ユーザー コントロールを選ぶのがベストです。複数の Web アプリケーションでコントロールを使用する場合や、コントロールにより多くの機能が必要な場合には、カスタム サーバー コントロールが適切です。

カスタム コントロールを使用する場合には、複数の既存のコントロールの持つ機能を複合したコントロール (コンポジット コントロールと呼ばれる) と独自の表示機能を持つコントロールという 2 つのタイプがあります。いずれのコントロールについても、作成する手順はほとんど同じです。コンポジット コントロールを作成するには、いずれかのコントロール基本クラス (Control や WebControl など) から継承する新しいクラスを作成し、CreateChildControls メソッドをオーバーライドします。このメソッドに、子コントロールのコレクション (Controls と呼ばれる) に機能を複合するコントロールを追加します。他のカスタム コントロールの場合は、代わりに Render をオーバーライドし、HtmlTextWriter パラメータを使ってコントロールに HTML を直接出力します。

どちらの形式のカスタム コントロールを選んだ場合でも、適切なタイミングで ViewState をロードして保存する、PostBack イベントの処理を可能にする、コントロール ライフサイクル イベントを適切な順序で発生させるなど、すべてのコントロールに共通な機能を実行するためのコードを記述する必要はありません。コントロールをどのようにロード、表示、またはアンロードするかを定義する主なアルゴリズムは、コントロール基本クラスに含まれています。

特定のコントロールの詳細は、コントロール アルゴリズム内の明確に定義された場所 (CreateChildControls メソッドまたは Render メソッド) で処理されます。これが Template Method パターンの一例です。図 6 に示すように、中核となるアルゴリズムの骨組みは基本クラスに定義されており、アルゴリズム自体に影響を及ぼすことなく、サブクラスを組み込むことで各自の詳細を追加できます。コンポジット コントロールとカスタム コントロールの全般的なライフサイクルは同じですが、結果としての画面に表示されるコントロールはまったく異なる場合があります。

図 6 Template Method パターン
図 6 Template Method パターン

このパターンは Strategy パターンとよく似ています。この 2 つのパターンの違いはスコープと方法論です。Strategy は、呼び出し側が 2 つのオブジェクトを比較する方法などのアルゴリズム全体を変えることができるようにするのに対して、Template Method はアルゴリズムのステップ (手順) を変えるために使用されます。このため、Strategy の方が粗粒です。クライアント実装は多様であるのに対して、Template Method ではアルゴリズムは基本的に同じです。もう 1 つの大きな違いは、Strategy ではデリゲートが使用されるのに大して、Template Method では継承が使用されることです。Strategy を使用したソート例では、比較アルゴリズムは IComparer パラメータにデリゲートされますが、カスタム コントロールの場合は、基本クラスをサブクラス化し、メソッドをオーバーライドして変更を処理します。ただし、各自のニーズに応じて簡単にプロセスを変更できる点は同じです。

Back to top

ASP.NET パイプラインのパターン

クライアントが Web サーバーから ASPX ページを要求すると、要求はいくつものステップを経て、クライアントのブラウザに HTML として表示されます。まず、要求が IIS によって処理され、適切な ISAPI 拡張機能に転送されます。ASP.NET の ISAPI 拡張機能 (aspnet_isapi.dll) は、要求を ASP.NET のワーカー プロセスに転送します。

図 7 ASP.NET の要求パイプライン
図 7 ASP.NET の要求パイプライン

この時点で、要求はみなさんがよく知っているクラスとの対話を開始します。要求は HttpApplication に渡されます。通常、HttpApplication は Global.asax のコードビハインド ファイルに作成されるクラスです。HttpApplication は要求を不定数の HTTP モジュールに渡します。これらのクラスは IHttpModule インターフェイスを実装し、次のモジュールに要求が渡されるまでに要求を変更 (または場合によっては要求の処理を中止) することがあります。ASP.NET には、FormsAuthenticationModule や PassportAuthenticationModule、WindowsAuthenticationModule、SessionStateModule など、みなさんが使い慣れている機能を提供する標準のモジュール が用意されています。これらのモジュールはそれぞれの名前が示すとおりの機能を提供します。

最終的に、要求は IHttpHandler に到達します。最も一般的な IHttpHandler は System.Web.UI.Page です。IHttpHandler.ProcessRequest メソッドでは、Page が適切なイベント (Init、Load、Render など) を発生させ、ViewState を処理して、ASP.NET のプログラミング モデルを提供します。図 7 にこのプロセスの概略を示します。

このプロセスで使用されるいくつかのパターンについては、パターンに関するもう 1 冊の標準的な参考書である Martin Fowler 著の『Patterns of Enterprise Application Architecture』(Addison-Wesley, 2002) で詳しく説明されています。

Back to top

Intercepting Filter パターン

要求が HttpApplication に到達すると、不定数の IHttpModule に渡されていきます。モジュールはそれぞれ独立しており、モジュールの呼び出し順序を制御する方法はごく限られています。HttpApplication クラスは、要求が処理される間に発生した一連のイベントを公開します。これらのイベントには、BeginRequest、AuthenticateRequest、AuthorizeRequest、EndRequest が含まれます。HttpApplication はモジュールをロードすると、IHttpModule インターフェイスの Init メソッドを呼び出します。これにより、モジュールは関連するイベントに登録できるようになります。要求が処理されると、適切な順序でイベントが発生し、登録されたすべてのモジュールに要求と対話する機会が与えられます。つまり、モジュールは呼び出されるステージを制御することはできても、そのステージの正確な順序を制御することはできません。

これらのモジュールは、Intercepting Filter パターンの一例です。このパターンは、通過する要求 (またはメッセージ) を変更する機会を順に与えられる一連のフィルタを表します。図 8 は、このプロセスの流れを簡略化したものです。このパターンのポイントは、フィルタが独立しており、通過する要求を変更できるという点です。

図 8 要求の流れ
図 8 要求の流れ

Intercepting Filter パターンにはいくつかの実装バリエーションがありますが、その 1 つが ASP.NET で使用されているイベントベースのモデルです。簡略化すれば、フィルタのリストを保持し、そのフィルタを反復処理して、順にメソッドを呼び出していくという形態になります。ASP.NET Web サービスの Web Services Enhancements (WSE) では、この方法で Intercepting Filter パターンを使用しています。各フィルタは、SoapInputFilter (要求メッセージ用) または SoapOutputFilter (応答用) を拡張し、ProcessMessage メソッドをオーバーライドして、フィルタの機能を実行します。

もう 1 つの選択肢は、Decorator パターンを通じて Intercepting Filter を実装する方法です。この場合、各フィルタが後続フィルタをラップし、前処理を実行してから後続フィルタを呼び出し、後処理を実行します。この一連の流れは、後方から前方への再帰的な複合を使用して組み立てられます。このパターンは、.NET リモート処理のチャネル シンクの実装に使用されます。

実装に関わらず、動的に構成可能な一連の独立したフィルタができあがります。フィルタは独立しているため、簡単に順序を変更したり、他のアプリケーションで再利用したりできます。認証やログの記録などの一般的なタスクをフィルタにカプセル化して、何度も使用できます。こうすれば、要求が HttpHandler に到達する前にでもフィルタ チェーンでタスクを処理できるため、ハンドラ コードをすっきりさせることができます。

Back to top

Page Controller パターン

System.Web.UI.Page は、ASP.NET のプログラミング モデルの中核部を実装します。Web アプリケーションに論理ページを追加するには、Web フォーム (ASPX ファイルとそのコードビハインドで表される) を作成します。次に、ページレベルのイベントを処理する、一連のコントロールを表示する、またはデータをロードして操作する、などの方法を通じて、新しいページに必要なものを処理するためのコードを記述します。アプリケーションの各論理ページには、動作を制御し、表示を統制するための対応する Web フォームがあります。

論理ページごとに 1 つのコントローラを持たせるのは、Page Controller パターンの一例です。この考え方は ASP.NET の基本です。論理ページがその URI を通じて要求されると、ASP.NET ランタイムがそのアドレスを対応する Page のサブクラスに解決し、そのクラスを使って要求を処理します。ページがどのように表示されるか、処理可能なユーザー入力は何か、その入力に対してどのように反応するかなどの詳細はすべて、1 か所にまとめられます。アプリケーションの論理ページを変更する必要が生じた場合でも、他のページは影響を受けません。これはあまりにも一般的な抽象化であるため、かえって思いつきません。

Page Controller の純粋な実装に供なう短所は、共通のコードをページごとに繰り返さなければならないことです。 ASP.NET では、パイプライン実装に他のパターンを含め、すべての Page Controller の共通の基本クラスとして System.Web.UI.Page を提供することでこの欠点を回避しています。認証やセッション状態などの横断的関心事 (CCC: Cross-Cutting Concerns) は、HttpModule Intercepting Filter で処理され、ページ ライフサイクル イベントの発生やその他のアクティビティは基本クラスで処理されます。

Back to top

ASP.NET のその他の Web プレゼンテーション パターン

Intercepting Filter や Page Controller に加えて、ASP.NET では他のいくつかの Web プレゼンテーション パターンのバリエーションが使用されています。要求を渡す HttpHandler を判断する際に、ASP.NET は Front Controller のようなものを使用します。Front Controller の特徴は、(System.Web.UI.Page と同様に)すべての要求に 1 つのハンドラで対応することです。ただし、要求が Page クラスに到達すると、Page Controller が処理を引き継ぎます。

Page Controller の ASP.NET 実装には、Model View Controller パターンの要素が存在します。Model View Controller はモデル (ビジネス オブジェクト、データ、プロセス) をビュー (情報の表示) から切り離します。コントローラはユーザー入力に反応して、モデルとビューを更新します。おおざっぱに言うと、ASPX ページはビューを表すのに対して、そのコードビハインド ファイルはハイブリッドなモデル/コントローラを表します。ビジネス関連のコードとデータ関連のコードをすべてコードビハインドから取り出して、イベント処理コードだけを残すと、コードビハインドは純粋な Controller になり、ビジネス ロジックが含まれる新しいクラスはモデルになります。

これらのバリエーションはパターンの標準的な形態とは異なるため、ここでは説明しません。詳細は、Martin Fowler の著作本と Microsoft Patterns サイトを参照することをお勧めします。

Back to top

まとめ

この記事では、.NET Framework と BCL 全般で使用されている一般的なパターンについて説明しました。これで、毎日見ているコードからも同じパターンを簡単に認識できるようになるでしょう。一般的なクラスや機能の基底にあるデザイン パターンを取り上げたことで、これらのパターンとは何か、そしてそれがもたらすメリットについてもご理解頂けたことと思います。Observer パターンを使用しない UI プログラミングや、Iterator を使用しないパターンやコレクションを想像してみれば、これらのフレームワークがどんなに不可欠なものであるかがおわかりになると思います。各パターンの役割を理解できれば、みなさんのツールボックスに便利なツールがもう 1 つ追加されることになります。

Back to top


Rob Pierry サービス指向アーキテクチャを専門とする有数の IT コンサルティング企業、Geniant 社のシニア コンサルタント。連絡先は rpierry+msdn@gmail.com (英語)。

 この記事は、 MSDN マガジン - 2005 年 7 月号からの翻訳です。 .
Back to topBack to top QJ: 050705

Microsoft