1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonで実現するVisitorパターン:構造に依存しない振る舞いの追加

Posted at

概要

Visitorパターンは、
オブジェクトの構造に変更を加えずに、外部から新たな振る舞い(処理)を追加するための設計パターンである。

従来、各クラスにロジックを追加しようとすると、構造そのものを汚染してしまう。
Visitorパターンはこの問題を解消し、構造と振る舞いを分離した拡張性の高いアーキテクチャを構築する。


1. なぜVisitorが必要か?

❌ 構造クラスに処理が混在しがち

class Circle:
    def draw(self): ...
    def export_json(self): ...
    def export_svg(self): ...

→ クラス本来の責務(図形の定義)にフォーマット処理などの副次的責務が混在
SRP(単一責任原則)違反


✅ Visitorで「処理を別のオブジェクト」に分離

circle.accept(JsonExporter())
circle.accept(SvgExporter())

構造(図形)に手を加えず、処理(エクスポート)を後から追加可能


2. 基本構造

✅ Elementインタフェース

class Shape:
    def accept(self, visitor):
        visitor.visit(self)

✅ 具象Elementクラス

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

class Square(Shape):
    def __init__(self, length):
        self.length = length

✅ Visitorインタフェースと実装

class Visitor:
    def visit(self, shape):
        raise NotImplementedError

class JsonExporter(Visitor):
    def visit(self, shape):
        if isinstance(shape, Circle):
            print({"type": "circle", "radius": shape.radius})
        elif isinstance(shape, Square):
            print({"type": "square", "length": shape.length})

class SvgExporter(Visitor):
    def visit(self, shape):
        if isinstance(shape, Circle):
            print(f"<circle r='{shape.radius}' />")
        elif isinstance(shape, Square):
            print(f"<rect width='{shape.length}' height='{shape.length}' />")

✅ 実行

shapes = [Circle(5), Square(3)]
exporter = SvgExporter()

for shape in shapes:
    shape.accept(exporter)

3. Python的な工夫:ダブルディスパッチ風の拡張

Pythonは静的型がないため、visit() を型ごとに分離しにくいが、以下のように改善可能。

class Visitor:
    def visit(self, shape):
        method_name = f'visit_{shape.__class__.__name__.lower()}'
        visitor = getattr(self, method_name, self.generic_visit)
        return visitor(shape)

    def generic_visit(self, shape):
        raise NotImplementedError
class PrettyPrinter(Visitor):
    def visit_circle(self, shape):
        print(f"Circle with radius {shape.radius}")

    def visit_square(self, shape):
        print(f"Square with side {shape.length}")

各型に対応したvisitメソッドを自然に分離・拡張可能


4. 実務ユースケース

✅ 抽象構文木(AST)の走査と解析

  • ノード(構文)を変更せず、型ごとの処理(コード生成、静的解析、トランスパイル)を追加可能

✅ 複雑なデータ構造のフォーマット変換

  • データ構造の定義を保ったまま、JSON, XML, YAML, HTML など任意の出力を実装

✅ 設計モデル上の振る舞い追加(図形、UIコンポーネント、ファイル構造)

  • validate, clone, draw, calculate_area などの振る舞いを Visitor として分離

5. よくある誤用と対策

if isinstance() で分岐が膨張

→ ✅ ダブルディスパッチ形式の visit_xxx に分離してスケーラブルに


❌ Visitorの処理が肥大化してSRPを侵害

→ ✅ Visitorを複数に分ける(ExporterVisitor, ValidatorVisitor など)ことで責務を分散


❌ Visitor実装がElementに強く依存しすぎる

→ ✅ Visitor構造の使用は最小限にとどめ、意図のみに集中


結語

Visitorパターンとは、“構造を保ちつつ、振る舞いを柔軟に外部から追加する”ための構造的手法である。

  • オープンクローズドの原則を保ったまま、責務の分離と後方互換性の確保が可能
  • 特に「構造は変えたくないが処理は増やしたい」場面で絶大な威力
  • Pythonでも工夫次第で自然かつパワフルに適用可能

Pythonicとは、“拡張性を構造で保証する”ことであり、
Visitorパターンはその保証を設計という文脈で強固に担保する手段である。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?