Chain of Responsibility
一言でいうと
Chain of Responsibility (責任の連鎖) は、 振る舞いに関するデザインパターンの一つで、 ハンドラーの連鎖に沿ってリクエストを渡すことができます。 各ハンドラーは、 リクエストを受け取ると、 リクエストを処理するか、 連鎖内の次のハンドラーに渡すかを決めます。
問題
オンライン注文システムの開発に取り組んでいるところを想像してみてください。 認証されたユーザーのみが注文を作成できるように、 システムへのアクセスを制限したいとします。 また、 管理者権限を持つユーザーは、 すべての注文へ完全にアクセスできる必要があります。
少し計画した後、 これらのチェックは順番に実行されなければならないことに気づきました。 アプリケーションが、 ユーザーの資格情報を含むリクエストを受信するたびに、 システムへのユーザー認証を試みようとすることは可能です。 しかし、 それらの資格情報が正しくなく、 認証に失敗した場合、 他のチェックを続行する理由はありません。
次の数か月間をかけて、 さらに以下のような一連のチェックを実装しました。
-
同僚の一人が、 生データを直接注文システムに渡すのは危険だと言いました。 リクエスト中のデータをサニタイズするための副次的検証ステップを追加しました。
-
その後、 システムが力づくのパスワード破り攻撃に対して脆弱であることに誰かが気づきました。 これに対処するために、 同じ IP アドレスからのリクエストが繰り返し失敗したことを検知するチェックを速やかに追加しました。
-
同じデータを含むリクエストが繰り返し来た場合、 キャッシュした結果を返せば、 システムのスピードの向上が可能なのではないかと、 別の誰かが提言しました。 そのようなわけで、 適切なキャッシュした応答がない場合にのみ、 リクエストをシステムに通すチェックを追加しました。
元々あまりきれいではなかったチェックのコードは、 新機能を追加するたびに膨張しました。 あるチェック項目の変更が、 他のチェックに影響を与えることもありました。 さらに悪いことに、 システムの他のコンポーネントを守るためにチェックを再利用しようとすると、 それらのコンポーネントはいくつかの、 しかし全部ではないチェックを必要とするため、 コードの一部を重複しなければなりませんでした。
システムを理解するのが非常に難しくなり、 保守コストが上がりました。 コードでしばらく悪戦苦闘した後のある日、 全体をリファクタリングすることに決めました。
解決策
他の多くの振る舞いに関するデザインパターンと同様、 Chain of Responsibility の根幹は、 特定の振る舞いを ハンドラーと呼ばれる独立したオブジェクトに転換することです。 我々の場合、 各チェックを、 独自のクラスのチェックを実行するメソッドに抽出する必要があります。 リクエストは、 そのデータとともにこのメソッドに引数として渡されます。
パターンに従うと、 これらのハンドラーはリンクされて連鎖となります。 リンクされた各ハンドラーには、 連鎖内の次のハンドラーへの参照を格納するためのフィールドがあります。 リクエストの処理に加えて、 ハンドラーはリクエストを連鎖にそって次のハンドラーに渡します。 リクエストは、 すべてのハンドラーがそれを処理する機会を得るまで、 連鎖に沿って移動して行きます。
そして一番の特長: ハンドラーはリクエストを連鎖上で次に渡さないことを決定し、 結果としてそれ以降の処理を停止することができます。
注文システムの例では、 ハンドラーが一つずつ処理を行い、 リクエストを連鎖の先に渡すかどうかを決めます。 リクエストに適切なデータが含まれていると仮定して、 すべてのハンドラーは、 その主要任務を実行することができます。 それは、 認証チェックだったりキャッシングかもしれません。
しかし、 少々異なった (そしてもう少し規範に沿った) やり方があります。 ハンドラーはリクエストを受け取るとそれを自分で処理できるかどうかを決めます。 もし処理可能であれば、 これ以上リクエストを先に渡しません。 ですから、 リクエストは、 一つのハンドラーで処理されるか、 まったくされないかのどちらかです。 このやり方は、 グラフィカル・ユーザー・インターフェース内上の積み重なった要素のイベント処理で非常に一般的です。
たとえば、 ユーザーがボタンをクリックすると、 ボタンから始まる GUI 要素の連鎖を通してイベントが伝播し、 (フォームやパネルのような) コンテナに行き、 最後にメインのアプリケーション・ウィンドウに到達します。 イベントは連鎖内で最初に処理可能な要素によって処理されます。 この例のもう一つ注目すべき点は、 連鎖は常にオブジェクト・ツリーから抽出できる、 ということです。
すべてのハンドラー・クラスが同じインターフェースを実装するということが大変重要です。 それぞれの具象ハンドラーは、 次のハンドラーが、 execute
メソッドを持っている、 ということだけ気にかけています。 これにより、 コードを具象クラスに結合することなく、 様々なハンドラーを使用した連鎖を実行時に構成することができます。
現実世界でのたとえ
新しい何らかのハードウェア部品を購入し、 コンピュータに取り付けたとします。 あなたはオタクなので、 コンピュータにはいくつかのオペレーティングシステムがインストールされています。 ハードウェアがすべての OS でサポートされているかどうかを確認するため、 全部ブートしようと試みます。 Windows は自動的にハードウェアを検出し、 有効化しました。 ところが、 愛する Linuxは 新しいハードウェアと一緒に動くことを拒否します。 あまり希望はもてませんが、 ダメモトで、 箱に書いてある技術サポートの番号に電話してみることにしました。
最初に耳にするのは、 音声自動応答装置のロボットの声です。 様々な問題に対する 9 つの一般的な解決策を提示しますが、 どれも自分の事例にあてはまりません。 しばらくすると、 ロボットは人間の担当者に接続します。
残念ながら、 担当者はちっとも役に立つ提言ができません。 マニュアルに書いてあることの一部を引用し続け、 あなたの意見に耳を傾けようとしません。 「コンピューターの電源を一旦オフにしてからオンにしてみましたか?」 というセリフを 10 回聞いた後で、 適切なエンジニアと話をしたいと要求します。
最終的に、 その担当者は、 エンジニアの一人に内線転送します。 雑居ビルの暗い地下室の孤独なサーバー・ルームに座りながら、 生きた人間との会話をずっと待ち望んでいたエンジニアは、 新しいハードウェアに適切なドライバーをどこからダウンロードし、 どう Linux にインストールすればいいかを教えてくれました。 ついに、 解決策にありつけた! 喜びに満ちて通話を切ることができました。
構造
-
ハンドラー (Handler) は、 すべての具象ハンドラーに共通したインタフェースを宣言します。 通常は、 リクエストを処理するためのメソッド一つだけを含んでいますが、 時には連鎖上の次のハンドラーを設定するための別のメソッドを含んでいることもあります。
-
基底ハンドラー (Base Handler) は、 ある場合とない場合があります。 すべてのハンドラー・クラスに共通する定型のコードを入れることができます。
通常、 このクラスには次のハンドラーへの参照を格納するためのフィールドが定義されています。 クライアントは、 ハンドラーを、 その手前のハンドラーのコンストラクターか setter に渡すことで、 連鎖を構築できます。 クラスは、 次のハンドラーがあることを確認した後、 実行を次に移すといった、 デフォルトの処理動作を実装することもできます。
-
具象ハンドラー (Concrete Handler) にはリクエストを処理するための実際のコードが含まれています。 リクエストを受け取ると、 各ハンドラーはそれを処理するかどうか、 さらには連鎖に沿ってそれを渡すかどうかを決める必要があります。
ハンドラーは通常、 自己完結型かつ不変であり、 コンストラクターを通して必要なすべてのデータを一度だけ受け入れます。
-
クライアント (Client) は、 一度だけ連鎖を構成するかもしれませんし、 動的に構成するかもしれません。 これは、 アプリケーションのロジックによります。 リクエストは連鎖内のいずれのハンドラーにも送ることができます。 最初のハンドラーとは限りません。
擬似コード
この例では、 Chain of Responsibility パターンを使い、 アクティブな GUI 要素の状況依存ヘルプ情報を表示します。
アプリケーションの GUI は通常、 オブジェクト・ツリーの構造をしています。 たとえば、 アプリのメイン・ウィンドウを描画する Dialog
クラスはオブジェクト・ツリーの根本となります。 ダイアログは Panel
を複数含み、 それはさらに他のパネルや、 Buttons
や TextFields
などの単純な低レベルの要素を含んでいるかもしれません。
単純なコンポーネントは、 それに割り当てられたヘルプテキストがある限り、 簡単な状況依存のツールチップを表示できます。 しかし、 もっと複雑なコンポーネントは、 マニュアルの抜粋を表示するとか、 ブラウザーでページを開くとか、 ヘルプを表示する独自の方法を定義します。
ユーザーがある GUI 要素にマウス・カーソルをあててから F1
キーを押すと、 アプリケーションはポインターの下のコンポーネントを検出し、 それにヘルプ・リクエストを送ります。 リクエストは、 情報を表示できる要素に到達するまで、 各要素のコンテナを通って泡のように上昇して行きます。
適応性
プログラムが様々な種類のリクエストを様々な方法で処理しなければいけないが、 正確にどういうリクエストがどういう順番で来るかが前もって予想できない場合、 Chain of Responsibility を適用します。
このパターンにより、 複数のハンドラーを一つの連鎖にリンクすることができます。 リクエストを受け取ると各ハンドラーに処理できるかどうかを 「問い合わせ」 ます。 これにより、 すべてのハンドラーはリクエストを処理する機会を得ることができます。
特定の順序で複数のハンドラーを実行することが必須な場合に、 パターンを使用します。
連鎖内のハンドラーを好きな順番にリンクできるので、 すべてのリクエストは計画通りに連鎖を通過します。
ハンドラの組み合わせと順序が実行時に変更されることが想定されている場合に、 CoR パターンを使用します。
ハンドラー・クラス内の参照フィールド用の setter を提供すれば、 ハンドラーを動的に挿入、 削除、 順番変更することが可能となります。
実装方法
-
ハンドラー・インターフェースを宣言し、 そこでリクエストを処理するメソッドのシグネチャーを記述します。
クライアントがリクエスト・データをどうにメソッドに渡すかを決めます。 最も柔軟な方法は、 リクエストをオブジェクトに変換し、 それを処理メソッドに引数として渡すことです。
-
具象ハンドラー内同士の定形コードの重複を排除するために、 ハンドラー・インターフェースから派生した抽象基底クラスを作成する価値があるかもしれません。
このクラスには、 連鎖内の次のハンドラーへの参照を格納するためのフィールドが必要です。 クラスを変更不可にすることを検討してください。 ただし、 実行時に連鎖を変更する計画がある場合は、 参照フィールドの値を変更する setter を定義する必要があります。
次のオブジェクトがある限りそこへリクエストを転送することを、 処理メソッドの便利なデフォルトとして実装することもできます。
-
ハンドラーの具象サブクラスの作成と、 その処理メソッドを一つずつ実装していきます。 各ハンドラーは、 リクエストを受け取る時に以下の二つの事項を決定をする必要があります:
- リクエストを処理すべきか。
- 連鎖に沿ってリクエストを渡すべきか。
-
クライアントは、 それ自身で連鎖を組み立てるかもしれませんし、 または他のオブジェクトから事前構築済みの連鎖を受け取ることもできます。 後者の場合、 構成または環境設定に従って連鎖を構築するためのファクトリー・クラスをいくつか実装する必要があります。
-
クライアントは、 最初のハンドラーに限らず、 連鎖内のどのハンドラーからでも、 処理を開始できます。 リクエストは、 あるハンドラーが次に渡すことを拒否するか、 あるいは連鎖の最後に到達するまで、 連鎖沿いに渡されます。
-
連鎖は動的に変化するので、 クライアントは次のような状況に対処できる必要があります:
- 連鎖には、 リンクが一つしかない。
- 一部のリクエストは連鎖の最後に到達しないかもしれない。
- 他のリクエストは処理されないまま、 連鎖の最後に到達するかもしれない。
長所と短所
- リクエスト処理の順序を制御できる。
- 単一責任の原則。 処理を起動するクラスを、 実際に処理をするクラスから分離可能。
- 開放閉鎖の原則。 新規ハンドラーをアプリに導入しても、 既存クライアント側コードは動作する。
- 一部のリクエストが未処理で終わる可能性。
他のパターンとの関係
-
Chain of Responsibility と Command と Mediator と Observer は、 リクエストの送り手と受け手を接続する様々な方法を示します:
- Chain of Responsibility は、 潜在的受け手の動的な連鎖に沿って、 どれか一つが処理するまで、 リクエストを順番に渡します。
- Command は、 送り手と受け手との間で単方向の接続を確立します。
- Mediator は、 送り手と受け手の間の直接の接続を削除し、 メディエーター・オブジェクトを介しての間接的通信を強制します。
- Observer では、 受け手が動的にリクエストの受信申し込みをしたり、 申し込み取り消しをしたりできます。
-
Chain of Responsibility は、 よく Composite と一緒に使われます。 この場合、 リーフ (末端) のコンポーネントがリクエストを受ける時、 リクエストは、 全部の親コンポーネントからオブジェクト・ツリーのルート (根) までを通るかもしれません。
-
Chain of Responsibility のハンドラーは、 Commands で実装可能です。 この場合、 リクエストに代表される同一のコンテキスト・オブジェクトに対して多くの異なる処理を実行できます。
しかしもう一つのやり方は、 リクエスト自身をコマンド・オブジェクトとすることです。 この場合、 連鎖にリンクされた異なる一連のコンテキスト中で同じ処理を実行できます。
-
Chain of Responsibility と Decorator とは非常によく似た階層構造をしています。 両パターンとも、 一連のオブジェクトを通して実行を渡すために、 再起的合成に依存します。 しかしながら、 いくつかの重要な違いがあります。
CoR ハンドラーは、 互いに独立して任意の処理を実行可能です。 また、 任意の時点でそれ以上リクエストを渡すのを止めることもできます。 一方、 様々な Decorator では、 オブジェクトの振る舞いの拡張をする時、 基底インターフェースとの一貫性を保つ必要があります。 さらに、 デコレーターはリクエストの流れを断ち切ることは許されていません。