Chain of Responsibilityパターン


 ★☆☆

Chain of Responsibilityパターン

処理できるクラスが現れるまで順送りする

chain は「鎖」、responsibility は「責任」を意味します。したがって、 Chain of Responsibility は、「責任の鎖」と訳すことができます。Chain of Responsibility パターンとは、「責任」を負ったものが、「鎖」状につながれた状態をイメージさせるパターンです。

例えば、お客さんからクレームがあった場合、その処理を自分ができるのならしますが、よく分からないときはより詳しい人とかその客の担当にお願いします。お願いされた人は、対応できるものならしますが、対応できないものについては、課長にお願いすることになります。当然課長が対応できないものであればより上位の責任者に委ねられます。Chain of Responsibility パターンは、「責任者」を「鎖状」につないでおき、「いずれかの段階」で、「誰か」が処理をすることを表現するようなパターンです。



このように、メッセージを他のクラスのオブジェクトに送って処理してもらうことを、「委譲」と呼びます。すなわち、Chain of Responsibilityパターンを実現するには、メッセージの送り先のオブジェクトのポインタ(「アドレス」や「参照」とも呼ぶ)を知っている必要があります。具体的には、クラスのメンバの中に、送り先のオブジェクトのポインタを格納するフィールドを用意しておくわけです。

まず、自身で処理できなければ次のサブクラスに回していきます。そして最初に条件に合致したクラスが処理を行い、あとのクラスには回されません。処理の優先順位を決める連鎖リストは呼び出し側で指定する必要があります。

別バリエーションとして、最後にデフォルトを持ってきて必ず何らかの処理をさせるパターンや合致する複数の条件すべての処理を可能にするパターンがあります。

スーパークラスおよび実際の処理のサブクラスは変更する必要がなく、別の処理が必要になればサブクラスを追加すればよい点がこのパターンの特徴です。


ただし、ある条件に対する処理が1対1で決まっている場合、たらい回しにしている分、呼び出しのオーバーヘッドがあります。毎回チェーンをたどっていくため、処理速度が遅くなることが考えられます。速度重視のプログラムが必要なのか、柔軟性が求められるのか、状況によって判断する必要がありそうです。


例題

日付の形式
yyyy/mm/dd
yyyymmdd
yyyy年mm月dd日

日付をあらわす文字列を日付型に変換するクラスを作成しなさい。日付の形式は次のとおりです。


実行結果
2019-05-01  
2019-05-02
2019-05-03









Chain of Responsibilityパターンを使用しない例

StringToDate.java
public class StringToDate { public Date getDate(String str) { if (str.length() == 10) { // yyyy/MM/dd int y = Integer.parseInt(str.substring(0, 4)); int m = Integer.parseInt(str.substring(5, 7)); int d = Integer.parseInt(str.substring(8)); Calendar cal = Calendar.getInstance(); cal.set(y, m - 1, d, 0, 0, 0); return cal.getTime(); } else if (str.length() == 8) { // yyyyMMdd int y = Integer.parseInt(str.substring(0, 4)); int m = Integer.parseInt(str.substring(4, 6)); int d = Integer.parseInt(str.substring(6)); Calendar cal = Calendar.getInstance(); cal.set(y, m - 1, d, 0, 0, 0); return cal.getTime(); } else if (str.length() == 11) { // yyyy年mm月dd日 int y = Integer.parseInt(str.substring(0, 4)); int m = Integer.parseInt(str.substring(5, 7)); int d = Integer.parseInt(str.substring(8, 10)); Calendar cal = Calendar.getInstance(); cal.set(y, m - 1, d, 0, 0, 0); return cal.getTime(); } return null; } }
Main.java
public class Main { public static void main(String[] args) { SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); StringToDate sdt = new StringToDate(); Date dt1 = sdt.getDate("2019/05/01"); Date dt2 = sdt.getDate("20190502"); Date dt3 = sdt.getDate("2019年05月03日"); System.out.println(df.format(dt1)); System.out.println(df.format(dt2)); System.out.println(df.format(dt3)); } }

Chain of Responsibilityパターンを使用した例

StringToDate.java
public abstract class StringToDate { private StringToDate next = null; public StringToDate setNext(StringToDate next) { this.next = next; return next; } public Date toDate(String str) { Date dt = resolve(str); if (dt != null) { return dt; } else if (next != null) { return next.toDate(str); } else { return null; } } protected abstract Date resolve(String str); }
SlashDelimitDate.java
public class SlashDelimitDate extends StringToDate { protected Date resolve(String str) { if (str.length() == 10) { // yyyy/MM/dd int y = Integer.parseInt(str.substring(0, 4)); int m = Integer.parseInt(str.substring(5, 7)); int d = Integer.parseInt(str.substring(8)); Calendar cal = Calendar.getInstance(); cal.set(y, m - 1, d, 0, 0, 0); return cal.getTime(); } else { return null; } } }
NonDelimitDate.java
public class NonDelimitDate extends StringToDate { protected Date resolve(String str) { if (str.length() == 8) { // yyyyMMdd int y = Integer.parseInt(str.substring(0, 4)); int m = Integer.parseInt(str.substring(4, 6)); int d = Integer.parseInt(str.substring(6)); Calendar cal = Calendar.getInstance(); cal.set(y, m - 1, d, 0, 0, 0); return cal.getTime(); } else { return null; } } }
KanjiDelimitDate.java
public class KanjiDelimitDate extends StringToDate { protected Date resolve(String str) { if (str.length() == 11) { // yyyy年mm月dd日 int y = Integer.parseInt(str.substring(0, 4)); int m = Integer.parseInt(str.substring(5, 7)); int d = Integer.parseInt(str.substring(8, 10)); Calendar cal = Calendar.getInstance(); cal.set(y, m - 1, d, 0, 0, 0); return cal.getTime(); } else { return null; } } }
Main.java
public class Main { public static void main(String[] args) { SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); SlashDelimitDate sdt = new SlashDelimitDate(); NonDelimitDate ndt = new NonDelimitDate(); KanjiDelimitDate kdt = new KanjiDelimitDate(); sdt.setNext(ndt).setNext(kdt); Date dt1 = sdt.toDate("2019/05/01"); Date dt2 = sdt.toDate("20190502"); Date dt3 = sdt.toDate("2019年05月03日"); System.out.println(df.format(dt1)); System.out.println(df.format(dt2)); System.out.println(df.format(dt3)); } }

日付型への変換メソッド(getDate)の中で、文字列の長さを調べることによって形式を知り、年月日を求め日付型を作成しています。

しかし、これでは新しい形式が増えたときにクラスを作り変えなければならなくなります。

Chain of Responsibility パターンでは、形式ごとに、StringToDate を継承したサブクラスを作成しています。 そして、それぞれのインスタンスを生成し、あるインスタンスを別のサブクラスの next に入れることによって、チェーンを作っています。

そして、チェーンの先頭となるインスタンスの toDate メソッドを呼び出して日付型に変換しています。

toDate メソッドは、次のように実行されます。

  1. サブクラスの想定した形式なら日付型に変換する。( resolve() )
  2. 日付型に変換できたのなら、そのインスタンスを戻す。
  3. 日付型に変換できないのなら 次のサブクラス( next )の toDate メソッドを呼び出す。
  4. 次のサブクラスがないのなら null を戻す。

先ほども言いましたが、毎回チェーンをたどっていくため、処理速度は遅くなります。しかし、チェーンの順番を変えることにより、いろいろな状況に対応できる柔軟性が得られます。