今更ながら、かの著名なGoFのデザインパターンを復習している。
10年前に居た会社にGoF本の日本語版があり、たまに何とかパターンという言葉を使う人が居たので、その本を一通り流し読みしたことがあるが、その時はそれの重要性に気付かず、23種類のパターンのそれぞれについてなんとなくわかった気になったら、1行に要約して控えておいただけだった。
結局、その部署で耳にしたパターン名は「オブザーバーパターン」だけだったので、それ以外のパターンは全て忘れてしまった。
Observerパターンは要するに状態変化時のコールバック関数を登録する設計のことであり、特にGUIのイベントハンドラーを定義する仕組みでは頻出なので、忘れようが無いし、「コールバックモデル」とか「イベントハンドラー」とか、Javaなら「リスナー」とか言えば通じるので、もはや「オブザーバーパターン」という言葉を耳にすることも無い。もしソフトウェア開発の場で耳にしたら、何をカッコつけとんねん、と言いたくなるだろうし、「何かあったら電話して」と言われた時に「オブザーバーパターンですね」と答えるような用途は考えられないことは無いが、よほどのマニアでない限り、その洒落が通じることは無いであろう。
その後、デザインパターンへの興味を失ってしまったのだが、オブジェクト指向とは何かということに興味を持ち、時々図書館でそれ系の本を借りて読むようになると、GoFのデザインパターンがよく引用されているので、また目にするようになった。
というか、GoFのデザインパターン以外に、これぞオブジェクト指向設計と思うような例はほとんど目にしない。オブジェクト指向設計になっていても、実際に動作するアプリケーションになっていると、そのアプリが提供する機能に目を奪われてしまうからだろうか。
そこで、もう一度GoFのデザインパターンを復習した。
半年かかった。
23種類ってのは、1つ1つ取り組むと、思ったより多かった。
当初は1つ1つのパターンについてオリジナルなサンプルコードを作ることを目標としていたが、結構大変であることに気付いたので、方針変更して、興味を持ったテーマについてだけ書くことにした。
今回は、StrategyパターンとVisitorパターンの使い分けについて考える。全然違うもののようだが、特定の処理を交換可能にするという用途に限ると、似たような効果が得られるようである。
Strategyパターンとは、特定の処理を外部のクラスに置くことにより、既存のクラスを変更せずにアルゴリズムを交換可能にするパターンである。次のUML図は、StableClass以下を変更せずに、合計の計算方法を変更することを可能にする、Strategyパターンの適用例である。
これをJavaで実装したサンプルコードを、下の方に添付している。
StableClassを、計算対象のデータを保持する、変更不可なクラスとする。
StableClassが抽象的なSumStrategyへの参照を持っており、合計の計算はそれを介して行われる。具体的なSumStrategyは実行時に決まるので、StableClassを変更することなく、合計の計算方法を追加することができる。合計の計算方法として、通常の足し算でなく、掛け算を選択することができるように変更するには、新たなSumStrategyであるMultiplicationクラスを追加するだけで良い。
Visitorパターンとは、任意の処理を持つオブジェクトを受け付ける口を設けることにより、既存のクラスを変更せずに、様々な処理を後から追加できるようにするパターンである。
次のUML図は、上と同じことをVisitorパターンでやってみた例である。
Visitorパターンでは、visitorが操作対象となるクラス(以下visitee)をvisitすることをそのクラスがacceptするという表現が用いられる。
操作対象のクラスは、visitorのvisitメソッドを呼ぶだけの、acceptメソッドを用意する。抽象的なVisitorが提供するI/Fが呼ばれると、visitorはvisiteeの具体的なクラスを知らないまま、自身を引数にしてaccept()を呼び、visiteeのaccept()は、visitorの具体的なクラスを知らずに自身を引数にしてvisit()を呼ぶ。(accept()の呼び出し時にStableClassの具体的なクラスの特定が行われ、visit()の呼び出し時にVisitorの具体的なクラスの特定が行われる)
このdouble dispatchと呼ばれる2段階の呼び出しを経由することにより、StableClassとVisitorの結合は呼び出し時まで抽象化される。つまり、visitorとvisiteeのクラスの組み合わせによって決まる、実際に実行される処理は、実行時まで未確定にすることができるので、StableClassを変更せずに、新たなVisitorを追加できるのである。
従って、これによっても、新たな合計の計算方法を追加するには、図中のMultipleのようなクラスを追加するだけで良い。
一般的にはVisitorはVisiteeの全ての具象クラスをvisitできる必要は無いとされるため、上図のように、抽象Visiteeクラスにaccept()が無く、VisiteeからVisitorへの関連は、accept()を持つ具象Visiteeクラスから抽象Visitorへの関連とされる(accept()を持たない具象クラスからの関連は無い)ようである(GoF本は手元に無いので未確認)。しかし、今回のように、デフォルトの処理がVisitorとして存在する場合は、全てのVisiteeはvisitableであるとしても問題無さそうなので、StableClassにaccept()を設けてみる。そうすると、StableClassからVisitorへの関連は抽象クラス同士になり、Strategyパターンに近い構図になる。
これをJavaで書いたコードを、下の方に貼っている。
(次のエントリーに続く)
●Strategyパターンのサンプルコード
Stable.java
//Some threatening like "NO WARRANTY IF ANY EVEN SLIGHT CHANGES" or //"Touching inside this code will bring a legal judgment on courts" public abstract class StableClass { protected SumStrategy summation; public StableClass() { summation = new OrdinarySummation(); //default strategy } public void setSumStrategy(SumStrategy s) { summation = s; } //*snip* (a lot of very untouchable codes) }
StableClassA.java
//Some threatening like "NO WARRANTY IF ANY EVEN SLIGHT CHANGES" or //"Touching inside this code will bring a legal judgment on courts" public class StableClassA extends StableClass { private int a,b; public StableClassA(int a, int b) { this.a = a; this.b = b; } public StableClassA(int a, int b, SumStrategy s) { this.a = a; this.b = b; setSumStrategy(s); } //*snip* (a lot of very untouchable codes) //One of strategy user public int getSum() { Input ia = new Input(a); Input ib = new Input(b); return summation.algorithm(ia, ib).value; } }
StableClassB.java
//Some threatening like "NO WARRANTY IF ANY EVEN SLIGHT CHANGES" or //"Touching inside this code will bring a legal judgment on courts" public class StableClassB extends StableClass { private int a,b,c; public StableClassB(int a, int b, int c) { this.a = a; this.b = b; this.c = c; } public StableClassB(int a, int b, int c, SumStrategy s) { this.a = a; this.b = b; this.c = c; summation = s; } //*snip* (a lot of very untouchable codes) //One of strategy user public int getSum() { Input ia = new Input(a); Input ib = new Input(b); Input iab = new Input(summation.algorithm(ia, ib).value); Input ic = new Input(c); return summation.algorithm(iab, ic).value; } }
SumStrategy.java
public interface SumStrategy { public Result algorithm(Input input1, Input input2); }
OrdinarySummation.java
public class OrdinarySummation implements SumStrategy { public Result algorithm(Input input1, Input input2) { Result r = new Result(); r.value = input1.value + input2.value; return r; } }
Multiplication.java
public class Multiplication implements SumStrategy { public Result algorithm(Input input1, Input input2) { Result r = new Result(); r.value = input1.value * input2.value; return r; } }
Input.java
public class Input { int value; public Input() {value = 0;} public Input(int i) {value = i;} }
Result.java
public class Result { public int value; }
Main.java(テストプログラム)
public class Main { enum PATTERN {A, B}; private static StableClassB StableClassBFactory(int a, int b, int c) { return new StableClassB(a, b, c, new Multiplication()); } private static SumStrategy SumStrategyFactory(PATTERN p) { if (p == PATTERN.B) return new Multiplication(); return new OrdinarySummation(); } public static void main(String[] args) { StableClassA object1 = new StableClassA(7,8); System.out.println("Sum of object1 = " + object1.getSum()); SumStrategy newsum = SumStrategyFactory(PATTERN.B); object1.setSumStrategy(newsum); System.out.println("Sum of object1 = " + object1.getSum()); StableClassB object2 = StableClassBFactory(4, 5, 6); System.out.println("Sum of object2 = " + object2.getSum()); } }
object1については、1つ目のgetSum()呼び出しではデフォルトの計算方法、2つ目以降のgetSum()呼び出しではパターンBの計算方法を使用している。
object2については、デフォルトの計算方法を変更できる感じを表しているつもりである。
出力結果
Sum of object1 = 15
Sum of object1 = 56
Sum of object2 = 120
●Visitorパターンのサンプルコード
StableClass.java
StableClassA.java//Some threatening like "NO WARRANTY IF ANY EVEN SLIGHT CHANGES" or //"Touching inside this code will bring a legal judgment on courts" public abstract class StableClass { //all subclasses override this with //public Result accept(Visitor v) {return v.visit(this);} public abstract Result accept(SumVisitor v); //*snip* (a lot of very untouchable codes) }
StableClassB.java//Some threatening like "NO WARRANTY IF ANY EVEN SLIGHT CHANGES" or //"Touching inside this code will bring a legal judgment on courts" public class StableClassA extends StableClass { private int a,b; public StableClassA(int a, int b) { this.a = a; this.b = b; } public int getA() {return a;} public int getB() {return b;} public Result accept(SumVisitor v) { return v.visit(this); } //*snip* (a lot of very untouchable codes) }
SumVisitor.java//Some threatening like "NO WARRANTY IF ANY EVEN SLIGHT CHANGES" or //"Touching inside this code will bring a legal judgment on courts" public class StableClassB extends StableClass { private int a,b,c; public StableClassB(int a, int b, int c) { this.a = a; this.b = b; this.c = c; } public int getA() {return a;} public int getB() {return b;} public int getC() {return c;} public Result accept(SumVisitor v) { return v.visit(this); } //*snip* (a lot of very untouchable codes) }
OrdinarySum.javapublic interface SumVisitor { public int getSum(StableClass obj); //must be implemented with {return obj.accept(this).value}; public Result visit(StableClassA obj); public Result visit(StableClassB obj); }
Multiple.javapublic class OrdinarySum implements SumVisitor { public int getSum(StableClass obj) { return obj.accept(this).value; } public Result visit(StableClassA obj) { Result r = new Result(); r.value = obj.getA() + obj.getB(); return r; } public Result visit(StableClassB obj) { Result r = new Result(); r.value = obj.getA() + obj.getB() + obj.getC(); return null; } }
Result.javapublic class Multiple implements SumVisitor { public int getSum(StableClass obj) { return obj.accept(this).value; } public Result visit(StableClassA obj) { Result r = new Result(); r.value = obj.getA() * obj.getB(); return r; } public Result visit(StableClassB obj) { Result r = new Result(); r.value = obj.getA() * obj.getB() * obj.getC(); return r; } }
Main.javapublic class Result { public int value; }
Visitor patternの特徴を際立たせるために、敢えてStrategy patternのテストコードとは違う形にしている。(StableClassも抽象化している)(getSum()を呼び出す処理ではStableClassについてもVisitorについてもインスタンスの具象クラスを知らなくて良いことを表しているつもり)public class Main { enum PATTERN {A, B}; private static StableClass StableClassFactory(PATTERN p) { if (p == PATTERN.B) return new StableClassB(4, 5, 6); return new StableClassA(7, 8); } private static SumVisitor SumVisitorFactory(PATTERN p) { if (p == PATTERN.B) return new Multiple(); return new OrdinarySum(); } public static void main(String[] args) { StableClass object1 = StableClassFactory(PATTERN.A); StableClass object2 = StableClassFactory(PATTERN.B); SumVisitor sum1 = SumVisitorFactory(PATTERN.A); SumVisitor sum2 = SumVisitorFactory(PATTERN.B); System.out.println("Sum of object1 by sum1 = " + sum1.getSum(object1)); System.out.println("Sum of object1 by sum2 = " + sum2.getSum(object1)); System.out.println("Sum of object1 by sum2 = " + sum2.getSum(object2)); } }
出力結果
Sum of object1 by sum1 = 15
Sum of object1 by sum2 = 56
Sum of object1 by sum2 = 120
なお、いわゆるGoF本は、図書館で借りられなかったため、今回まだ読んでいない。解説本を読んで理解したものをベースにしている。その内借りられたら、違うことを書いてないかどうか確かめることにする。
Jahlin
Finally! This is just what I was looknig for.
匿名
かなり古い記事ですが読んでいて気になったので。
visitorパターンの SumVisitorインターフェースが抽象化されていないです。
StableClassCが登場した場合に、インターフェースとすべての具現クラス(Concrete)に変更が発生します。
各visitor内で、instanceofを使って具象クラスの増減を吸収するのが骨子かと思います。
StrategyとVisitorの違いですが、呼び出す側から見て、
・新しい処理の追加に強いのがVisitor
従ってvisitorは訪問先の構造を知らなくてはならない。状態も持つことが多いはず。
(例:エクスローラのデータサイズ合計の表示、グラフ表示のプラグインなど)
・構造が殆ど同じで、一部処理を切り替えるのがStrategy
状態を持たない場合もあるでしょう。
(例:データベースとの接続で、MS Access/Oracle/My SQLなど)
ではないでしょうか?
ynomura
鋭いコメントありがとうございます。
SumVisitorが、全てのStableClassの具象クラスについて、それを引数として受けるためのメソッドを定義するべきかどうかというのは、確かに悩みました。実は私も、最初はSumVisitorのvisitの引数の型を抽象StableClassにしていました。その方が楽だからです。
おっしゃる通り、そのようにして、具象クラスのvisitメソッドにてinstanceofを使ってどのStableClassの具象クラスかで場合分けをするようにすれば、StableClassCが現れてもSumVisitor以下全てに改造が発生することになりません。
しかし、いわゆるGoF本では、Visitorインターフェースは全ての具象Visiteeクラスについてvisitメソッドを定義しています(参考リンク1)ので、ここではそれに従いました。
実はVisitorパターンの目的の1つは、そのような場合分けによるvisitメソッドの複雑化を回避することにあります(参考リンク2)。オブジェクト指向設計は元々、できるだけ条件分岐はクラス分け(データの特徴付け)やメソッドのオーバーロード(協調相手の特徴付け)によって解決しようとする考え方ですので、それを追求した結果、VisitorパターンではVisitorインターフェースでVisiteeの具象クラスを解決するようになっているのだと思います。
実用的かどうかは別にして、VisitorインターフェースにおけるVisitor側の具象クラスの解決は、Visitorパターンの定義の一部だと考えて良いと思います。また、VisitorパターンではVisiteeが主でVisitorが従で、Visitee側は不変(なので"Stable"というクラス名にしました)だという前提があり、もしVisiteeに追加変更があれば全てのVisitorに変更が及ぶというのはVisitorパターンの宿命と考えて良いと思います。
なお、acceptメソッドをVisitableインターフェースと定義し、visitメソッドにて、VisiteeがVisitableかどうかをinstanceofで調べるのは、Visitorパターンの発展形として知られているようです。この用途でinstanceofを使用するのはVisitorパターンの範囲内だと思いますし、そういう選択肢があることはVisitorパターンを適用する上で必須の知識だと思いますが、VisitableでないStableClassはVisitorパターンに当てはまらないので、本記事では省きました。
後半のStrategyとVisitorの違いについては、全くおっしゃる通りだと思います。