かとじゅんの技術日誌

技術の話をするところ

今さらデザインパターン Visitorパターン編

今回は,Visitorパターン.一言でいうとデータ構造(つまりモデル)にVisit(訪問)して処理するパターン.目的はモデルとモデルを処理するロジックの分離です.実は使ったことがないw よい機会なので考えてみたいと思います.

Wikipedia Visitorパターン

まずは事例から

いつものごとく,ダイちゃんのサンプルを改編!

モデルに直接ロジックを書くのではなく,Visitorを受け入れてそのVisitorにモデル(自分自身)を渡し,ロジックを実行するイメージです.
こうすることでモデルとロジックが分離できるというパターンなんですね.各Converterのconvertメソッドで渡されたTable, Columnのモデルに用意したVisitorを訪問させて処理させています.

public interface Acceptor {

	public void accept(Visitor visitor, StringBuilder result);

}

acceptメソッドでは,引数に渡されたVisitor#visitを読んでいます.これをダブルディスパッチと呼びます.AcceptorとVisitorの組み合わせでvisitの処理が変化するということですね.

public class Column implements Acceptor {
	public String name;
	public String type;

	public Column(String name, String type) {
		this.name = name;
		this.type = type;
	}

	public void accept(Visitor visitor, StringBuilder result) {
		visitor.visit(this, result);
	}
}
import java.util.List;

public class Table implements Acceptor {

	public String name;
	public List<Column> columns;

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

	public void accept(Visitor visitor, StringBuilder result) {
		visitor.visit(this, result);
	}

}
public interface Visitor {

	void visit(Table table, StringBuilder result);

	void visit(Column column, StringBuilder result);

}

Tableから得られるColumnの処理は,Column自体にVisitor(自分自身)を訪問させて処理しています.Visitorの渡り歩きですね.TableとColumnの識別はVisitor#visitメソッドのオーバーロードで代用しています.コードがすっきりしてますね.

public class MySQLVisitorImpl implements Visitor {

	public void visit(Table table, StringBuilder result) {
		result.append("CREATE TABLE ").append(table.name).append("(\n");
		for (Column column : table.columns) {
			column.accept(this, result);
		}
		result.delete(result.length() - 2, result.length() - 1);
		result.append(");\n");
	}

	public void visit(Column column, StringBuilder result) {
		result.append("  ").append(column.name).append(" ");
		if ("integer".equals(column.type)) {
			result.append("INT");
		} else if ("string".equals(column.type)) {
			result.append("TEXT");
		} else {
			result.append(column.type);
		}
		result.append(",\n");
	}

}
public class PostgreSQLVisitorImpl implements Visitor {

	public void visit(Table table, StringBuilder result) {
		result.append("CREATE TABLE ").append(table.name).append("(\n");
		for (Column column : table.columns) {
			column.accept(this, result);
		}
		result.delete(result.length() - 2, result.length() - 1);
		result.append(");\n");
	}

	public void visit(Column column, StringBuilder result) {
		result.append("  ").append(column.name).append(" ");
		if ("integer".equals(column.type)) {
			result.append("INTEGER");
		} else if ("string".equals(column.type)) {
			result.append("VARCHAR(32)");
		} else {
			result.append(column.type);
		}
		result.append(",\n");
	}

}

各Visitorはname属性付きでdicon上に定義されているとして,visitorプロパティにBindingアノテーションでDIするVisitorを指定します.

public class MySQLConverter implements Converter {

	@Binding("mySQLVisitor")
	public Visitor visitor;

	public String convert(Table table) {
		StringBuilder result = new StringBuilder();
		table.accept(visitor, result);
		return result.toString();
	}

}
public class PostgreSQLConverter implements Converter {

	@Binding("postgreSQLVisitor")
	public Visitor visitor;

	public String convert(Table table) {
		StringBuilder result = new StringBuilder();
		table.accept(visitor, result);
		return result.toString();
	}

}

あら,各コンバータのconvertが同じ処理になってしまいました.
では,抽象クラスにまとめてconvertメソッドを一本化しておきましょう.

public abstract class AbstractSQLConverter implements Converter {

	private Visitor visitor;

	public String convert(Table table) {
		StringBuilder result = new StringBuilder();
		table.accept(visitor, result);
		return result.toString();
	}

	public void setVisitor(Visitor visitor) {
		this.visitor = visitor;
	}

}

あとは,訪問するVisitorだけを変えましょう.これですっきりしました.

public class MySQLConverter extends AbstractSQLConverter {

	@Binding("mySQLVisitor")
	@Override
	public void setVisitor(Visitor visitor) {
		super.setVisitor(visitor);
	}

}
public class PostgreSQLConverter extends AbstractSQLConverter {

	@Binding("postgreSQLVisitor")
	@Override
	public void setVisitor(Visitor visitor) {
		super.setVisitor(visitor);
	}

}

まとめ

私は,モデルとロジックを別々に作り,ロジックに処理対象のモデルを関連付けるということはよくやっているのですが(Daoパターンのようなもの),このようなパターンは使ったことありませんでした.
このパターンの場合は,逆にモデルにロジックを関連付けるパターンなんですね.おそらくモデル中心にロジックを考えなければならなばい場合は使いやすいパターンなんでしょう.
少なくとも,モデルに直接ロジックを書かざるを得ないなら,Visitorパターンにするべきだと思います.以上,ご参考までに.

追記:StringBuilderは引数に持たせていますが,Visitorのプロパティにしたほうがよいかもしれません.