オブジェクト構造に操作を追加するためのビジターパターン

ビジターパターンとは

ビジターパターン(Visitor Pattern)は、データ構造とその構造に含まれる要素に対する操作を分離するためのデザインパターンです。このパターンにより、オブジェクト構造を変更せずに新しい操作を追加することが可能になります。

つまり、既存のクラス階層を変更することなく、それらのクラスに対して新たな振る舞いを追加できる仕組みを提供します。これは特に、構造が安定しており頻繁に変更されない一方で、さまざまな処理をその構造に対して行いたい場合に有効です。

適用されるケース

  • 複数の異なる型のオブジェクトから成る構造があり、各オブジェクトの型に応じた特定の処理を行いたい場合。
  • オブジェクトのクラスに多数の無関係な処理を実装するとクラスが肥大化してしまうため、処理を外部に切り出したい場合。
  • 将来的に新しい操作を追加する頻度が高いが、データ構造自体の変更はほとんどない場合。

逆に、要素となるクラスが頻繁に追加・変更される場合は、すべてのビジターにメソッドを追加する必要が生じるため、このパターンは向いていません。

主要コンポーネントと実装

1. ビジターインターフェース

訪問される各要素に対して、対応するvisitメソッドを宣言します。

interface IElementVisitor
{
    void Visit(ElementA element);
    void Visit(ElementB element);
}

2. 具体的なビジター

実際に処理を行う具体的なビジタークラスです。例えば、ログ出力やデータ集計などの異なる用途ごとに実装を分けられます。

class LoggingVisitor : IElementVisitor
{
    public void Visit(ElementA element)
    {
        Console.WriteLine($"[Log] ElementA の処理中: {element.Id}");
    }

    public void Visit(ElementB element)
    {
        Console.WriteLine($"[Log] ElementB の処理中: {element.Name}");
    }
}

class DataProcessingVisitor : IElementVisitor
{
    public void Visit(ElementA element)
    {
        // 特定のビジネスロジックを実装
        Console.WriteLine($"処理中: ElementA (ID={element.Id})");
    }

    public void Visit(ElementB element)
    {
        // 別の処理
        Console.WriteLine($"処理中: ElementB (Name={element.Name})");
    }
}

3. 要素の基本インタフェース

すべての要素が共通して持つインターフェース。accept メソッドを通じてビジターを受け入れます。

interface IElement
{
    void Accept(IElementVisitor visitor);
}

4. 具体的な要素クラス

個々のオブジェクトが accept メソッドを実装し、自身をビジターに渡します。この際、動的ディスパッチによる「二重ディスパッチ」が利用されます。

class ElementA : IElement
{
    public int Id { get; set; } = 1;

    public void Accept(IElementVisitor visitor)
    {
        visitor.Visit(this); // 型に基づき適切なVisitが呼び出される
    }
}

class ElementB : IElement
{
    public string Name { get; set; } = "Sample";

    public void Accept(IElementVisitor visitor)
    {
        visitor.Visit(this);
    }
}

5. オブジェクト構造を管理するコンテナ

複数の要素を保持し、一括でビジターを適用できるようにするクラスです。

class CompositeStructure
{
    private readonly List<IElement> _elements = new();

    public void AddElement(IElement element)
    {
        _elements.Add(element);
    }

    public void RemoveElement(IElement element)
    {
        _elements.Remove(element);
    }

    public void ApplyVisitor(IElementVisitor visitor)
    {
        foreach (var element in _elements)
        {
            element.Accept(visitor);
        }
    }
}

6. クライアントでの使用例

static void Main()
{
    var structure = new CompositeStructure();
    structure.AddElement(new ElementA());
    structure.AddElement(new ElementB());

    var logger = new LoggingVisitor();
    var processor = new DataProcessingVisitor();

    structure.ApplyVisitor(logger);      // ログ出力
    structure.ApplyVisitor(processor);   // 処理実行
}

利点と注意点

  • 拡張性: 新しい操作を追加しても、既存の要素クラスを変更する必要がない。
  • 関心の分離: 操作のロジックがビジタークラスに集中するため、可読性と保守性が向上する。
  • 二重ディスパッチ: 実行時型に基づいて適切なvisitメソッドが呼び出される。
  • メンテナンスコスト: 要素クラスが増えた場合、すべてのビジターに新しいvisitメソッドを追加する必要がある。

したがって、要素の種類がほぼ固定されているシステムにおいて最も効果を発揮します。

タグ: デザインパターン C# ビジターパターン 二重ディスパッチ オブジェクト指向

5月21日 08:42 投稿