JavaでのChain of Responsibilityデザインパターン

Chain of responsibility design patternは、行動デザインパターンの一つです。

Chain of Responsibility Design Pattern

チェーン・オブ・レスポンシビリティ・パターンは、ソフトウェアデザインにおいてクライアントからのリクエストがオブジェクトのチェーンに渡され、それらを処理するために使用されます。その後、チェーン内のオブジェクトは自分自身でリクエストを処理する責任があるかどうか、および次のオブジェクトにリクエストを送る必要があるかどうかを決定します。

JDKでのChain of Responsibility Patternの例

実際の例で、JDKの責任連鎖パターンを見てから、このパターンの実際の例を実装してみましょう。私たちはtry-catchブロックコード内で複数のcatchブロックを持つことができることを知っています。ここで、各catchブロックは特定の例外を処理するためのプロセッサのようなものです。したがって、tryブロックで例外が発生すると、それは最初のcatchブロックに送信されて処理されます。もしcatchブロックがそれを処理できない場合、リクエストは連鎖内の次のオブジェクト、つまり次のcatchブロックに転送されます。最後のcatchブロックでも処理できない場合、例外は連鎖の外部に投げられて呼び出し元プログラムに渡されます。

責任連鎖デザインパターンの例

Chain of Responsibilityパターンの素晴らしい例の1つは、ATMの現金引き出し機です。ユーザーは引き出す金額を入力し、機械は50ドル、20ドル、10ドルなどの定義された通貨の紙幣で金額を引き出します。ユーザーが10の倍数ではない金額を入力すると、エラーが発生します。この解決策を実装するためにChain of Responsibilityパターンを使用します。チェーンは、以下の画像と同じ順序でリクエストを処理します。なお、この解決策を単一のプログラム内で簡単に実装することもできますが、複雑さが増し、解決策が密結合になります。したがって、50ドル、20ドル、10ドルの紙幣を出すためのディスペンスシステムのチェーンを作成します。

Chain of Responsibilityデザインパターン – ベースクラスとインターフェース

チェーンの実装で使用される金額を保存するCurrencyクラスを作成できます。Currency.java

package com.journaldev.design.chainofresponsibility;

public class Currency {

	private int amount;
	
	public Currency(int amt){
		this.amount=amt;
	}
	
	public int getAmount(){
		return this.amount;
	}
}

ベースインターフェースには、チェーン内の次のプロセッサを定義するメソッドと、リクエストを処理するメソッドが必要です。私たちのATMディスペンスインターフェースは以下のようになります。DispenseChain.java

package com.journaldev.design.chainofresponsibility;

public interface DispenseChain {

	void setNextChain(DispenseChain nextChain);
	
	void dispense(Currency cur);
}

Chain of Responsibilities パターン – 実装の連鎖

異なるプロセッサクラスを作成する必要があります。これらはDispenseChain インターフェースを実装し、dispenseメソッドの実装を提供します。システムを50ドル、20ドル、10ドルの3種類の通貨紙幣で動作させるように開発しているため、3つの具体的な実装を作成します。Dollar50Dispenser.java

package com.journaldev.design.chainofresponsibility;

public class Dollar50Dispenser implements DispenseChain {

	private DispenseChain chain;
	
	@Override
	public void setNextChain(DispenseChain nextChain) {
		this.chain=nextChain;
	}

	@Override
	public void dispense(Currency cur) {
		if(cur.getAmount() >= 50){
			int num = cur.getAmount()/50;
			int remainder = cur.getAmount() % 50;
			System.out.println("Dispensing "+num+" 50$ note");
			if(remainder !=0) this.chain.dispense(new Currency(remainder));
		}else{
			this.chain.dispense(cur);
		}
	}

}

Dollar20Dispenser.java

package com.journaldev.design.chainofresponsibility;

public class Dollar20Dispenser implements DispenseChain{

	private DispenseChain chain;
	
	@Override
	public void setNextChain(DispenseChain nextChain) {
		this.chain=nextChain;
	}

	@Override
	public void dispense(Currency cur) {
		if(cur.getAmount() >= 20){
			int num = cur.getAmount()/20;
			int remainder = cur.getAmount() % 20;
			System.out.println("Dispensing "+num+" 20$ note");
			if(remainder !=0) this.chain.dispense(new Currency(remainder));
		}else{
			this.chain.dispense(cur);
		}
	}

}

Dollar10Dispenser.java

package com.journaldev.design.chainofresponsibility;

public class Dollar10Dispenser implements DispenseChain {

	private DispenseChain chain;
	
	@Override
	public void setNextChain(DispenseChain nextChain) {
		this.chain=nextChain;
	}

	@Override
	public void dispense(Currency cur) {
		if(cur.getAmount() >= 10){
			int num = cur.getAmount()/10;
			int remainder = cur.getAmount() % 10;
			System.out.println("Dispensing "+num+" 10$ note");
			if(remainder !=0) this.chain.dispense(new Currency(remainder));
		}else{
			this.chain.dispense(cur);
		}
	}

}

ここで重要な点は、dispenseメソッドの実装です。すべての実装がリクエストを処理し、金額に基づいてそれを一部または完全に処理することを確認できます。連鎖の中の1つがそれを完全に処理できない場合、残りのリクエストを処理するために次のプロセッサにリクエストを送信します。プロセッサが何も処理できない場合、同じリクエストを次の連鎖に転送します。

Chain of Responsibilities デザインパターン – 連鎖の作成

これは非常に重要なステップであり、注意深くチェーンを作成する必要があります。そうしないと、プロセッサは一切のリクエストを受け取らない可能性があります。たとえば、最初のプロセッサチェーンをDollar10Dispenserとして保持し、次にDollar20Dispenserとすると、リクエストは二番目のプロセッサには転送されず、チェーンは無効になります。こちらは、ユーザーの要求された金額を処理するためのATMディスペンサーの実装です。ATMDispenseChain.java

package com.journaldev.design.chainofresponsibility;

import java.util.Scanner;

public class ATMDispenseChain {

	private DispenseChain c1;

	public ATMDispenseChain() {
		// チェーンを初期化する
		this.c1 = new Dollar50Dispenser();
		DispenseChain c2 = new Dollar20Dispenser();
		DispenseChain c3 = new Dollar10Dispenser();

		// 責任の連鎖を設定する
		c1.setNextChain(c2);
		c2.setNextChain(c3);
	}

	public static void main(String[] args) {
		ATMDispenseChain atmDispenser = new ATMDispenseChain();
		while (true) {
			int amount = 0;
			System.out.println("Enter amount to dispense");
			Scanner input = new Scanner(System.in);
			amount = input.nextInt();
			if (amount % 10 != 0) {
				System.out.println("Amount should be in multiple of 10s.");
				return;
			}
			// リクエストを処理する
			atmDispenser.c1.dispense(new Currency(amount));
		}

	}

}

上記のアプリケーションを実行すると、以下のような出力が得られます。

Enter amount to dispense
530
Dispensing 10 50$ note
Dispensing 1 20$ note
Dispensing 1 10$ note
Enter amount to dispense
100
Dispensing 2 50$ note
Enter amount to dispense
120
Dispensing 2 50$ note
Dispensing 1 20$ note
Enter amount to dispense
15
Amount should be in multiple of 10s.

責任連鎖デザインパターンクラス図

責任連鎖デザインパターンの実装のATMディスペンスの例は、以下の画像のようになります。

Chain of Responsibility Design Patternの重要なポイント

  • クライアントは、チェーンのどの部分がリクエストを処理するかわからず、リクエストをチェーン内の最初のオブジェクトに送信します。たとえば、私たちのプログラムでは、ATMDispenseChainは、入力された金額を支払うためのリクエストを誰が処理しているかを認識していません。
  • チェーン内の各オブジェクトは、リクエストを処理するための独自の実装を持ち、完全または部分的に処理するか、またはチェーン内の次のオブジェクトに送信するかを決定します。
  • チェーン内のすべてのオブジェクトは、リクエストを次のオブジェクトに転送するための参照を持つ必要があります。これは、Javaの構成によって実現されています。
  • チェーンを注意深く作成することは非常に重要です。そうしないと、特定のプロセッサにリクエストが転送されない場合や、チェーン内にリクエストを処理できるオブジェクトがない場合があります。私の実装では、ユーザーが入力した金額をチェックして、すべてのプロセッサによって完全に処理されることを確認しましたが、それをチェックしないで、リクエストが最後のオブジェクトに到達し、チェーン内にそれ以上のオブジェクトがない場合に例外をスローするかもしれません。これは設計上の決定です。
  • 責任連鎖の設計パターンは、疎結合性を実現するのに適していますが、実装クラスが多くなり、コードの大部分がすべての実装で共通している場合、メンテナンスの問題が発生するというトレードオフがあります。

Chain of Responsibility PatternのJDKにおける例

  • java.util.logging.Logger#log()
  • javax.servlet.Filter#doFilter()

これでChain of Responsibilityデザインパターンに関する説明は終わりです。お役に立てたら幸いです。

Source:
https://www.digitalocean.com/community/tutorials/chain-of-responsibility-design-pattern-in-java