14. 責任の連鎖パターン (Chain of Responsibility)
頼みごとを処理係の鎖に流して、引き受けられる処理係に引き受けさせる責任の連鎖パターンについて学びましょう。
ひとことで言うと
頼みごとを処理できるオブジェクトたちを鎖のようにつないでおき、頼みごとが来たら先頭から「自分が引き受けられるなら引き受け、無理なら次に渡す」ようにするパターンです。
問題: だれが、どの順番で引き受けるかが1つの関数に全部入っているコード
ネットショップの返金処理を作るとしましょう。金額によって、処理できる人が違います。
| 金額 | 処理する人 |
|---|---|
| 1000円以下 | 自動承認(人を通さずすぐに) |
| 10000円以下 | オペレーター |
| 100000円以下 | チームリーダー |
| それより多いと | 本社の審査へご案内 |
そしてVIP窓口は、自動承認なしでオペレーターから始まります。まず思いつくのは、金額を見てもしで分ける方法です。
(参考:だれが処理するか、どの順番で聞くかが、1つの関数に全部入っています)
〈返金処理〉を作ろう(【数字】の『金額』):
もし(『金額』 <= 1000)なら:
枠「[自動承認] {『金額』}円を返金しました。」を出力しよう
もしくは(『金額』 <= 10000)なら:
枠「[オペレーター] {『金額』}円の返金を承認しました。」を出力しよう
もしくは(『金額』 <= 100000)なら:
枠「[チームリーダー] {『金額』}円の返金を承認しました。」を出力しよう
それ以外なら:
枠「[ご案内] {『金額』}円は本社の審査が必要です。」を出力しよう
(参考:VIP窓口は自動承認なしで、オペレーターから始まります。だから、ほとんど同じ関数をもう1つ作りました)
〈VIP返金処理〉を作ろう(【数字】の『金額』):
もし(『金額』 <= 10000)なら:
枠「[オペレーター] {『金額』}円の返金を承認しました。」を出力しよう
もしくは(『金額』 <= 100000)なら:
枠「[チームリーダー] {『金額』}円の返金を承認しました。」を出力しよう
それ以外なら:
枠「[ご案内] {『金額』}円は本社の審査が必要です。」を出力しよう
〈返金処理〉(500)を実行しよう
〈返金処理〉(5000)を実行しよう
〈返金処理〉(300000)を実行しよう
〈VIP返金処理〉(500)を実行しよう
実行結果
[自動承認] 500円を返金しました。
[オペレーター] 5000円の返金を承認しました。
[ご案内] 300000円は本社の審査が必要です。
[オペレーター] 500円の返金を承認しました。
ちゃんと動きます。でも、ルールが増えると痛くなります。
- 「だれが」と「どの順番で」がひとかたまりです。 自動承認、オペレーター、チームリーダーがそれぞれ何をするのかと、だれに先に聞くのかが、
〈返金処理〉1つの関数に混ざっています。 - 窓口ごとに順番が違うと、関数がコピーされます。 VIP窓口は順番が少し違うだけなのに、ほとんど同じ
〈VIP返金処理〉をもう1つ作りました。 - 新しい処理係をはさむには、すべてのコピーを直さなければなりません。「50000円以下はサブリーダー」ができたら、
〈返金処理〉と〈VIP返金処理〉の両方にもしくはを同じ場所に入れる必要があります。片方だけ直すと、窓口ごとにルールが食い違います。 - 処理係ごとの仕事が大きくなると、関数がふくらみます。 オペレーターが「返金の理由も確かめる」、チームリーダーが「記録を残す」ようになると、その仕事がすべて道の1つ1つの中に入ります。
解決策: 処理係たちを鎖でつなぐ
責任の連鎖パターンは、処理係の1人1人をオブジェクトにして、そのオブジェクトたちを鎖のようにつなぎます。
- すべての処理係の共通の骨組みを親に置きます。「自分が引き受けられるなら引き受け、無理なら次に渡す」です。次がいなければ、本社へご案内します。
- 処理係ごとに違うもの、引き受けられる範囲と引き受け方だけを子が埋めます。
- それぞれの処理係は、次の処理係1人だけを知っています。鎖全体がどうなっているかは知りません。
- 鎖は使う側がつなぎます。窓口ごとに違うつなぎ方ができます。
たとえ: 会社の決裁ルート
休暇の申請書を出すと、まずチームリーダーが見ます。1日だけならチームリーダーがすぐ承認し、1週間なら「私の権限では無理です」と部長に上げます。部長も無理なら社長に上げます。申請書を出した人は、だれが最後に承認するのか知らなくてかまいません。先頭の人に出すだけで、引き受けられる人が出てくるまで自然に上がっていきます。この決裁ルートこそが「責任の連鎖」です。
だれが何をするの?
| 役割 | この例では | やること |
|---|---|---|
| 処理係の骨組み | 【処理係】(下設計) |
引き受けるか渡すかを決め、次の処理係を覚えています |
| 具体的な処理係 | 【自動承認】、【オペレーター】、【チームリーダー】 |
引き受けられる範囲と引き受け方を知っています |
| 鎖をつなぐ側 | 『一般窓口』、『VIP窓口』 |
処理係たちを好きな順番でつなぎます |
| 頼む側 | 〈処理する〉(金額)を呼ぶ場所 |
鎖の先頭に頼むだけです |
カナデで作る
同じ返金処理を、責任の連鎖パターンで作り直してみましょう。
(参考:ステップ1 — 処理する人の共通の骨組み。「自分が引き受けられるなら引き受け、無理なら次に渡す」)
【処理係】を下設計しよう:
『次』を空っぽに隠そう
(参考:作る時に次の処理係を受け取ります。渡さなければ、鎖の終わりです)
最初に作られる時(『次』 = 空っぽ)次のようにしよう:
『私』の『次』を『次』にしよう
〈処理する〉を作ろう(【数字】の『金額』):
もし(『私』の〈引き受けられるか〉(『金額』)が真と同じだ)なら:
『私』の〈引き受ける〉(『金額』)を実行しよう
もしくは(『私』の『次』が空っぽと同じだ)なら:
枠「[ご案内] {『金額』}円は本社の審査が必要です。」を出力しよう
それ以外なら:
『私』の『次』の〈処理する〉(『金額』)を実行しよう
(参考:ステップ2 — 処理係ごとに違うもの:引き受けられる範囲と、引き受け方)
〈引き受けられるか〉がなければならない(【数字】の『金額』)
〈引き受ける〉がなければならない(【数字】の『金額』)
【処理係】をもとにして【自動承認】を設計しよう:
【論理】を返す〈引き受けられるか〉を作ろう(【数字】の『金額』):
(『金額』 <= 1000)を返そう
〈引き受ける〉を作ろう(【数字】の『金額』):
枠「[自動承認] {『金額』}円を返金しました。」を出力しよう
【処理係】をもとにして【オペレーター】を設計しよう:
【論理】を返す〈引き受けられるか〉を作ろう(【数字】の『金額』):
(『金額』 <= 10000)を返そう
〈引き受ける〉を作ろう(【数字】の『金額』):
枠「[オペレーター] {『金額』}円の返金を承認しました。」を出力しよう
【処理係】をもとにして【チームリーダー】を設計しよう:
【論理】を返す〈引き受けられるか〉を作ろう(【数字】の『金額』):
(『金額』 <= 100000)を返そう
〈引き受ける〉を作ろう(【数字】の『金額』):
枠「[チームリーダー] {『金額』}円の返金を承認しました。」を出力しよう
(参考:ステップ3 — 鎖は使う側がつなぎます。内側(終わり)から作って前に渡します。窓口ごとに違うつなぎ方ができます)
『一般窓口』を新しい【自動承認】(新しい【オペレーター】(新しい【チームリーダー】()))にしよう
『VIP窓口』を新しい【オペレーター】(新しい【チームリーダー】())にしよう
『一般窓口』の〈処理する〉(500)を実行しよう
『一般窓口』の〈処理する〉(5000)を実行しよう
『一般窓口』の〈処理する〉(300000)を実行しよう
『VIP窓口』の〈処理する〉(500)を実行しよう
実行結果
[自動承認] 500円を返金しました。
[オペレーター] 5000円の返金を承認しました。
[ご案内] 300000円は本社の審査が必要です。
[オペレーター] 500円の返金を承認しました。
結果はリファクタリング前と1行も違いません。でも、VIP窓口のための関数はもうありません。同じ処理係たちを違う順番でつないだだけです。
ステップごとにもう一度
- ステップ1 — 共通の骨組み。
【処理係】の〈処理する〉は、「引き受けられる? → 引き受ける / 次がいない? → 本社へご案内 / それ以外 → 次へ」という順番を1回だけ書きます。テンプレートメソッドと同じ形です。 - ステップ2 — 処理係ごとに違うものだけ。
【オペレーター】は「10000円以下なら引き受けられる」と「承認しました」だけを知っています。自分の前にだれがいて、後ろにだれがいるのかは知りません。 - 作る時に次を渡す。 処理係は、作られる時に次の処理係を受け取ります。だから
新しい【自動承認】(新しい【オペレーター】(新しい【チームリーダー】()))のように、鎖の終わり(チームリーダー)から作って前に渡します。何も渡さなければ『次』が空っぽなので、その処理係が鎖の終わりになります。子たちはコンストラクタを作らなくても、親のコンストラクタが使われます。 - ステップ3 — 鎖は使う側が。 一般窓口は自動承認 → オペレーター → チームリーダー、VIP窓口はオペレーター → チームリーダーです。処理係のクラスは1セットなのに、つなぐ順番だけが違います。
鎖の最後まで、だれも引き受けられなかったら?
この例では、最後の処理係の『次』が空っぽなので、骨組みが「本社の審査が必要です」とご案内します。鎖の終わりで何をするか(ご案内、エラー、既定の処理)は必ず決めておきましょう。決めておかないと、頼みごとが黙って消えてしまうことがあります。
新しい処理係をはさんでみると
「50000円以下はサブリーダー」ができたとしましょう。
- 前のやり方:
〈返金処理〉と〈VIP返金処理〉の両方に、同じ道を同じ場所に入れなければなりません。 - 責任の連鎖パターン:
【処理係】をもとにして【サブリーダー】を1つだけ作り、鎖をつなぐ行で、オペレーターとチームリーダーの間にはさめば済みます。ほかの処理係のクラスはそのままです。
やってみよう
上のコードに【サブリーダー】(50000円以下)を足し、一般窓口とVIP窓口の両方で、オペレーターの次、チームリーダーの前にはさんでみてください。それから『一般窓口』の〈処理する〉(30000)がサブリーダーのところへ行くか確かめましょう。一般窓口を作る行は新しい【自動承認】(新しい【オペレーター】(新しい【サブリーダー】(新しい【チームリーダー】())))になるはずです。
どんな時に使うといいの?
- 頼みごとを処理できる側がいくつもあり、だれが引き受けるかは頼みごとを見ないと分からない時
- 処理する順番が状況ごとに違う時(窓口ごと、ユーザーごと)
- 処理係をはさんだり外したりすることがよくある時
- 頼む側が、だれが最後に処理するのかを知らなくて済むようにしたい時
いいところと気になるところ
| いいところ | 気になるところ |
|---|---|
| 処理係ごとに自分の仕事だけを知っていて単純です | 頼みごとがだれのところで処理されたか、コードを見ただけではすぐ分かりません |
| 同じ処理係たちを、状況ごとに違う形でつなげます | 鎖の終わりで何をするか決めておかないと、頼みごとが消えることがあります |
| 処理係をはさんだり外したりしやすいです | 鎖が長いと、先頭から1人ずつ聞くので遅くなることがあります |
| 頼む側は先頭だけを知っていれば済みます | つなぐ順番を間違える(チームリーダーを先頭に)と、見当違いの処理係が引き受けます |
早すぎるパターンは毒です。
処理係が2〜3人で順番がいつも同じなら、もしのはしご1つのほうが読みやすいです。順番が状況ごとに変わり始めたり、処理係をよくはさんだり外したりし始めたりしてから鎖に変えても、遅くはありません。
ほかのパターンとの関係
- デコレーターパターンと形が似ています。 どちらもオブジェクトが次のオブジェクトを抱えて仕事を渡します。デコレーターは全員が自分の分を足しながら最後まで渡し、責任の連鎖はだれかが引き受けたらそこで止まります。
- テンプレートメソッドパターンで骨組みを作りました。「引き受けるか、渡すか」という順番は親が決め、範囲と引き受け方だけを子が埋めました。
- コマンドパターンと一緒に使えます。 頼みごとを金額のような値の代わりにコマンドオブジェクトにして鎖に流せば、処理係がそのコマンドを実行するか、次に渡すかを選べます。
- コンポジットパターンの木でもよく使います。 内側の項目が処理できなかった頼みごとを外側のまとまりに上げていくのも、責任の連鎖です。