概要

データ構造を変更せずに、新しい処理(操作)を追加できるようにする。Visitor パターンは、ファイルシステムの走査、構文木の解析、ドキュメント変換など、構造が安定していて処理のバリエーションが増える場面で有効です。各要素が accept メソッドで Visitor を受け入れ、Visitor の visit メソッドが具体的な処理を行うダブルディスパッチの仕組みにより、要素の型に応じた処理を実行します。この記事では、ファイルシステム(ファイルとディレクトリ)を題材に、一覧表示・サイズ計算・拡張子カウントの3つの Visitor を実装します。sealed interface と record による要素の型安全な定義、Java 21 の switch パターンマッチングで Visitor を使わず直接型分岐する手法も確認します。

使いどころ

ファイルシステムのツリー構造を走査して、一覧表示・合計サイズ計算・特定拡張子のカウントをそれぞれ Visitor として追加する

構文木(AST)の各ノードに対してコード生成・型チェック・最適化の処理を Visitor として追加する

商品カタログのツリー構造に対して、価格集計・在庫チェック・レポート生成を Visitor で分離する

コード例

VisitorPatternDemo.java
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() + " 件");
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

sealed interface で要素を型安全に限定し、record で不変な要素(FileNode)を簡潔に定義できる。instanceof パターンマッチングでキャスト不要に。

Java 17
// 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

標準 API(interface + ダブルディスパッチ)データ構造が安定しており、処理のバリエーションが増える場面。外部依存なしで完結する。要素の種類が増えると全 Visitor に影響する。Java 21 では switch で代替できる場面もある。
Java 21 switch パターンマッチング要素の種類が少なく、処理が数行で済む場合。Visitor のダブルディスパッチを避けて簡潔に書ける。要素が sealed でない場合は使えない。走査ロジックは別途必要。
ASM / javassistJava バイトコードの走査・変換で Visitor パターンが使われている。ClassVisitor / MethodVisitor が代表例。バイトコード操作に特化しており、汎用的なデータ構造走査には使えない。

注意点

要素の種類が頻繁に追加される場合、Visitor インターフェースにメソッドを追加する必要があり、既存の全 Visitor 実装クラスに影響する。要素が安定していることが前提

ダブルディスパッチの仕組み(element.accept(visitor) → visitor.visit(element))は初見で理解しにくい。チーム内でパターンの共通認識を持っておくこと

Visitor 内で状態を持つ場合(例: depth カウント)、同じ Visitor インスタンスで複数回走査すると状態が蓄積する。走査ごとに新しいインスタンスを作るか、リセットメソッドを用意する

Java 21 の switch パターンマッチングで sealed interface の型分岐ができるため、小規模なケースでは Visitor を使わず switch で済ませる選択肢もある

FAQ

Visitor パターンと instanceof 分岐の違いは何ですか。

Visitor は accept/visit のダブルディスパッチで型に応じた処理を実行します。instanceof 分岐は呼び出し側で型を判定します。要素が sealed interface なら Java 21 の switch で網羅チェック付きの分岐も可能です。

Visitor パターンが不向きな場面はどこですか。

要素の種類が頻繁に追加される場面です。新しい要素を追加するたびに全 Visitor にメソッドを追加する必要があり、変更コストが高くなります。

Composite パターンと組み合わせることが多いのはなぜですか。

Composite はツリー構造を表現し、Visitor はそのツリーを走査して処理を追加します。ファイルシステムやDOMツリーのように、再帰構造の走査と処理の分離に自然に組み合わさります。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます