Python 訪問者パターン
訪問者パターンは、既存のクラス構造を変更することなく、それらのクラスに新しい操作を追加できるようにする、ビヘイビアデザインパターンです。簡単に言うと、訪問者パターンはアルゴリズムと、それが操作するオブジェクト構造を分離します。
コアとなる考え方
病院で健康診断を受ける場面を想像してください。あなた(被訪問者)はじっとしたまま、さまざまな医師(訪問者)があなたのところに来て検査をします。各医師は自分の専門分野に集中していますが、検査する相手は同じあなたです。
プログラミングにおいて、訪問者パターンの動作もこれに似ています。
- 訪問される要素安定して変化しない
- 訪問者新しい操作を柔軟に追加できる
- 要素は訪問者の訪問を受け入れ、訪問者に自分自身に対する操作を実行させます。
なぜ訪問者パターンが必要なのか
従来方式の限界
複数の図形を含むグラフィックシステムがあると仮定しましょう。
例
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius * self.radius
def perimeter(self):
return 2 * 3.14 * self.radius
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
問題はここからです:図形の重心の計算やSVG形式へのエクスポートなど、新しい機能を追加したい場合、各図形クラスを変更する必要があります。これは開放/閉鎖の原則(拡張に対して開かれ、修正に対して閉じている)に違反します。
訪問者パターンの利点
訪問者パターンは次の方法でこの問題を解決します。
- 新しい操作の追加が容易:既存の要素クラスを変更せずに、新しい訪問者クラスを作成するだけでよい
- 関連する操作を一元管理:関連する操作を同じ訪問者にまとめる
- 要素構造が安定:要素クラスを頻繁に変更する必要がない
訪問者パターンの実装
基本構造
具体的な例を通して訪問者パターンの実装を理解しましょう。
例
# 1. 要素インターフェースを定義
class ShapeElement(ABC):
@abstractmethod
def accept(self, visitor):
pass
# 2. 訪問者インターフェースを定義
class ShapeVisitor(ABC):
@abstractmethod
def visit_circle(self, circle):
pass
@abstractmethod
def visit_rectangle(self, rectangle):
pass
# 3. 具体的な要素クラス
class Circle(ShapeElement):
def __init__(self, radius):
self.radius = radius
def accept(self, visitor):
visitor.visit_circle(self)
class Rectangle(ShapeElement):
def __init__(self, width, height):
self.width = width
self.height = height
def accept(self, visitor):
visitor.visit_rectangle(self)
# 4. 具体的な訪問者クラス
class AreaCalculator(ShapeVisitor):
def visit_circle(self, circle):
area = 3.14 * circle.radius * circle.radius
print(f"円の面積: {area:.2f}")
return area
def visit_rectangle(self, rectangle):
area = rectangle.width * rectangle.height
print(f"長方形の面積: {area:.2f}")
return area
class PerimeterCalculator(ShapeVisitor):
def visit_circle(self, circle):
perimeter = 2 * 3.14 * circle.radius
print(f"円の周長: {perimeter:.2f}")
return perimeter
def visit_rectangle(self, rectangle):
perimeter = 2 * (rectangle.width + rectangle.height)
print(f"長方形の周長: {perimeter:.2f}")
return perimeter
使用例
例
shapes = [
Circle(5),
Rectangle(4, 6),
Circle(3)
]
# 訪問者を作成
area_calculator = AreaCalculator()
perimeter_calculator = PerimeterCalculator()
print("=== 面積の計算 ===")
for shape in shapes:
shape.accept(area_calculator)
print("\n"=== 周長の計算 ===")
for shape in shapes:
shape.accept(perimeter_calculator)
出力結果:
=== 计算面积 === 圆的面积: 78.50 矩形的面积: 24.00 圆的面积: 28.26 === 计算周长 === 圆的周长: 31.40 矩形的周长: 20.00 圆的周长: 18.84
訪問者パターンの核心コンポーネント
1. Visitor(訪問者インターフェース)
各具体的な要素クラスに対する訪問メソッドを定義します。
例
def visit_circle(self, circle): pass
def visit_rectangle(self, rectangle): pass
def visit_triangle(self, triangle): pass # 簡単に拡張可能
2. ConcreteVisitor(具体的な訪問者)
訪問者インターフェースで定義された操作を実装します。
例
def visit_circle(self, circle):
return f'<circle r="{circle.radius}" />'
def visit_rectangle(self, rectangle):
return f'<rect width="{rectangle.width}" height="{rectangle.height}" />'
3. Element(要素インターフェース)
訪問者を受け入れるメソッドを定義します。
例
@abstractmethod
def accept(self, visitor):
pass
4. ConcreteElement(具体的な要素)
要素インターフェースを実装し、acceptメソッド内で訪問者の対応するメソッドを呼び出します。
例
def accept(self, visitor):
visitor.visit_circle(self) # ここで二重ディスパッチが発生する
高度な応用例
複雑なドキュメント処理システム
より実際的な例を見てみましょう。さまざまな種類のドキュメント要素を処理します。
例
# ドキュメント要素インターフェース
class DocumentElement(ABC):
@abstractmethod
def accept(self, visitor):
pass
# 具体的なドキュメント要素
class Paragraph(DocumentElement):
def __init__(self, text):
self.text = text
def accept(self, visitor):
return visitor.visit_paragraph(self)
class Heading(DocumentElement):
def __init__(self, text, level):
self.text = text
self.level = level
def accept(self, visitor):
return visitor.visit_heading(self)
class List(DocumentElement):
def __init__(self, items):
self.items = items
def accept(self, visitor):
return visitor.visit_list(self)
# 訪問者インターフェース
class DocumentVisitor(ABC):
@abstractmethod
def visit_paragraph(self, paragraph): pass
@abstractmethod
def visit_heading(self, heading): pass
@abstractmethod
def visit_list(self, list_element): pass
# 具体的な訪問者:HTMLエクスポート
class HTMLExportVisitor(DocumentVisitor):
def visit_paragraph(self, paragraph):
return f"<p>{paragraph.text}</p>"
def visit_heading(self, heading):
return f"<h{heading.level}>{heading.text}</h{heading.level}>"
def visit_list(self, list_element):
items = "".join(f"<li>{item}</li>" for item in list_element.items)
return f"<ul>{items}</ul>"
# 具体的な訪問者:語数カウント
class WordCountVisitor(DocumentVisitor):
def __init__(self):
self.total_words = 0
def visit_paragraph(self, paragraph):
words = len(paragraph.text.split())
self.total_words += words
return words
def visit_heading(self, heading):
words = len(heading.text.split())
self.total_words += words
return words
def visit_list(self, list_element):
words = sum(len(item.split()) for item in list_element.items)
self.total_words += words
return words
# 使用例
document = [
Heading("Python チュートリアル", 1),
Paragraph("Python は強力なプログラミング言語です。"),
List(["リスト項目1", "リスト項目2", "リスト項目3"]),
Paragraph("Pythonを学ぶのはとても面白いです。")
]
# HTMLエクスポート
html_visitor = HTMLExportVisitor()
print("=== HTML エクスポート ===")
for element in document:
print(element.accept(html_visitor))
# 語数カウント
word_visitor = WordCountVisitor()
print("\n"=== 語数カウント ===")
for element in document:
words = element.accept(word_visitor)
print(f"{element.__class__.__name__}: {words} 語")
print(f"総語数: {word_visitor.total_words}")
訪問者パターンの長所と短所
長所
- 開放/閉鎖の原則:既存のクラスを変更せずに新しい操作を追加しやすい
- 単一責任の原則:関連する振る舞いを1つの訪問者クラスに集約する
- 柔軟性:実行時に異なる訪問者を選択できる
- データの分離:アルゴリズムとデータ構造を分離する
短所
- 要素インターフェースの変更が困難:新しい要素クラスを追加するにはすべての訪問者を変更する必要がある
- カプセル化を壊す可能性がある:訪問者が要素のプライベートメンバーにアクセスする必要がある場合がある
- 複雑さ:単純なデータ構造には複雑すぎる場合がある
適用シナリオ
訪問者パターンが適しているシナリオ
- オブジェクト構造が安定している:比較的安定したオブジェクト構造上で複数の操作を定義する必要がある
- 操作が頻繁に変わる:新しい操作やアルゴリズムを頻繁に追加する必要がある
- 関連する操作をまとめたい:関連する操作を一緒にまとめたい場合
- 複雑なオブジェクト構造:オブジェクト構造にさまざまな種類のオブジェクトが多数含まれる
実際の応用事例
- コンパイラ:構文木上のさまざまな分析(型チェック、コード最適化など)
- ドキュメント処理:ドキュメント要素に対して異なる処理(エクスポート、統計、フォーマットなど)を行う
- GUIシステム:UIコンポーネントに対して異なる操作(レンダリング、イベント処理など)を行う
- ゲーム開発:ゲームオブジェクトに対して異なる処理(衝突判定、AIなど)を行う
ベストプラクティスと注意点
1. 二重ディスパッチを使用する
訪問者パターンの核心は二重ディスパッチ(double dispatch)です。
- 最初のディスパッチ:要素が訪問者を受け入れる
- 2番目のディスパッチ:訪問者が具体的な要素を訪問する
例
def accept(self, visitor):
visitor.visit_circle(self) # ここで具体的な訪問メソッドが決定される
2. 訪問者の状態を処理する
訪問者が状態を保持する必要がある場合:
例
def __init__(self):
self.total_area = 0
self.shape_count = 0
def visit_circle(self, circle):
area = 3.14 * circle.radius * circle.radius
self.total_area += area
self.shape_count += 1
def visit_rectangle(self, rectangle):
area = rectangle.width * rectangle.height
self.total_area += area
self.shape_count += 1
def get_statistics(self):
return {
'total_area': self.total_area,
'shape_count': self.shape_count,
'average_area': self.total_area / self.shape_count if self.shape_count > 0 else 0
}
3. 訪問時の例外を処理する
例
def visit_circle(self, circle):
try:
# 操作を実行
pass
except Exception as e:
print(f"円の処理中にエラー: {e}")
def visit_rectangle(self, rectangle):
try:
# 操作を実行
pass
except Exception as e:
print(f"長方形の処理中にエラー: {e}")
まとめ
訪問者パターンは強力なデザインパターンであり、アルゴリズムとデータ構造を分離することで優れた拡張性を提供します。ある程度の複雑さはありますが、適切なシナリオであればコードの保守性と拡張性を大幅に向上させることができます。
重要なポイント:
- ビジターパターンは、オブジェクト構造が安定しているが操作が頻繁に変化するシナリオに適しています
- 二重ディスパッチメカニズムを通じてポリモーフィックな動作を実現します
- 新しい操作の追加は容易ですが、新しい要素タイプの追加は困難です
- 実際のプロジェクトでは、その複雑さと得られる利点を比較検討する必要があります