概要
データ構造を変更せずに、新しい処理(操作)を追加できるようにする。Visitor パターンは、ファイルシステムの走査、構文木の解析、ドキュメント変換など、構造が安定していて処理のバリエーションが増える場面で有効です。各要素が accept メソッドで Visitor を受け入れ、Visitor の visit メソッドが具体的な処理を行うダブルディスパッチの仕組みにより、要素の型に応じた処理を実行します。この記事では、ファイルシステム(ファイルとディレクトリ)を題材に、一覧表示・サイズ計算・拡張子カウントの3つの Visitor を実装します。sealed interface と record による要素の型安全な定義、Java 21 の switch パターンマッチングで Visitor を使わず直接型分岐する手法も確認します。
使いどころ
ファイルシステムのツリー構造を走査して、一覧表示・合計サイズ計算・特定拡張子のカウントをそれぞれ Visitor として追加する
構文木(AST)の各ノードに対してコード生成・型チェック・最適化の処理を Visitor として追加する
商品カタログのツリー構造に対して、価格集計・在庫チェック・レポート生成を Visitor で分離する
コード例
import java.util.ArrayList;
import java.util.List;
public class VisitorPatternDemo {
interface FileSystemVisitor {
void visitFile(FileNode file);
void visitDirectory(DirectoryNode dir);
}
sealed interface FileSystemNode
permits FileNode, DirectoryNode {
String getName();
void accept(FileSystemVisitor visitor);
}
record FileNode(String name, long sizeBytes)
implements FileSystemNode {
@Override public String getName() { return name; }
@Override public void accept(FileSystemVisitor v) {
v.visitFile(this);
}
}
static final class DirectoryNode implements FileSystemNode {
private final String name;
private final List<FileSystemNode> children
= new ArrayList<>();
DirectoryNode(String name) { this.name = name; }
@Override public String getName() { return name; }
void add(FileSystemNode node) { children.add(node); }
List<FileSystemNode> getChildren() { return children; }
@Override public void accept(FileSystemVisitor v) {
v.visitDirectory(this);
for (var child : children) child.accept(v);
}
}
static class SizeCalculatorVisitor
implements FileSystemVisitor {
private long totalSize = 0;
@Override public void visitFile(FileNode file) {
totalSize += file.sizeBytes();
}
@Override
public void visitDirectory(DirectoryNode dir) {}
long getTotalSize() { return totalSize; }
}
static class FileCountVisitor
implements FileSystemVisitor {
private final String extension;
private int count = 0;
FileCountVisitor(String ext) {
extension = ext.toLowerCase();
}
@Override public void visitFile(FileNode file) {
if (file.name().toLowerCase()
.endsWith(extension)) count++;
}
@Override
public void visitDirectory(DirectoryNode dir) {}
int getCount() { return count; }
}
public static void main(String[] args) {
var root = new DirectoryNode("project");
var src = new DirectoryNode("src");
src.add(new FileNode("Main.java", 2048));
src.add(new FileNode("Utils.java", 1024));
root.add(src);
root.add(new FileNode("pom.xml", 384));
var sizeCalc = new SizeCalculatorVisitor();
root.accept(sizeCalc);
System.out.println("合計: "
+ sizeCalc.getTotalSize() + " bytes");
var javaCount = new FileCountVisitor(".java");
root.accept(javaCount);
System.out.println(".java ファイル: "
+ javaCount.getCount() + " 件");
}
}Version Coverage
sealed interface で要素を型安全に限定し、record で不変な要素(FileNode)を簡潔に定義できる。instanceof パターンマッチングでキャスト不要に。
// Java 17: sealed interface + record で型安全に
sealed interface FileSystemNode
permits FileNode, DirectoryNode {
String getName();
void accept(FileSystemVisitor visitor);
}
record FileNode(String name, long sizeBytes)
implements FileSystemNode {
@Override public void accept(FileSystemVisitor v) {
v.visitFile(this);
}
}
// instanceof パターンマッチングでキャスト不要
if (node instanceof FileNode f) {
System.out.println(f.name() + ": " + f.sizeBytes());
}Library Comparison
注意点
要素の種類が頻繁に追加される場合、Visitor インターフェースにメソッドを追加する必要があり、既存の全 Visitor 実装クラスに影響する。要素が安定していることが前提
ダブルディスパッチの仕組み(element.accept(visitor) → visitor.visit(element))は初見で理解しにくい。チーム内でパターンの共通認識を持っておくこと
Visitor 内で状態を持つ場合(例: depth カウント)、同じ Visitor インスタンスで複数回走査すると状態が蓄積する。走査ごとに新しいインスタンスを作るか、リセットメソッドを用意する
Java 21 の switch パターンマッチングで sealed interface の型分岐ができるため、小規模なケースでは Visitor を使わず switch で済ませる選択肢もある
FAQ
Visitor は accept/visit のダブルディスパッチで型に応じた処理を実行します。instanceof 分岐は呼び出し側で型を判定します。要素が sealed interface なら Java 21 の switch で網羅チェック付きの分岐も可能です。
要素の種類が頻繁に追加される場面です。新しい要素を追加するたびに全 Visitor にメソッドを追加する必要があり、変更コストが高くなります。
Composite はツリー構造を表現し、Visitor はそのツリーを走査して処理を追加します。ファイルシステムやDOMツリーのように、再帰構造の走査と処理の分離に自然に組み合わさります。