Autumn SALE

Chain of Responsibility

別名:CoR、Chain of Command、責任のチェーン、責任の連鎖、コマンドのチェーン、コマンドの連鎖

一言でいうと

Chain of Responsibility 責任の連鎖 振る舞いに関するデザインパターンの一つで ハンドラーの連鎖に沿ってリクエストを渡すことができます 各ハンドラーは リクエストを受け取ると リクエストを処理するか 連鎖内の次のハンドラーに渡すかを決めます

Chain of Responsibility デザインパターン

問題

オンライン注文システムの開発に取り組んでいるところを想像してみてください 認証されたユーザーのみが注文を作成できるように システムへのアクセスを制限したいとします また 管理者権限を持つユーザーは すべての注文へ完全にアクセスできる必要があります

少し計画した後 これらのチェックは順番に実行されなければならないことに気づきました アプリケーションが ユーザーの資格情報を含むリクエストを受信するたびに システムへのユーザー認証を試みようとすることは可能です しかし それらの資格情報が正しくなく 認証に失敗した場合 他のチェックを続行する理由はありません

Chain of Responsibility によって解決できる問題

リクエストは 注文システムに到達する前に 一連のチェックを通過しなければならない

次の数か月間をかけて さらに以下のような一連のチェックを実装しました

  • 同僚の一人が 生データを直接注文システムに渡すのは危険だと言いました リクエスト中のデータをサニタイズするための副次的検証ステップを追加しました

  • その後 システムが力づくのパスワード破り攻撃に対して脆弱であることに誰かが気づきました これに対処するために 同じ IP アドレスからのリクエストが繰り返し失敗したことを検知するチェックを速やかに追加しました

  • 同じデータを含むリクエストが繰り返し来た場合 キャッシュした結果を返せば システムのスピードの向上が可能なのではないかと 別の誰かが提言しました そのようなわけで 適切なキャッシュした応答がない場合にのみ リクエストをシステムに通すチェックを追加しました

新規チェック追加のたびに、コードは巨大でゴチャゴチャして醜くなりました。

コードが大きくなるにつれ それはゴチャゴチャしてきた

元々あまりきれいではなかったチェックのコードは 新機能を追加するたびに膨張しました あるチェック項目の変更が 他のチェックに影響を与えることもありました さらに悪いことに システムの他のコンポーネントを守るためにチェックを再利用しようとすると それらのコンポーネントはいくつかの しかし全部ではないチェックを必要とするため コードの一部を重複しなければなりませんでした

システムを理解するのが非常に難しくなり 保守コストが上がりました コードでしばらく悪戦苦闘した後のある日 全体をリファクタリングすることに決めました

解決策

他の多くの振る舞いに関するデザインパターンと同様 Chain of Responsibility の根幹は 特定の振る舞いを と呼ばれる独立したオブジェクトに転換することです 我々の場合 各チェックを 独自のクラスのチェックを実行するメソッドに抽出する必要があります リクエストは そのデータとともにこのメソッドに引数として渡されます

パターンに従うと これらのハンドラーはリンクされて連鎖となります リンクされた各ハンドラーには 連鎖内の次のハンドラーへの参照を格納するためのフィールドがあります リクエストの処理に加えて ハンドラーはリクエストを連鎖にそって次のハンドラーに渡します リクエストは すべてのハンドラーがそれを処理する機会を得るまで 連鎖に沿って移動して行きます

そして一番の特長 ハンドラーはリクエストを連鎖上で次に渡さないことを決定し 結果としてそれ以降の処理を停止することができます

注文システムの例では ハンドラーが一つずつ処理を行い リクエストを連鎖の先に渡すかどうかを決めます リクエストに適切なデータが含まれていると仮定して すべてのハンドラーは その主要任務を実行することができます それは 認証チェックだったりキャッシングかもしれません

ハンドラーは、列をなして連鎖を作る。

ハンドラーは 列をなして連鎖を作る

しかし 少々異なった そしてもう少し規範に沿った やり方があります ハンドラーはリクエストを受け取るとそれを自分で処理できるかどうかを決めます もし処理可能であれば これ以上リクエストを先に渡しません ですから リクエストは 一つのハンドラーで処理されるか まったくされないかのどちらかです このやり方は グラフィカル・ユーザー・インターフェース内上の積み重なった要素のイベント処理で非常に一般的です

たとえば ユーザーがボタンをクリックすると ボタンから始まる GUI 要素の連鎖を通してイベントが伝播し フォームやパネルのような コンテナに行き 最後にメインのアプリケーション・ウィンドウに到達します イベントは連鎖内で最初に処理可能な要素によって処理されます この例のもう一つ注目すべき点は 連鎖は常にオブジェクト・ツリーから抽出できる ということです

連鎖はオブジェクト・ツリーの枝から形成可能。

連鎖はオブジェクト・ツリーの枝から形成可能

すべてのハンドラー・クラスが同じインターフェースを実装するということが大変重要です それぞれの具象ハンドラーは 次のハンドラーが execute メソッドを持っている ということだけ気にかけています これにより コードを具象クラスに結合することなく 様々なハンドラーを使用した連鎖を実行時に構成することができます

現実世界でのたとえ

技術サポートと話すのは大変

技術サポートへの通話は 複数の担当者に回されるかもしれない

新しい何らかのハードウェア部品を購入し コンピュータに取り付けたとします あなたはオタクなので コンピュータにはいくつかのオペレーティングシステムがインストールされています ハードウェアがすべての OS でサポートされているかどうかを確認するため 全部ブートしようと試みます Windows は自動的にハードウェアを検出し 有効化しました ところが 愛する Linuxは 新しいハードウェアと一緒に動くことを拒否します あまり希望はもてませんが ダメモトで 箱に書いてある技術サポートの番号に電話してみることにしました

最初に耳にするのは 音声自動応答装置のロボットの声です 様々な問題に対する 9 つの一般的な解決策を提示しますが どれも自分の事例にあてはまりません しばらくすると ロボットは人間の担当者に接続します

残念ながら 担当者はちっとも役に立つ提言ができません マニュアルに書いてあることの一部を引用し続け あなたの意見に耳を傾けようとしません コンピューターの電源を一旦オフにしてからオンにしてみましたか? というセリフを 10 回聞いた後で 適切なエンジニアと話をしたいと要求します

最終的に その担当者は エンジニアの一人に内線転送します 雑居ビルの暗い地下室の孤独なサーバー・ルームに座りながら 生きた人間との会話をずっと待ち望んでいたエンジニアは 新しいハードウェアに適切なドライバーをどこからダウンロードし どう Linux にインストールすればいいかを教えてくれました ついに 解決策にありつけた 喜びに満ちて通話を切ることができました

構造

Chain of Responsibility デザインパターンの構造Chain of Responsibility デザインパターンの構造
  1. ハンドラー Handler すべての具象ハンドラーに共通したインタフェースを宣言します 通常は リクエストを処理するためのメソッド一つだけを含んでいますが 時には連鎖上の次のハンドラーを設定するための別のメソッドを含んでいることもあります

  2. 基底ハンドラー Base Handler ある場合とない場合があります すべてのハンドラー・クラスに共通する定型のコードを入れることができます

    通常 このクラスには次のハンドラーへの参照を格納するためのフィールドが定義されています クライアントは ハンドラーを その手前のハンドラーのコンストラクターか setter に渡すことで 連鎖を構築できます クラスは 次のハンドラーがあることを確認した後 実行を次に移すといった デフォルトの処理動作を実装することもできます

  3. 具象ハンドラー Concrete Handler にはリクエストを処理するための実際のコードが含まれています リクエストを受け取ると 各ハンドラーはそれを処理するかどうか さらには連鎖に沿ってそれを渡すかどうかを決める必要があります

    ハンドラーは通常 自己完結型かつ不変であり コンストラクターを通して必要なすべてのデータを一度だけ受け入れます

  4. クライアント Client 一度だけ連鎖を構成するかもしれませんし 動的に構成するかもしれません これは アプリケーションのロジックによります リクエストは連鎖内のいずれのハンドラーにも送ることができます 最初のハンドラーとは限りません

擬似コード

この例では Chain of Responsibility パターンを使い アクティブな GUI 要素の状況依存ヘルプ情報を表示します

Chain of Responsibility デザインパターンの例の構造

GUI のクラスは Composite パターンに従って構築されていて 各要素は そのコンテナ要素にリンクされている いつでも 要素自体から始まり それを含むすべてのコンテナ要素を通過する GUI 要素の連鎖が構築可能

アプリケーションの GUI は通常 オブジェクト・ツリーの構造をしています たとえば アプリのメイン・ウィンドウを描画する Dialog クラスはオブジェクト・ツリーの根本となります ダイアログは Panel を複数含み それはさらに他のパネルや ButtonsText­Fields などの単純な低レベルの要素を含んでいるかもしれません

単純なコンポーネントは それに割り当てられたヘルプテキストがある限り 簡単な状況依存のツールチップを表示できます しかし もっと複雑なコンポーネントは マニュアルの抜粋を表示するとか ブラウザーでページを開くとか ヘルプを表示する独自の方法を定義します

Chain of Responsibility 例の構造

ヘルプ・リクエストが GUI オブジェクトをたどって行く様子

ユーザーがある GUI 要素にマウス・カーソルをあててから F1 キーを押すと アプリケーションはポインターの下のコンポーネントを検出し それにヘルプ・リクエストを送ります リクエストは 情報を表示できる要素に到達するまで 各要素のコンテナを通って泡のように上昇して行きます

// ハンドラーのインターフェースは、リクエストを実行するためのメソッドを宣
// 言。
interface ComponentWithContextualHelp is
    method showHelp()


// 単純なコンポーネントのための基底クラス。
abstract class Component implements ComponentWithContextualHelp is
    field tooltipText: string

    // コンポーネントのコンテナは、ハンドラーの連鎖の中で次へのリンクとし
    // て機能。
    protected field container: Container

    // コンポーネントは、ヘルプ・テキストが指定されている場合は、ツール
    // チップを表示。そうでない場合は、呼び出しをコンテナに(もしあれば)
    // 転送。
    method showHelp() is
        if (tooltipText != null)
            // ツールチップを表示。
        else
            container.showHelp()


// コンテナは、単純なコンポーネントと他のコンポーネントの両方を子として含
// めることができる。連鎖関係はここで発生する。クラスは、showHelp の振る
// 舞いを親から引き継ぐ。
abstract class Container extends Component is
    protected field children: array of Component

    method add(child) is
        children.add(child)
        child.container = this


// 基本コンポーネントに対しては、デフォルトのヘルプの実装で十分かもしれな
// い。
class Button extends Component is
    // ……

// しかし、複雑なコンポーネントの場合は、デフォルトの実装を上書きするかも
// しれない。ヘルプ文が新規の方法で提供できない場合、コンポーネントは常に
// 基底クラスの実装を呼ぶことができる(Component クラス参照)。
class Panel extends Container is
    field modalHelpText: string

    method showHelp() is
        if (modalHelpText != null)
            // ヘルプ文をモーダル・ウィンドウで表示。
        else
            super.showHelp()

// ……同上……
class Dialog extends Container is
    field wikiPageURL: string

    method showHelp() is
        if (wikiPageURL != null)
            // Wiki のヘルプ・ページを開く。
        else
            super.showHelp()


// クライアント・コード。
class Application is
    // 連鎖の構成の方法はアプリによって異なる。
    method createUI() is
        dialog = new Dialog("Budget Reports")
        dialog.wikiPageURL = "http://……"
        panel = new Panel(0, 0, 400, 800)
        panel.modalHelpText = "This panel does……"
        ok = new Button(250, 760, 50, 20, "OK")
        ok.tooltipText = "This is an OK button that……"
        cancel = new Button(320, 760, 50, 20, "Cancel")
        // ……
        panel.add(ok)
        panel.add(cancel)
        dialog.add(panel)

    // 何が起きるか考えてみてください。
    method onF1KeyPress() is
        component = this.getComponentAtMouseCoords()
        component.showHelp()

適応性

プログラムが様々な種類のリクエストを様々な方法で処理しなければいけないが 正確にどういうリクエストがどういう順番で来るかが前もって予想できない場合 Chain of Responsibility を適用します

このパターンにより 複数のハンドラーを一つの連鎖にリンクすることができます リクエストを受け取ると各ハンドラーに処理できるかどうかを 問い合わせ ます これにより すべてのハンドラーはリクエストを処理する機会を得ることができます

特定の順序で複数のハンドラーを実行することが必須な場合に パターンを使用します

連鎖内のハンドラーを好きな順番にリンクできるので すべてのリクエストは計画通りに連鎖を通過します

ハンドラの組み合わせと順序が実行時に変更されることが想定されている場合に CoR パターンを使用します

ハンドラー・クラス内の参照フィールド用の setter を提供すれば ハンドラーを動的に挿入 削除 順番変更することが可能となります

実装方法

  1. ハンドラー・インターフェースを宣言し そこでリクエストを処理するメソッドのシグネチャーを記述します

    クライアントがリクエスト・データをどうにメソッドに渡すかを決めます 最も柔軟な方法は リクエストをオブジェクトに変換し それを処理メソッドに引数として渡すことです

  2. 具象ハンドラー内同士の定形コードの重複を排除するために ハンドラー・インターフェースから派生した抽象基底クラスを作成する価値があるかもしれません

    このクラスには 連鎖内の次のハンドラーへの参照を格納するためのフィールドが必要です クラスを変更不可にすることを検討してください ただし 実行時に連鎖を変更する計画がある場合は 参照フィールドの値を変更する setter を定義する必要があります

    次のオブジェクトがある限りそこへリクエストを転送することを 処理メソッドの便利なデフォルトとして実装することもできます

  3. ハンドラーの具象サブクラスの作成と その処理メソッドを一つずつ実装していきます 各ハンドラーは リクエストを受け取る時に以下の二つの事項を決定をする必要があります

    • リクエストを処理すべきか
    • 連鎖に沿ってリクエストを渡すべきか
  4. クライアントは それ自身で連鎖を組み立てるかもしれませんし または他のオブジェクトから事前構築済みの連鎖を受け取ることもできます 後者の場合 構成または環境設定に従って連鎖を構築するためのファクトリー・クラスをいくつか実装する必要があります

  5. クライアントは 最初のハンドラーに限らず 連鎖内のどのハンドラーからでも 処理を開始できます リクエストは あるハンドラーが次に渡すことを拒否するか あるいは連鎖の最後に到達するまで 連鎖沿いに渡されます

  6. 連鎖は動的に変化するので クライアントは次のような状況に対処できる必要があります

    • 連鎖には リンクが一つしかない
    • 一部のリクエストは連鎖の最後に到達しないかもしれない
    • 他のリクエストは処理されないまま 連鎖の最後に到達するかもしれない

長所と短所

  • リクエスト処理の順序を制御できる
  • 処理を起動するクラスを 実際に処理をするクラスから分離可能
  • 新規ハンドラーをアプリに導入しても 既存クライアント側コードは動作する
  • 一部のリクエストが未処理で終わる可能性

他のパターンとの関係

  • Chain of ResponsibilityCommandMediatorObserver リクエストの送り手と受け手を接続する様々な方法を示します

    • Chain of Responsibility 潜在的受け手の動的な連鎖に沿って どれか一つが処理するまで リクエストを順番に渡します
    • Command 送り手と受け手との間で単方向の接続を確立します
    • Mediator 送り手と受け手の間の直接の接続を削除し メディエーター・オブジェクトを介しての間接的通信を強制します
    • Observer では 受け手が動的にリクエストの受信申し込みをしたり 申し込み取り消しをしたりできます
  • Chain of Responsibility よく Composite と一緒に使われます この場合 リーフ 末端 のコンポーネントがリクエストを受ける時 リクエストは 全部の親コンポーネントからオブジェクト・ツリーのルート までを通るかもしれません

  • Chain of Responsibility のハンドラーは Commands で実装可能です この場合 リクエストに代表される同一のコンテキスト・オブジェクトに対して多くの異なる処理を実行できます

    しかしもう一つのやり方は リクエスト自身を・オブジェクトとすることです この場合 連鎖にリンクされた異なる一連のコンテキスト中で同じ処理を実行できます

  • Chain of ResponsibilityDecorator とは非常によく似た階層構造をしています 両パターンとも 一連のオブジェクトを通して実行を渡すために 再起的合成に依存します しかしながら いくつかの重要な違いがあります

    CoR ハンドラーは 互いに独立して任意の処理を実行可能です また 任意の時点でそれ以上リクエストを渡すのを止めることもできます 一方 様々な Decorator では オブジェクトの振る舞いの拡張をする時 基底インターフェースとの一貫性を保つ必要があります さらに デコレーターはリクエストの流れを断ち切ることは許されていません

コード例

Chain of Responsibility を C# で Chain of Responsibility を C++ で Chain of Responsibility を Go で Chain of Responsibility を Java で Chain of Responsibility を PHP で Chain of Responsibility を Python で Chain of Responsibility を Ruby で Chain of Responsibility を Rust で Chain of Responsibility を Swift で Chain of Responsibility を TypeScript で