Visitorパターン


 ★★☆

Visitorパターン

処理を行うクラスとデータ構造を持つクラスを分離する

Visitor とは、英語で「訪問者」を意味します。Visitor パターンでは、「処理」を訪問者である Visitor クラスに記述することで、処理の追加を簡単にします。

あるオブジェクトに対する処理を別のオブジェクトに委ねるわけです。この際、相手のオブジェクトに、thisキーワードを使って、自分自身をすべて引き渡してしまいます。そして、相手のオブジェクトは、自分のメソッドを呼び出して処理を行います。これは、データ構造を持つオブジェクトとその処理をするオブジェクトとを分離することを目的としています。そうすることでデータ構造を持つオブジェクトをシンプルに保ち、そのデータ構造に対する処理を外出しにして、処理を追加しやすくするのです。



例えば、家の「水道工事」を行ってもらう場合、あなたは、「水道工事業者」を家に呼んで、「よろしくお願いします。」と言って、後は全てお任せしますよね。同様に、「庭の手入れ」を行ってもらう場合は、「庭師」を家に呼んで、全てお任せしてしまうでしょう。そのほかにも、電気工事業者を呼ぶことも、リフォーム業者を呼ぶこともあるでしょう。これらの訪問者に対して、あなたは、「では、よろしく」と言って、ほとんどの作業をお任せするはずです。

お任せの仕方に多少の違いがあるかもしれませんが、最終的には、全てを業者にお任せすることになると思います。もし、新しいサービスを提供する業者が現れたときにも、各家庭は、なんら態度を変える必要が無く、その業者を呼んで、「よろしくお願いします。」というだけで、その新しいサービスを受けることができます。


accept() メソッドは以下のような呼び出しになります。
element.accept(visitor)
visit メソッドは以下のような呼び出しになります。
visitor.visit(element)

この2つを見るとちょうど正反対の関係になります。なぜこのような複雑なメソッド呼び出しをしなけらばならないのでしょうか。

それは、処理(visitor)をデータ構造(element)から分離するためです。同じデータ構造に対してまったく違う処理をする場合にも visitor の変更だけですみます。

ただし、各 element に処理を持たせず visitor に集中させるというのは、気を付けないとカプセル化を破壊することにもなります。visitor は element から必要な情報を取得して働きますが、必要な情報を得られないとうまく働くことができません。その反面、公開すべきではない情報までを公開してしまうと、将来データ構造を変更することが難しくもなるからです。

また、対象となる element が増えると、各 visitor がそれを扱えるようにメソッドを追加する必要がでてくる点にも注意が必要です。



例題

ファイル構成
dir1
├ file1
├ dir2
│ ├ file2
│ └ file3
└ file4

ファイルシステムは、ディレクトリとファイルの階層構造でできています。指定されたディレクトリの中のファイルとそのサイズを表示するクラスを作成しなさい。もちろん、さらに下層のディレクトリの中のファイルも表示します。


実行結果
dir1\file1 (10MB)
dir1\dir2\file2 (20MB)
dir1\dir2\file3 (30MB)
dir1\file4 (40MB)









Visitorパターンを使用しない例

IEntry.java
public interface IEntry { public void dir(String path); }
Directory.java
public class Directory implements IEntry { private List<IEntry> list = null; private String name = null; public Directory(String name) { this.name = name; list = new ArrayList<IEntry>(); } public void add(IEntry entry) { list.add(entry); } public void dir(String path) { path += name + "\\"; Iterator<IEntry> it = list.iterator(); while (it.hasNext()) { IEntry entry = it.next(); entry.dir(path); } } }
File.java
public class File implements IEntry { private String name = null; private int size = 0; public File(String name, int size) { this.name = name; this.size = size; } public void dir(String path) { System.out.println(path + name + " (" + size + "MB)"); } }
Main.java
public class Main { public static void main(String[] args) { File file1 = new File("file1", 10); File file2 = new File("file2", 20); File file3 = new File("file3", 30); File file4 = new File("file4", 40); Directory dir1 = new Directory("dir1"); dir1.add(file1); Directory dir2 = new Directory("dir2"); dir2.add(file2); dir2.add(file3); dir1.add(dir2); dir1.add(file4); dir1.dir(""); } }

Visitorパターンを使用した例

IEntry.java
public interface IEntry { void accept(IVisitor v); String getName(); }
Directory.java
public class Directory implements IEntry { private List<IEntry> list = null; private String name = null; public Directory(String name) { this.name = name; list = new ArrayList<IEntry>(); } public String getName() { return name; } public IEntry add(IEntry entry) { list.add(entry); return this; } public Iterator<IEntry> iterator() { return list.iterator(); } public void accept(IVisitor v) { v.visit(this); } }
File.java
public class File implements IEntry { private String name = null; private int size = 0; public File(String name, int size) { this.name = name; this.size = size; } public String getName() { return name; } public int getSize() { return size; } public void accept(IVisitor v) { v.visit(this); } }
IVisitor.java
public interface IVisitor { void visit(File file); void visit(Directory directory); }
ListVisitor.java
public class ListVisitor implements IVisitor { private String currentDir = ""; public void visit(File file) { System.out.println(currentDir + file.getName() + " (" + file.getSize() + "MB)"); } public void visit(Directory directory) { String saveDir = currentDir; currentDir += directory.getName() + "\\"; Iterator<IEntry> it = directory.iterator(); while (it.hasNext()) { IEntry entry = (IEntry) it.next(); entry.accept(this); } currentDir = saveDir; } }
Main.java
public class Main { public static void main(String[] args) { File file1 = new File("file1", 10); File file2 = new File("file2", 20); File file3 = new File("file3", 30); File file4 = new File("file4", 40); Directory dir1 = new Directory("dir1"); dir1.add(file1); Directory dir2 = new Directory("dir2"); dir2.add(file2); dir2.add(file3); dir1.add(dir2); dir1.add(file4); dir1.accept(new ListVisitor()); } }

まずは、Composite パターンを利用して上記のファイル構造を記述しました。しかし、ファイル構造を記述したDirectory クラス、File クラス共に dir() メソッドの中にどう表示するか(Directory クラスはフォルダ名を\でつなげ、File クラスは括弧の中にファイルサイズを表示する)という処理も書かれてしまっています。

Visitor パターンでは、 データ構造は Directory クラス、File クラス、処理は ListVisitor クラスに分けて書かれています。一般に Visitor クラスの実装は、データ構造の Directory クラス、File クラスとは別に開発することができます。つまり、部品として独立性を高めていることになります。もし、処理内容を Directory クラス、File クラスのメソッドとしてプログラムしてしますと( Composite パターンがそうですが…)、新しい「処理」を追加して機能拡張するたびに、 Directory クラス、File クラスを修正しなくてはならなくなります。

最初に呼ばれる Directory インスタンスの accept() からは、引数で渡された ListVisitor インスタンスの visit(Directory) が呼び出されます。visit(Directory) からは、次のオブジェクトが、ディレクトリならば Directory インスタンスの、ファイルならば File インスタンスの accept() が呼び出されます。そして、Directory インスタンスの accept() からは、また ListVisitor インスタンスの visit(Directory) が呼び出されますし、File インスタンスの accept() からは、ListVisitor インスタンスの visit(File) が呼び出され、ファイル名とサイズが表示されます。

メソッドの呼び出しをもう一度整理してみましょう。この例では、

ListVisitorクラスでは、
entry.accept(this)   this は ListVisitor自身のインスタンス
Directory クラス、File クラスでは、
v.visit(this)   this は Directory や File 自身のインスタンス

としています。これにより、相手のオブジェクトに、thisキーワードを使って、自分自身をすべて引き渡しています。そして、相手のオブジェクトは、自分のメソッドを呼び出して処理を行います。これは、データ構造を持つオブジェクトとその処理をするオブジェクトとを分離することを目的としています。そうすることでデータ構造を持つオブジェクトをシンプルに保ち、そのデータ構造に対する処理を外出しにして、処理を追加しやすくしているのです。