デザインパターンvisitorについて勉強した

visitorパターンとは

visitorは訪問者の意味です。
複数の情報が配列などでひとまとめにされているとき、そのデータに対する処理をデータと一緒のクラスに書いてしまえば簡単です。
しかし、その処理が複数パターンある場合、データと処理を別で管理してしまった方が見通しが良くなります。

例えば公園の遊具があるとします。
遊具ごとにどのような使い方、管理の仕方があるかは異なります。
ここに誰かがやってきたとき、その人は何をするでしょう。
子供なら遊ぶでしょうし、管理者であれば遊具の整備をします。

こう考えてみると滑り台は滑り台クラス、管理者は管理者クラスとして作った方が現実のイメージとつなげやすいのではないでしょうか。
遊具が、そこにきた人で遊ばせたり管理させたりするわけではないですから。

Visitorインタフェース

コード例

public interface Visitor {

	void visit(Swing swing);

	void visit(Slide slide);

	void visit(Bench bench);
}

公園にある遊具を表すインタフェースです。

宣言されたメソッドは全てvisitメソッドで、オーバーロードしています。
この公園にはswing:ブランコ、slide:滑り台、bench:ベンチがあります。
このインタフェースを実装したのが子供クラスだったらvisit(Swing)メソッドでは何をするか、Slideだったら、Benchだったらと実装します。

もしこの公園に新たに鉄棒を追加したいなら、visitor(鉄棒)メソッドを追加してやることになります。

Playsetインタフェース

public interface Playset {

	void execute(Visitor visitor);
}

遊具が実装するインタフェースです。

executeメソッドは訪問者を受け入れます。
ただし、その遊具が故障中であるなどした場合は受け入れないという分岐もできます。

実装

Swingクラス

public class Swing implements Playset {

	@Override
	public void execute(Visitor visitor) {
		visitor.visit(this);
	}
}

ブランコのクラスです。

Playsetインタフェースを実装しており、executeメソッドをオーバライドしています。

executeメソッドでは訪問者を全て受け入れるようになっています。
visitorに対してvisitメソッドを呼び出しています。引数にはthisを渡していますね。
Visitorインタフェースにはvisitメソッドがあり、引数はSwing,Slide,Benchでした。
今このクラスはSwingクラスですから、visit(this)によって呼び出されるのはvisit(Swing)メソッドとなります。
そう考えるとVisitor.visit(Swing)メソッドにはブランコの使用方法を書けば良いことがわかりますね。

やることは同じなので、同じようにSlideクラス、Benchクラスのコードも載せていきます。

Slideクラス

public class Slide implements Playset {

	@Override
	public void execute(Visitor visitor) {
		visitor.visit(this);
	}
}

Benchクラス>|java|
public class Bench implements Playset {

@Override
public void execute(Visitor visitor) {
visitor.visit(this);
}
}
|

Childクラス

public class Child implements Visitor {

	private String name ;

	public Child(String name) {
		this.name = name;
	}

	@Override
	public void visit(Swing swing) {
		System.out.println(String.format("%s君がブランコをこぐ",name));
	}

	@Override
	public void visit(Slide slide) {
		System.out.println(String.format("%s君が滑り台を滑る",name));
	}

	@Override
	public void visit(Bench bench) {
		System.out.println(String.format("%s君がベンチに座る",name));
	}
}

今度はVisitorインタフェースの実装クラスです。

childクラスは子供のクラスです。

nameフィールドはこの子供の名前を保持します。
コンストラクタ引数はnameフィールドに渡します。

visitメソッドは3つありますね。
それぞれSwing,Slide,Benchで何をするかを実装しています。
子供なのでどのように遊ぶかが書かれていますね。

Administratorクラス

public class Administrator implements Visitor {

	private String name ;

	public Administrator(String name) {
		this.name = name;
	}

	@Override
	public void visit(Swing swing) {
		System.out.println(String.format("%sさんは絡まっていたブランコの鎖を解いた",name));
	}

	@Override
	public void visit(Slide slide) {
		System.out.println(String.format("%sさんは滑り台の手すりが壊れていないか確認した",name));
	}

	@Override
	public void visit(Bench bench) {
		System.out.println(String.format("%sさんはベンチにペンキを塗った",name));
	}
}

Administratorは管理者の意味です。

基本的な実装はChildクラスと同じですが、visitメソッドの内容が異なります。
こちらは管理方法が書かれています。

Antクラス

public class Ant implements Visitor {

	@Override
	public void visit(Swing swing) {
		System.out.println("アリさんは巣にごはんを持ち帰っているところだ");
	}

	@Override
	public void visit(Slide slide) {
		System.out.println("アリさんは巣にごはんを持ち帰っているところだ");
	}

	@Override
	public void visit(Bench bench) {
		System.out.println("アリさんは巣にごはんを持ち帰っているところだ");
	}

}

ついでにアリさんのクラスも作ってみました。

アリさんは遊具には目もくれず今日を一所懸命生きているようですね。

実行

public class Main {

	public Main() {
		Playset[] playsets = new Playset[] {
				new Swing(),
				new Slide(),
				new Bench(),
		};
		Visitor[] visitors = new Visitor[] {
				new Child("山田"),
				new Administrator("竹本"),
				new Ant(),
		};

		for(Visitor v : visitors) {
			for(Playset p : playsets) {
				p.execute(v);
			}
		}
	}

	public static void main(String[] args) {
		new Main();
	}
}

各クラスのインスタンスを作り、forループを使って遊具と訪問者全ての組み合わせでexecuteメソッドを呼び出しています。

山田君がブランコをこぐ
山田君が滑り台を滑る
山田君がベンチに座る
竹本さんは絡まっていたブランコの鎖を解いた
竹本さんは滑り台の手すりが壊れていないか確認した
竹本さんはベンチにペンキを塗った
アリさんは巣にごはんを持ち帰っているところだ
アリさんは巣にごはんを持ち帰っているところだ
アリさんは巣にごはんを持ち帰っているところだ

特徴

データと処理を分割して管理するために便利なパターンです。

新たに処理(訪問者)を増やしたければVisitorインタフェースを実装した新たなクラスを作るだけで済みます。

反対にデータ(遊具)を増やすにはVisitorインタフェースを実装した全てのクラスを書き換えることになるので、少々手間がかかります。

補足

後から処理を追加するときに威力を発揮するパターンですが、一つだけ注意すべき点があります。
データ側がどこまで自身の情報を公開し、外部からの編集を許可するかを先に決めておく必要があることです。
例では管理人がベンチにペンキを塗っていましたが、ペンキの色までは指定していませんでした。(そもそも例では色という概念すらありませんでした)
後からベンチの色を指定したくなった場合はBenchクラスを変更しなければいけなくなります。
既存のクラスはなるべく変更しないためにも、なるべく後の拡張性まで考えてクラスを作ることを心がけたいところです。