本記事ではGoFのデザインパターンのプログラムの振る舞いに関するパターンの一つである「Chain of Responsibility」パターンを解説します。このパターンを一言で説明するならば、「複数のオブジェクトに自身のオブジェクト持つことで、鎖のように連結されたオブジェクトを渡り歩くことで、目的のオブジェクトを参照する方法」と言えるでしょう。「Chain of Responsibility」パターンをサンプルを踏まえて解説します。
「Chain of Responsibility」パターンとは
「Chain of Responsibility」は「誰かがリクエストに応える」仕組みです。たとえば、何か困ったことがあったとき、まずは身近な人に助けを求めます。その人が解決できない場合は、次の人にお願いし、最終的に問題を解決できる人にたどり着きます。このパターンでも同じように、処理をいくつかの人(オブジェクト)に順番に渡していくことができます。誰かが解決できたらそれで終了しますし、解決できなければ次の人に渡していきます。
このパターンを利用すると既存のコードに変更を加えることなく、新しく処理を追加することができます。上記のイメージだと、Dの作業ができるDさんのオブジェクトを追加するだけです。CさんがDの作業ができない場合、CさんはDさんに作業依頼を出します。また一つのクラスに複数の処理が記述されることがないためシンプルなクラスとなりとても読みやすいコードになります。
「Chain of Responsibility」パターンの書き方
今回の例ではカスタマーセンターにお問合せをする例で考えてみます。カスタマーセンターはいろいろなお問合せを処理しています。その仕組みをコード上で表現してみます。
✖︎ Chain of Responsibilityパターンを使用しない例 ✖︎
まずはChain of Responsibilityパターンを利用しない例を考えてみます。カスタマーセンターのクラスを作ります。このクラスにはhandleRequest()メソッドを持っており、お問合せの種類に応じて適切な内容を返却しています。
public class CustomerSupport {
public void handleRequest(SupportType supportType) {
switch (supportType) {
case PASSWORD_RESET:
System.out.println("パスワードのリセットを行います");
break;
case SYSTEM_OUTAGE:
System.out.println("システムが異常停止したため復旧します");
break;
case NETWORK_ERROR:
System.out.println("ネットワークエラーが発生したため再接続を実施します");
break;
default:
throw new IllegalArgumentException("お問い合わせいただいた内容はさぽーとしていません");
}
}
}
public enum SupportType {
PASSWORD_RESET,
SYSTEM_OUTAGE,
NETWORK_ERROR;
}
public class Main {
public static void main(String[] args) {
CustomerSupport customerSupport = new CustomerSupport();
customerSupport.handleRequest(SupportType.PASSWORD_RESET);
customerSupport.handleRequest(SupportType.SYSTEM_OUTAGE);
customerSupport.handleRequest(SupportType.NETWORK_ERROR);
}
}
今回の例では標準出力をするだけですが、ここにお問合せ内容に応じた処理が入ってきます。お問合せの種類が増えるたびにhandleRequest()の修正を行わなければなりません。switch文のパターンを増やす必要が出てきます。これでは保守性の高いクラスとはなりません。新規の処理が既存の処理に影響を与えてしまっています。
⭕️ Chain of Responsibilityパターンを使用する例 ⭕️
次にChain of Responsibilityパターンを利用してカスタマーセンターを実装します。まずはすべてのお問合せの設計図となるような抽象クラスを定義します。
public abstract class SupportHandler {
protected SupportHandler nextSupportHandler;
public void setNextHandler(SupportHandler nextHandler) {
this.nextSupportHandler = nextHandler;
}
public abstract void handleRequest(SupportType supportType);
protected void outputNotFoundMessage(SupportType supportType) {
System.out.println(supportType.name() + "このお問合わせは対応できません。");
}
}
このSupportHandlerクラスは自分自身のオブジェクトをフィールドで持っています。自分自身のオブジェクトを持つことで連鎖的に目的の処理ができるオブジェクトを見つけることができます。
また抽象メソッドとしてhandleRequest()があります。このメソッドの中にそれぞれのお問い合わせに合う処理を記述します。最後に定義されていないお問合せを受理した時に出力するメソッドを定義しています。
では、上記のSupportHandlerクラスを実装してそれぞれのお問合せを定義していきます。
// 総合窓口(オペレーションセンター)
public class OperatorSupportHandler extends SupportHandler {
@Override
public void handleRequest(SupportType supportType) {
System.out.println("こちらオペレーションセンターです。お問合せ内容を確認します");
nextSupportHandler.handleRequest(supportType);
}
}
// パスワードのリセットを担うサポート
public class PasswordResetSupportHandler extends SupportHandler {
@Override
public void handleRequest(SupportType supportType) {
if (supportType.equals(SupportType.PASSWORD_RESET)) {
System.out.println("パスワードのリセットを行います");
} else if (nextSupportHandler != null) {
System.out.println("こちらは個人情報管理課です。別の部署に問い合わせます");
nextSupportHandler.handleRequest(supportType);
} else {
super.outputNotFoundMessage(supportType);
}
}
}
// ネットワークの復旧を行うサポート
public class NetworkErrorSupporthandler extends SupportHandler {
@Override
public void handleRequest(SupportType supportType) {
if (supportType.equals(SupportType.NETWORK_ERROR)) {
System.out.println("ネットワークエラーが発生したため再接続を実施します");
} else if (nextSupportHandler != null) {
System.out.println("こちらはネットワーク課です。別の部署に問い合わせます");
nextSupportHandler.handleRequest(supportType);
} else {
super.outputNotFoundMessage(supportType);
}
}
}
// システムの復旧をするサポート
public class SystemOutageSupportHandler extends SupportHandler {
@Override
public void handleRequest(SupportType supportType) {
if (supportType.equals(SupportType.SYSTEM_OUTAGE)) {
System.out.println("システムが異常停止したため復旧します");
} else if (nextSupportHandler != null) {
System.out.println("こちらはシステム課です。別の部署に問い合わせます");
nextSupportHandler.handleRequest(supportType);
} else {
super.outputNotFoundMessage(supportType);
}
}
}
OperatorSupportHandlerクラスでは特に処理をすることがないため、自分自身が持っているSupportHandlerインスタンスのhandleRequest()を呼び出します。他のクラスではSupportTypeが自身が受け持つお問合せだった場合に処理を実行します。それ以外の場合で、自分自身がSupportHandlerインスタンス持っている場合は、そのhandleRequestを実行します。持っていない場合にはお問合せの処理は実行できないため、抽象クラスで定義したoutputNotFoundMessage()を呼び出します。
このクラスを実際に利用してお問合せを処理してみます。
public class Main {
public static void main(String[] args) {
SupportHandler operatorSupportHandler = new OperatorSupportHandler();
SupportHandler passwordResetSupportHandler = new PasswordResetSupportHandler();
SupportHandler networkErrorSupporthandler = new NetworkErrorSupporthandler();
SupportHandler systemOutageSupportHandler = new SystemOutageSupportHandler();
operatorSupportHandler.setNextHandler(passwordResetSupportHandler);
passwordResetSupportHandler.setNextHandler(networkErrorSupporthandler);
networkErrorSupporthandler.setNextHandler(systemOutageSupportHandler);
System.out.println("==========================================================");
operatorSupportHandler.handleRequest(SupportType.PASSWORD_RESET);
System.out.println("==========================================================");
operatorSupportHandler.handleRequest(SupportType.NETWORK_ERROR);
System.out.println("==========================================================");
operatorSupportHandler.handleRequest(SupportType.SYSTEM_OUTAGE);
System.out.println("==========================================================");
operatorSupportHandler.handleRequest(SupportType.ORDER_ERROR);
System.out.println("==========================================================");
}
}
まずはそれぞれのお問合せを処理できるSupportHandlerをSupportHandler型で生成します。すべてSupportHandlerを継承しているため、どのインスタンスも同じ型で生成できます。
次にsetNextHandlerで順番にSupportHandlerのインスタンスをセットしていきます。これで鎖状にオブジェクトを連結することができました。
実際にoperatorSupportHandlerのhandleRequest()メソッドにお問い合わせのタイプを指定して、処理させてみます。実行結果は下記になります。
>> 実行結果
==========================================================
こちらオペレーションセンターです。お問合せ内容を確認します
パスワードのリセットを行います
==========================================================
こちらオペレーションセンターです。お問合せ内容を確認します
こちらは個人情報管理課です。別の部署に問い合わせます
ネットワークエラーが発生したため再接続を実施します
==========================================================
こちらオペレーションセンターです。お問合せ内容を確認します
こちらは個人情報管理課です。別の部署に問い合わせます
こちらはネットワーク課です。別の部署に問い合わせます
システムが異常停止したため復旧します
==========================================================
こちらオペレーションセンターです。お問合せ内容を確認します
こちらは個人情報管理課です。別の部署に問い合わせます
こちらはネットワーク課です。別の部署に問い合わせます
ORDER_ERRORこのお問合わせは対応できません。
==========================================================
上記の実行結果を見て分かる通り、operatorSupportHandlerから呼び出したにも関わらず、それぞれのお問合せを処理できていることがわかります。また対応していないお問い合わせに関しても、正常に標準出力ができています。
もしまだ対応していないORDER_ERRORを実装するとなれば、SupportHandlerを継承してクラスを作り、handleRequest()メソッドを実装すればいいだけです。その新しく作ったクラスを生成して、鎖の最後尾に繋げます。このように実装することで既存の処理には影響を与えることなく、新規の処理を追加できます。
まとめ
利点⭕️
- 複数の処理を別のクラスに分けて扱えるため、コードの可読性が向上する
- 既存の処理に手を加える必要がないため、新規処理の拡張が容易に行える
- 鎖の順序を変更することで、処理の順序を容易に変更できる
欠点×
- チェーンが長くなると処理しないオブジェクトを挟んでしまうため、パフォーマンスの低下が懸念される
- どのチェーンで処理が実行されているかがわかりにくく、デバッグが難しい場合がある
Chain of Responsibilityパターンは、リクエストを誰が処理するかがはっきりしない状況で役立ちます。利点、欠点を踏まえて上手く利用していきましょう。
本記事で利用したサンプルケースのクラス図は下記になります。
本記事で利用したコードは下記のgit上で公開しております。
https://github.com/tamotech1028/chain-of-responsibility/tree/main
コメント