Visitorパターン

作成:2004年6月7日

吉田誠一のホームページ   >   ソフトウェア工学   >   技術コラム   >   デザインパターン

Visitorパターンは、集合の要素を1つ1つ巡って、何らかの処理を行っていく時のパターンである。

目次

  1. ダブルディスパッチ
  2. 呼び出しの非対称性と、拡張の方向性
  3. Iteratorパターンとの違い
  4. 「デザインパターン入門」の例について
  5. データ構造と処理の分離
  6. Visitorパターンを使うべきケース
  7. Visitorパターンのメリットとデメリット

ダブルディスパッチ

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パターンを使う際には、これらのメリット・デメリットを秤にかける必要があると思う。

Copyright(C) Seiichi Yoshida ( comet@aerith.net ). All rights reserved.