StrategyパターンとVisitorパターンの使い分けに関する考察(1)

今更ながら、かの著名な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

//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)
}
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 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)
}
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 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)
}
SumVisitor.java
public 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);
}
OrdinarySum.java
public 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;
	}
}
Multiple.java
public 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;
	}
}
Result.java
public class Result {
	public int value;
}
Main.java
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));
	}
}
Visitor patternの特徴を際立たせるために、敢えてStrategy patternのテストコードとは違う形にしている。(StableClassも抽象化している)(getSum()を呼び出す処理ではStableClassについてもVisitorについてもインスタンスの具象クラスを知らなくて良いことを表しているつもり)

出力結果
Sum of object1 by sum1 = 15
Sum of object1 by sum2 = 56
Sum of object1 by sum2 = 120

なお、いわゆるGoF本は、図書館で借りられなかったため、今回まだ読んでいない。解説本を読んで理解したものをベースにしている。その内借りられたら、違うことを書いてないかどうか確かめることにする。