はじめに
本記事は、GoFデザインパターン解説シリーズの1つです。
今回でいよいよGoFの23パターン最後、Visitor(ビジター)パターンを解説します。
「データ構造」と「データに対する処理」を分離するという、少しトリッキーですが強力な考え方を学びます。
解決したい課題
複数の種類のクラス(例えば図形のCircle・Rectangle・Triangle)からなるデータ構造に対して、新しい種類の処理(操作)を追加したいが、各クラスのコード自体は変更したくない(できない)という場面があります。
素朴な方法として、各クラスに直接メソッドを追加する(CircleにcalculateArea()、exportToJson()、renderToScreen()...とメソッドを増やしていく)実装をすると、新しい操作を追加するたびに、全種類のクラスに同じような追加が必要になり、クラス自体がどんどん肥大化していきます。
また、サードパーティ製のライブラリが提供するクラスなど、そもそも自分ではコードを編集できないクラスに対して、新しい操作を追加すること自体ができません。
Visitorパターンは、「処理(操作)」を、データ構造のクラス自体からは切り離し、外部の「訪問者(Visitor)」オブジェクトとして表現することで、この問題を解決します。
新しい操作を追加したい場合は、新しいVisitorクラスを1つ追加するだけで済み、既存のデータ構造クラスのコードは変更不要になります。
クラス構成
- Element(要素): Visitorを受け入れるメソッド(
accept())を持つ、共通インターフェース - ConcreteElement(具体的な要素): 実際のデータ構造を表す、具体的なクラス群
- Visitor(ビジター): 各ConcreteElementに対応する処理メソッドを持つ、共通インターフェース
- ConcreteVisitor(具体的なビジター): 実際の処理内容を実装する、具体的なVisitorクラス
Javaでの実装例
図形(Circle・Rectangle)に対して、「面積計算」「JSON出力」という2種類の操作をVisitorとして実装してみます。
Element:図形共通のインターフェース
public interface Shape {
// 「自分自身をVisitorに渡す」というのが特徴的なポイント(ダブルディスパッチ)
void accept(ShapeVisitor visitor);
}
ConcreteElement:具体的な図形クラス
public class Circle implements Shape {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
public double getRadius() {
return radius;
}
@Override
public void accept(ShapeVisitor visitor) {
visitor.visit(this); // 自分の具体的な型(Circle)をVisitorに渡す
}
}
public class Rectangle implements Shape {
private final double width;
private final double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double getWidth() {
return width;
}
public double getHeight() {
return height;
}
@Override
public void accept(ShapeVisitor visitor) {
visitor.visit(this);
}
}
Visitor:共通インターフェース
public interface ShapeVisitor {
void visit(Circle circle);
void visit(Rectangle rectangle);
}
ConcreteVisitor:具体的な処理群
public class AreaCalculatorVisitor implements ShapeVisitor {
private double totalArea = 0;
@Override
public void visit(Circle circle) {
double area = Math.PI * circle.getRadius() * circle.getRadius();
System.out.println("円の面積: " + area);
totalArea += area;
}
@Override
public void visit(Rectangle rectangle) {
double area = rectangle.getWidth() * rectangle.getHeight();
System.out.println("四角形の面積: " + area);
totalArea += area;
}
public double getTotalArea() {
return totalArea;
}
}
public class JsonExportVisitor implements ShapeVisitor {
@Override
public void visit(Circle circle) {
System.out.println("{\"type\": \"circle\", \"radius\": " + circle.getRadius() + "}");
}
@Override
public void visit(Rectangle rectangle) {
System.out.println("{\"type\": \"rectangle\", \"width\": " + rectangle.getWidth()
+ ", \"height\": " + rectangle.getHeight() + "}");
}
}
利用側のコード
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Shape> shapes = List.of(
new Circle(5.0),
new Rectangle(4.0, 6.0)
);
System.out.println("--- 面積計算 ---");
AreaCalculatorVisitor areaVisitor = new AreaCalculatorVisitor();
for (Shape shape : shapes) {
shape.accept(areaVisitor); // 図形側は「自分をvisitorに渡す」だけ
}
System.out.println("合計面積: " + areaVisitor.getTotalArea());
System.out.println();
System.out.println("--- JSON出力 ---");
JsonExportVisitor jsonVisitor = new JsonExportVisitor();
for (Shape shape : shapes) {
shape.accept(jsonVisitor);
}
}
}
実行結果:
--- 面積計算 ---
円の面積: 78.53981633974483
四角形の面積: 24.0
合計面積: 102.53981633974483
--- JSON出力 ---
{"type": "circle", "radius": 5.0}
{"type": "rectangle", "width": 4.0, "height": 6.0}
Circle・Rectangleクラスには、「面積を計算する」「JSONに変換する」といった処理のロジックは一切書かれていません。
それぞれの処理はAreaCalculatorVisitor・JsonExportVisitorという、独立したVisitorクラスに実装されています。
新しい操作(例えば「SVGとして描画する」)を追加したい場合も、新しいShapeVisitor実装クラスを1つ追加するだけでよく、Circle・Rectangleのコードは一切変更する必要がありません。
なぜaccept(visitor)が必要なのか(ダブルディスパッチ)
「visitor.visit(shape)のように、直接Visitor側から図形を渡せばよいのでは?」と思うかもしれません。
しかし、shapesリストの要素の型はShape(インターフェース)であり、Javaのメソッドオーバーロードの解決はコンパイル時の静的型に基づいて行われるため、visitor.visit(shape)と書いてしまうと、実際の中身がCircleでもRectangleでも、コンパイル時の型Shapeに対応するオーバーロードを解決できず、意図通りに動きません。
そこで、各ConcreteElement自身にaccept(visitor)を実装させ、その中でvisitor.visit(this)と自分自身の具体的な型で呼び出すことで、正しいオーバーロードが選ばれるようにしています。
この「2段階の呼び出し」の仕組みはダブルディスパッチと呼ばれ、Visitorパターンの核心的なテクニックです。
使用場面
- コンパイラ・インタープリターにおける、構文木(AST)に対する複数の処理(型チェック、コード生成、最適化など)
- 複数の種類の要素からなるデータ構造に対して、頻繁に新しい種類の「処理」を追加する必要があるが、要素の種類(クラス)自体はあまり増えない場合
- 既存のクラス群(変更できないライブラリのクラスなど)に、後から新しい操作を追加したい場合
注意点
Visitorパターンは、要素の種類(ConcreteElement)が増えるたびに、すべてのVisitor実装に新しいメソッドを追加しなければならないという、Strategyパターンなどとは逆方向のトレードオフを持ちます。
「処理の種類は増えるが、データ構造の種類はあまり増えない」場合に向いているパターンであり、逆に「データ構造の種類が頻繁に増える」場合には不向きです。
また、ダブルディスパッチの仕組みはやや理解しづらく、GoFの23パターンの中でも学習コストが高いパターンの1つとされています。
まとめ
| 項目 | 内容 |
|---|---|
| 解決する課題 | 既存のデータ構造クラス群に対して、新しい処理を、クラス自体を変更せずに追加したい |
| 実現方法 | 処理をVisitorオブジェクトとして切り出し、各要素にaccept()経由でダブルディスパッチさせる |
| メリット | 新しい操作の追加が、新しいVisitorクラスの追加だけで完結する |
| 注意点 | 要素の種類が増えるたびに、全Visitor実装の修正が必要になる(トレードオフの向きがStrategy等と逆) |
おすすめ書籍
GoFの23パターンをより深く学びたい方には、以下の書籍がおすすめです。
Java言語のサンプルコードとUMLを使い、初心者にもわかりやすく解説されています。
Java言語で学ぶデザインパターン入門 第3版 [ 結城 浩 ]
※本リンクはアフィリエイトリンクを含みます。


