はじめに
皆さん、こんにちは!「JavaとPythonで比べるデザインパターン」シリーズの第25回目です。
今回は、振る舞いパターンの最終章として、オブジェクト構造から操作(アルゴリズム)を分離し、既存のクラス階層を変更することなく新しい操作を追加できるVisitor(ビジター)パターンについて解説します。
Visitorパターンとは?
Visitorパターンは、要素(Element)を表現するクラス階層と、それらの要素に対して実行される操作(Visitor)を分離する振る舞いパターンです。このパターンの最大の特徴は、新しい操作を追加する際に既存の要素クラスを一切変更する必要がないことです。
身近な例で理解する
税務調査官が各家庭を訪問する様子を想像してください。
- 各家庭(要素):一人暮らし、夫婦世帯、子育て世帯など
- 税務調査官(ビジター):所得税調査官、消費税調査官、資産税調査官など
各調査官は、家庭の構成に応じて異なる調査を行います:
- 所得税調査官は、働いている人の収入を調査
- 消費税調査官は、家計の支出を調査
- 資産税調査官は、不動産や金融資産を調査
新しい税制(例:デジタル税)が導入されても、各家庭の構造は変わりません。新しいデジタル税調査官(新しいビジター)を派遣するだけで対応できます。
パターンの目的とメリット
1. オープン・クローズド原則の実現
- 新しい操作の追加:開いている(容易)
- 既存のコード変更:閉じている(不要)
2. 単一責任原則の維持
- 要素クラス:データ構造の責任
- ビジタークラス:操作の責任
3. 関連する操作の集約
複数の要素に対する関連操作を一つのビジタークラスにまとめる
Javaでの実装:ダブルディスパッチによる型安全性
Javaではダブルディスパッチ(メソッド呼び出しが2つのオブジェクトの型に依存)を利用して、型安全なVisitorパターンを実装できます。
基本構造とコンポーネント
import java.util.*;
// 1. ビジターインターフェース
interface ShapeVisitor {
void visit(Circle circle);
void visit(Rectangle rectangle);
void visit(Triangle triangle);
}
// 2. 要素インターフェース
interface Shape {
void accept(ShapeVisitor visitor);
}
// 3. 具象要素クラス群
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); // ダブルディスパッチの1段階目
}
}
class Rectangle implements Shape {
private final double width, 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);
}
}
class Triangle implements Shape {
private final double base, height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
public double getBase() { return base; }
public double getHeight() { return height; }
@Override
public void accept(ShapeVisitor visitor) {
visitor.visit(this);
}
}
// 4. 具象ビジター:面積計算
class AreaCalculatorVisitor implements ShapeVisitor {
private double totalArea = 0.0;
@Override
public void visit(Circle circle) {
double area = Math.PI * circle.getRadius() * circle.getRadius();
System.out.printf("円の面積: %.2f%n", area);
totalArea += area;
}
@Override
public void visit(Rectangle rectangle) {
double area = rectangle.getWidth() * rectangle.getHeight();
System.out.printf("長方形の面積: %.2f%n", area);
totalArea += area;
}
@Override
public void visit(Triangle triangle) {
double area = 0.5 * triangle.getBase() * triangle.getHeight();
System.out.printf("三角形の面積: %.2f%n", area);
totalArea += area;
}
public double getTotalArea() {
return totalArea;
}
}
// 4. 具象ビジター:描画処理
class DrawVisitor implements ShapeVisitor {
private final String context;
public DrawVisitor(String context) {
this.context = context;
}
@Override
public void visit(Circle circle) {
System.out.printf("[%s] 半径%.1fの円を描画%n",
context, circle.getRadius());
}
@Override
public void visit(Rectangle rectangle) {
System.out.printf("[%s] %.1f×%.1fの長方形を描画%n",
context, rectangle.getWidth(), rectangle.getHeight());
}
@Override
public void visit(Triangle triangle) {
System.out.printf("[%s] 底辺%.1f、高さ%.1fの三角形を描画%n",
context, triangle.getBase(), triangle.getHeight());
}
}
// 4. 具象ビジター:XML出力
class XmlExportVisitor implements ShapeVisitor {
private final StringBuilder xml = new StringBuilder();
public XmlExportVisitor() {
xml.append("<shapes>\n");
}
@Override
public void visit(Circle circle) {
xml.append(String.format(" <circle radius=\"%.2f\" />\n",
circle.getRadius()));
}
@Override
public void visit(Rectangle rectangle) {
xml.append(String.format(" <rectangle width=\"%.2f\" height=\"%.2f\" />\n",
rectangle.getWidth(), rectangle.getHeight()));
}
@Override
public void visit(Triangle triangle) {
xml.append(String.format(" <triangle base=\"%.2f\" height=\"%.2f\" />\n",
triangle.getBase(), triangle.getHeight()));
}
public String getXml() {
return xml.toString() + "</shapes>";
}
}
// 使用例
public class VisitorPatternDemo {
public static void main(String[] args) {
// 図形のコレクション
List<Shape> shapes = Arrays.asList(
new Circle(5.0),
new Rectangle(4.0, 6.0),
new Triangle(3.0, 8.0)
);
System.out.println("=== 面積計算 ===");
AreaCalculatorVisitor areaVisitor = new AreaCalculatorVisitor();
shapes.forEach(shape -> shape.accept(areaVisitor));
System.out.printf("総面積: %.2f%n%n", areaVisitor.getTotalArea());
System.out.println("=== 描画処理 ===");
DrawVisitor drawVisitor = new DrawVisitor("Canvas");
shapes.forEach(shape -> shape.accept(drawVisitor));
System.out.println("\n=== XML出力 ===");
XmlExportVisitor xmlVisitor = new XmlExportVisitor();
shapes.forEach(shape -> shape.accept(xmlVisitor));
System.out.println(xmlVisitor.getXml());
}
}
Javaの特徴とメリット
- コンパイル時型チェック:visitメソッドの対応漏れを防止
- メソッドオーバーロード:同じメソッド名で異なる型を処理
- IDE支援:新しい要素を追加した時の実装漏れを検出
- パフォーマンス:実行時の型判定が不要
Pythonでの実装:動的ディスパッチと柔軟性
Pythonでは動的な特性を活かして、より柔軟なVisitorパターンを実装できます。
from abc import ABC, abstractmethod
import math
from typing import List, Union
# 1. ビジター基底クラス
class ShapeVisitor(ABC):
def visit(self, element):
"""動的ディスパッチを実装"""
method_name = f'visit_{element.__class__.__name__}'
visitor_method = getattr(self, method_name, self.generic_visit)
return visitor_method(element)
def generic_visit(self, element):
"""未知の要素に対するデフォルト処理"""
print(f"警告: {element.__class__.__name__} の処理が未実装")
# 2. 要素基底クラス
class Shape(ABC):
def accept(self, visitor: ShapeVisitor):
return visitor.visit(self)
# 3. 具象要素クラス群
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def __str__(self):
return f"Circle(radius={self.radius})"
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def __str__(self):
return f"Rectangle(width={self.width}, height={self.height})"
class Triangle(Shape):
def __init__(self, base: float, height: float):
self.base = base
self.height = height
def __str__(self):
return f"Triangle(base={self.base}, height={self.height})"
# 新しい要素を簡単に追加可能
class Pentagon(Shape):
def __init__(self, side: float):
self.side = side
def __str__(self):
return f"Pentagon(side={self.side})"
# 4. 具象ビジター:面積計算
class AreaCalculatorVisitor(ShapeVisitor):
def __init__(self):
self.total_area = 0.0
def visit_Circle(self, circle: Circle) -> float:
area = math.pi * circle.radius ** 2
print(f"円の面積: {area:.2f}")
self.total_area += area
return area
def visit_Rectangle(self, rectangle: Rectangle) -> float:
area = rectangle.width * rectangle.height
print(f"長方形の面積: {area:.2f}")
self.total_area += area
return area
def visit_Triangle(self, triangle: Triangle) -> float:
area = 0.5 * triangle.base * triangle.height
print(f"三角形の面積: {area:.2f}")
self.total_area += area
return area
def visit_Pentagon(self, pentagon: Pentagon) -> float:
# 正五角形の面積計算
area = (1/4) * math.sqrt(25 + 10*math.sqrt(5)) * pentagon.side ** 2
print(f"正五角形の面積: {area:.2f}")
self.total_area += area
return area
# 4. 具象ビジター:描画処理
class DrawVisitor(ShapeVisitor):
def __init__(self, context: str = "Screen"):
self.context = context
def visit_Circle(self, circle: Circle):
print(f"[{self.context}] 半径{circle.radius}の円を描画")
def visit_Rectangle(self, rectangle: Rectangle):
print(f"[{self.context}] {rectangle.width}×{rectangle.height}の長方形を描画")
def visit_Triangle(self, triangle: Triangle):
print(f"[{self.context}] 底辺{triangle.base}、高さ{triangle.height}の三角形を描画")
def visit_Pentagon(self, pentagon: Pentagon):
print(f"[{self.context}] 一辺{pentagon.side}の正五角形を描画")
# 4. 具象ビジター:JSON出力
class JsonExportVisitor(ShapeVisitor):
def __init__(self):
self.shapes_data = []
def visit_Circle(self, circle: Circle):
data = {"type": "circle", "radius": circle.radius}
self.shapes_data.append(data)
return data
def visit_Rectangle(self, rectangle: Rectangle):
data = {"type": "rectangle", "width": rectangle.width, "height": rectangle.height}
self.shapes_data.append(data)
return data
def visit_Triangle(self, triangle: Triangle):
data = {"type": "triangle", "base": triangle.base, "height": triangle.height}
self.shapes_data.append(data)
return data
def visit_Pentagon(self, pentagon: Pentagon):
data = {"type": "pentagon", "side": pentagon.side}
self.shapes_data.append(data)
return data
def get_json(self) -> dict:
return {"shapes": self.shapes_data}
# 関数型アプローチの例
def create_shape_processor(operations: dict):
"""関数ベースのビジターパターン"""
def process_shape(shape: Shape):
shape_type = shape.__class__.__name__
if shape_type in operations:
return operations[shape_type](shape)
else:
print(f"警告: {shape_type} の処理が未定義")
return process_shape
# 使用例
if __name__ == "__main__":
import json
# 図形のコレクション(新しい要素も含む)
shapes: List[Shape] = [
Circle(5.0),
Rectangle(4.0, 6.0),
Triangle(3.0, 8.0),
Pentagon(2.0) # 新しい要素
]
print("=== 面積計算 ===")
area_visitor = AreaCalculatorVisitor()
for shape in shapes:
shape.accept(area_visitor)
print(f"総面積: {area_visitor.total_area:.2f}\n")
print("=== 描画処理 ===")
draw_visitor = DrawVisitor("Canvas")
for shape in shapes:
shape.accept(draw_visitor)
print("\n=== JSON出力 ===")
json_visitor = JsonExportVisitor()
for shape in shapes:
shape.accept(json_visitor)
print(json.dumps(json_visitor.get_json(), indent=2))
print("\n=== 関数型アプローチ ===")
# 関数ベースの処理定義
shape_operations = {
'Circle': lambda c: f"🔵 円(半径: {c.radius})",
'Rectangle': lambda r: f"🟨 長方形({r.width}×{r.height})",
'Triangle': lambda t: f"🔺 三角形(底辺: {t.base}, 高さ: {t.height})",
'Pentagon': lambda p: f"⭐ 正五角形(一辺: {p.side})"
}
processor = create_shape_processor(shape_operations)
for shape in shapes:
result = processor(shape)
if result:
print(result)
Pythonの特徴とメリット
-
動的ディスパッチ:
getattrを使った柔軟なメソッド解決 - 簡単な拡張:新しい要素や操作の追加が容易
- 関数型アプローチ:辞書と関数を使った代替実装
- 実行時柔軟性:未知の要素に対する graceful degradation
実践的な応用例
コンパイラでの活用
// 抽象構文木(AST)の処理
interface ASTVisitor {
void visit(VariableNode node);
void visit(FunctionCallNode node);
void visit(BinaryOperatorNode node);
}
class CodeGeneratorVisitor implements ASTVisitor {
// 各ノードに対してコード生成
}
class TypeCheckerVisitor implements ASTVisitor {
// 各ノードで型チェック
}
class OptimizationVisitor implements ASTVisitor {
// 各ノードで最適化
}
ビジネスルールエンジン
class BusinessRuleVisitor:
def visit_Customer(self, customer):
# 顧客に対するビジネスルール
pass
def visit_Order(self, order):
# 注文に対するビジネスルール
pass
def visit_Product(self, product):
# 商品に対するビジネスルール
pass
パターンの適用指針
✅ 適している場面
1. 安定した要素階層 + 頻繁な操作追加
- 要素の種類は固定的
- 新しい操作を頻繁に追加する必要がある
2. 複雑なオブジェクト構造の処理
- AST、ファイルシステム、グラフ構造など
- 要素間の関係が重要
3. 異なる種類の操作の分離
- 表示、保存、変換、検証など
- 各操作を独立して開発・テスト
❌ 避けるべき場面
1. 要素階層が頻繁に変更される
- 新しい要素を追加するたびに全ビジターを更新
2. 操作が少ない場合
- シンプルな継承やストラテジーパターンで十分
3. 要素間の状態共有が必要
- ビジターは基本的にステートレスであるべき
まとめ:アルゴリズムの外部化による拡張性
| 観点 | Java | Python |
|---|---|---|
| 型安全性 | コンパイル時チェックで完全保証 | 実行時チェック + 型ヒント |
| 拡張性 | 新ビジター追加は容易、新要素は全ビジター更新必要 | 動的ディスパッチで柔軟な拡張 |
| パフォーマンス | 静的ディスパッチで高速 | 動的解決でややオーバーヘッド |
| 開発生産性 | IDE支援で実装漏れを防止 | 実行時エラーで問題発見 |
Visitorパターンの本質
Visitorパターンは**「操作をオブジェクト構造から分離し、新しい操作を既存コードを変更せずに追加する」**ことを可能にする強力な設計パターンです。
- Java:厳密な型システムと静的ディスパッチによる安全な実装
- Python:動的な特性を活かした柔軟で実用的な実装
両言語とも、複雑なオブジェクト構造に対して多様な操作を適用する際に、その威力を発揮します。
振る舞いパターンの集大成として
これで振る舞いパターンの学習は完了です!🎊
これまでに学んだ振る舞いパターンを振り返ってみましょう:
- Observer:オブジェクト間の依存関係を疎結合で管理
- Strategy:アルゴリズムの切り替えを動的に実現
- Command:リクエストをオブジェクト化して柔軟な処理
- Template Method:アルゴリズムの骨格を定義し、詳細をサブクラスに委譲
- Visitor:操作をオブジェクト構造から分離し、新しい操作を安全に追加
Visitorパターンは振る舞いパターンの中でも最も高度で複雑なパターンの一つです。その分、適切に使用した時の効果も絶大です。
振る舞いパターンはここで一区切りですが、まだまだ学ぶべきパターンが残っています!次回は、リクエストの処理を複数のハンドラに連鎖させて処理する、実用的で興味深いパターンを学びましょう。
素晴らしい設計への道のりは続きます! ✨
次回予告:「Day 26 チェーン・オブ・レスポンシビリティ:リクエストの処理を連鎖させる」