ダブルディスパッチ
Visitorパターンでは、データと処理を分けて、別々のクラスとして作る。そして、この2つのクラスが、互いに相手のメソッドを呼び出すことによって、連係して動作する。この仕組みをダブルディスパッチという。
具体的には、下記のようになっている。Acceptorインターフェースは、データを表す。処理を受け入れるもの、という意味で、Acceptorという名前になっている。Acceptor1, Acceptor2は、Acceptorインターフェースを実装したクラスである。Visitorクラスは、処理を表す。データを1つ1つ訪れて処理を行うもの、という意味で、Visitorという名前になっている。Visitor1は、具体的な処理を記述した、Visitorクラスのサブクラスである。
public interface Acceptor { public abstract void accept ( Visitor visitor ); } public class Acceptor1 implements Acceptor { public void accept ( Visitor visitor ) { visitor.visit(this); } } public class Acceptor2 implements Acceptor { public void accept ( Visitor visitor ) { visitor.visit(this); } } public abstract class Visitor { public abstract void visit ( Acceptor1 acceptor1 ); public abstract void visit ( Acceptor2 acceptor2 ); } public class Visitor1 extends Visitor { public void visit ( Acceptor1 acceptor1 ) { // acceptor1についての処理を実行。 : : // acceptor1から辿れる次のデータを訪れる。 next_acceptor.accept(this); } public void visit ( Acceptor2 acceptor2 ) { // acceptor2についての処理を実行。 : : // acceptor2から辿れる次のデータを訪れる。 next_acceptor.accept(this); } }
呼び出しの非対称性と、拡張の方向性
Visitorパターンでは、Visitorは自分自身を引数として、Acceptorのacceptメソッドを呼び出す。また、Acceptorは自分自身を引数として、Visitorのvisitメソッドを呼び出す。
これらは、一見すると対称的な呼び出し関係にあるように思えるが、実は対称的ではない。
Acceptorのacceptメソッドは、引数としてVisitorを取る。Visitorは抽象クラスであり、visitメソッドを呼び出すことしかできない。Acceptor側では、引数で渡されたVisitorが、具体的にどのような処理を行うものなのかを知らない。
一方、Visitorのvisitメソッドは、引数としてAcceptorインターフェースではなく、そのインターフェースを実装した具体的なクラスを取る。前述の例では、2種類のデータ(Acceptor1, Acceptor2)があったため、それぞれを引数に取るvisitメソッドが、2つ作られていた。
Visitorのvisitメソッドには、具体的な処理を記述する。処理の内容は、データによって異なる。そのため、visitクラスの引数は、抽象的なAcceptorインターフェースではなく、具体的な実装クラスでなければならない。
この非対称性により、Visitorパターンを使った仕組みでは、拡張の方向性は次のように限定される。
- 処理(Visitor)はいくらでも増やすことができる。
- データ(Acceptor)を増やすことはできない。
Iteratorパターンとの違い
冒頭の概要を読むと、Iteratorパターンの解説にある概要と、良く似ている。
Visitorパターンは、集合の要素を1つ1つ巡って、何らかの処理を行っていく時のパターンである。
Iteratorパターンは、集合の要素に順に1つずつアクセスする時のパターンである。
Iteratorパターンを使って、集合の要素に何らかの処理を行う場合は、次のようなコードになる。
Iterator it = set.iterator(); while (it.hasNext()) { Data data = (Data)it.next(); operator.operate(data); }
Iteratorパターンでは、たくさんの要素について1つ1つ処理を行っていることが良く分かる。
一方、Visitorパターンでは、次のようなコードになる。
operator.operator(data);
Visitorパターンでは、ある1つの要素について呼び出せば、ダブルディスパッチの仕組みによって、その要素から辿れる要素について、次々と処理が行われていく。そのため、呼び出し側から見ると、あたかもたった1つの要素について処理を行っているように見えてしまう。
「デザインパターン入門」の例について
Visitorパターンについて、下記の本(以下「デザインパターン入門」と記す)
Java言語で学ぶデザインパターン入門
結城浩著
ソフトバンクパブリッシング株式会社
で取り上げられている例は、Visitorパターンを使うケースではないように思う。
「デザインパターン入門」では、ファイルやディレクトリの一覧を表示する処理にVisitorパターンが使われている。しかし、「デザインパターン入門」のListVisitorクラスは、再帰呼び出しを使って、次のように書き換えることができる。
public class ListVisitor { private String currentdir = ""; public void visit (Entry entry) { System.out.println(currentdir + "/" + entry); String savedir = currentdir; currentdir = currentdir + "/" + entry.getName(); try { Iterator it = entry.iterator(); while (it.hasNext()) { Entry entry = (Entry)it.next(); visit(entry); } } catch ( FileTreatmentException exception ) { } currentdir = savedir; } }
敢えて複雑なVisitorパターンを使う必要はない。
データ構造と処理の分離
「デザインパターン入門」では、ファイルやディレクトリの一覧を表示する処理にVisitorパターンを使っているが、その理由として、『データ構造と処理を分離するため』とされている。しかし、これは誤っていると思う。
「デザインパターン入門」には、次のような文章がある。
もし、処理の内容をFileクラスやDirectoryクラスのメソッドとしてプログラムしてしまうと、新しい「処理」を追加して機能拡張したくなるたびに、FileクラスやDirectoryクラスを修正しなければならなくなります。
ここで説明されているのは、『データ構造と処理の分離』ではなく、『データと処理の分離』である。
「デザインパターン入門」の例では、データ構造は次の通りになっている。
- Directoryは、その下にいくつかのEntry(FileやDirectory)を持っている。
- Fileは、その下に何も持っていない。
- これらがツリー構造になっている。
「デザインパターン入門」の例で、処理を表しているのはListVisitorクラスである。しかし、処理を表しているはずのListVisitorクラスのコードを読むと、上記のデータ構造が、そのまま実装されている。すなわち、データ構造と処理は、分離されていない。
データ構造と処理を分離するためには、Visitorパターンではなく、Iteratorパターンを使うべきと考える。
例えば、人名リストに含まれる人名を一覧表示することを考えてみよう。人名リストのデータ構造は、個数の上限が決まった配列になっているかもしれないし、先頭から順に並んだリストになっているかもしれない。または、検索がしやすいように、二分木になっているかもしれない。
人名リストのデータ構造がどのようになっていても良いように、一覧表示する処理を作るためには、人名リストが持つ人名データに1つ1つアクセスできるようなiteratorを、人名リストが返すようになっていれば良い。
人名リストのデータ構造を変更すると、iteratorも書き換える必要がある。だが、一覧表示をする処理はiteratorを使って書かれており、データ構造に依存していないので、データ構造が変わっても、書き換える必要はない。
Iteratorパターンを使うもう1つのメリットは、アクセス順序と処理も分離できることだ。人名リストを、あいうえお順に表示したい場合は、人名にあいうえお順にアクセスするiteratorを作れば良い。アルファベット順に表示したい場合は、アルファベット順にアクセスするiteratorを作れば良い。いずれにせよ、一覧表示をする処理には、手を加える必要はない。
「デザインパターン入門」の例にあるListVisitorクラスは、ディレクトリとファイルを作成した順にしか表示できない。もし、アルファベット順に表示したり、ディレクトリだけを先に表示したくなった時は、そのたびにListVisitorと良く似た別のVisitorクラスを作らなくてはならない。
Visitorパターンを使うべきケース
前章では、「デザインパターン入門」に載っている例を、再帰呼び出しを使って書き換えた。
再帰呼び出しを使った場合のListVisitorクラスを良く見ると、FileクラスやDirectoryクラスは使われておらず、Entryクラスだけを使って記述されていることが分かる。すなわち、実際にはFileやDirectoryなど、いくつかの種類があったとしても、処理側から見れば、集合の中にはEntryという1種類のデータしか存在しない、という状況であった。
もし、ListVisitorクラスが、
- Fileについては、getFileName()を呼び出して表示する。
- Directoryについては、getTitle()を呼び出して表示する。
という仕様であった場合は、次のようになる。
public class ListVisitor { private String currentdir = ""; public void visit (Entry entry) { if (entry instanceof File) { File file = (File)entry; System.out.println(currentdir + "/" + file.getFileName()); } else if (entry instanceof Directory) { Directory directory = (Directory)entry; System.out.println(currentdir + "/" + directory.getTitle()); } : } }
もしくは、次のようになる。
public class ListVisitor { private String currentdir = ""; public void visit (File file) { System.out.println(currentdir + "/" + file.getFileName()); } public void visit (Directory directory) { System.out.println(currentdir + "/" + directory.getTitle()); String savedir = currentdir; currentdir = currentdir + "/" + directory.getName(); Iterator it = directory.iterator(); while (it.hasNext()) { Entry entry = (Entry)it.next(); if (entry instanceof File) { visit((File)entry); } else if (entry instanceof Directory) { visit((Directory)entry); } } currentdir = savedir; } }
この場合は、FileとDirectoryとでは、処理が異なる。すなわち、ListVisitorが処理を行う対象は、Entryという1つのデータではなく、FileとDirectoryという2種類のデータである。
このように、集合にいくつかの異なる要素が含まれている場合は、再帰呼び出しやIteratorを使うと、場合分けが必要になってしまう。要素の種類が多くなるほど、たくさんのif文の分岐が並んだマヌケなコードになる。
Visitorパターンを使うと、instanceofやキャストを使う必要がなく、if文の分岐が並ぶこともない。
何種類もの要素があっても、Visitorから見れば、すべてAcceptorであり、acceptメソッドを呼び出すことができる。Visitorでは、抽象的なAcceptorインターフェースのacceptメソッドを呼び出すように記述する。だが実際には、例えばFileであれば、Fileクラスのacceptメソッドが呼び出される。
Fileクラスのacceptメソッドは、自分自身、すなわちFileインスタンスを引数とするvisitメソッドを呼び出す。その結果、Fileに対する処理が適切に行われるのである。
Visitorパターンのメリットとデメリット
私なりに考えたところでは、Visitorパターンを使うべきケースは、下記の場合しか思いつかなかった。
- 集合に、型(クラス)が異なる要素が含まれている。
- 要素の型(クラス)に応じて、行う処理が異なる。
- ある要素から、次の要素に辿る際、次の要素の型(クラス)が分からない。
「デザインパターン入門」の例にもあるように、ツリー構造に対して何らかの処理をする時は、Visitorパターンを使うことが理想だと思われているかもしれない。だが、実際にVisitorパターンを使うべきであることは、稀なように思われる。
集合にいくつかの異なる要素が含まれている場合でも、Visitorパターンを使わない時のデメリットは、if文の分岐が並ぶことだけである。Visitorパターンを使うと、再帰呼び出しが持つシンプルさや、Iteratorによるデータ構造と処理の分離といったメリットを失うことになる。Visitorパターンを使う際には、これらのメリット・デメリットを秤にかける必要があると思う。