DP-23: Visitor パターン
オブジェクト構造を走査しながら、各要素に対する処理を「訪問者(Visitor)」として外部から追加するパターンです。 要素クラスを変更せずに新しい操作を追加できます。
Visitor パターンとは
Visitor(ビジター/訪問者)パターンは、オブジェクト構造(ツリーやリストなど)の要素を 走査しながら、要素の種類に応じた処理を「訪問者」クラスに集約するパターンです。 操作を要素クラスの外に切り出すことで、要素クラスを変えずに新しい操作を追加できます。
登場する役割
- Visitor(訪問者): 各要素への処理を定義するインターフェース。要素の種類ごとに
visit()メソッドを持つ - ConcreteVisitor(具体訪問者): 実際の処理を実装するクラス(例: ファイル一覧表示、サイズ計算)
- Element(要素):
accept(visitor)メソッドで Visitor を受け入れるインターフェース - ConcreteElement(具体要素): accept() で
visitor.visit(this)を呼び出す(ダブルディスパッチ)
ダブルディスパッチとは、メソッドの呼び出しが「要素の型」と「Visitor の型」の 2つで決まる仕組みです。accept() で Visitor を呼ぶことで、 実行時に「どの要素が」「どの Visitor で」処理されるかが決まります。
主なユースケースとして、コンパイラの AST(抽象構文木)走査、ファイルシステムの集計処理、 XML/JSON ドキュメントの変換などがあります。 下のサンプルでは、ファイルとディレクトリからなるツリー構造を、 「一覧表示」「合計サイズ計算」「拡張子カウント」の 3 つの Visitor で処理します。
サンプルコード
Java 8 では Visitor インターフェースと Element インターフェースを定義し、accept() メソッドで訪問を委譲します。新しい操作(Visitor)を追加しても、FileNode や DirectoryNode のコードは一切変更しなくて済みます。
よくあるミス・注意点
⚠️ 新しい要素クラスを追加すると全 Visitor を修正する必要がある
Visitor パターンの弱点は「要素の種類を増やすこと」が難しい点です。 新しい要素(例: SymlinkNode)を追加すると、全ての Visitor インターフェースと 具体 Visitor クラスに新しい visitSymlink() メソッドを追加する必要があります。 要素の種類が頻繁に変わる設計には向いていません。
⚠️ accept() の実装を間違えると Visitor が呼ばれない
accept() の中でvisitor.visitFile(this) の this を正しく渡さないと、 ダブルディスパッチが機能しません。 親クラスの accept() をそのまま継承するだけでは、visit() に渡される型が意図した型にならないことがあります。
⚠️ ListVisitor の depth カウンターがずれる
サンプルの ListVisitor は visitDirectory() でdepth++ を行い、 子要素の走査が終わった後も depth を戻していません。 同一 Visitor を複数のツリーに適用する場合は、depth の管理に注意が必要です。 再利用する場合はツリーを抜けたタイミングで depth-- する処理を追加しましょう。
⚠️ Visitor がステートフル(状態を持つ)な場合は使い捨てにする
SizeCalculatorVisitor の totalSize のように、 Visitor が内部状態を持つ場合は同じインスタンスを複数のツリーに適用しないでください。 前回の走査結果が残ったまま加算されてしまいます。 1回の走査ごとに新しい Visitor インスタンスを生成しましょう。
テストする観点
- ファイルのみのディレクトリに対して
SizeCalculatorVisitorを適用すると、全ファイルのサイズ合計が返ること - 空のディレクトリ(子要素が 0 件)に対して
SizeCalculatorVisitorを適用すると 0 が返ること(境界値) - ネストしたディレクトリ構造(root → src → Main.java など)を走査したとき、全ファイルが集計されること
FileCountVisitor(".java")が大文字小文字を区別せずに拡張子を一致させること(例: ".JAVA" も一致)- 同一の Visitor インスタンスを再利用せず、新しいインスタンスで走査したとき前回の集計結果が引き継がれないこと
- 新しい Visitor を追加しても、FileNode・DirectoryNode のコードを変更しなくて済むことを確認すること(拡張性の検証)