はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【C#】データ構造と処理を分離するVisitorパターンを学ぶ

はじめに

今回はデータ構造と処理を分離する目的で用いられるデザインパターンVisitorパターンを紹介したいと思います。

f:id:hanaaaaaachiru:20210210143434p:plain
クラス図

登場人物

大切なことなので何度も言いますが、Visitorパターンはデータ構造と処理を分けることが本質です。

f:id:hanaaaaaachiru:20210218194428p:plain
イメージ

ですので登場人物にも、データ側処理の記述側の2つを意識しながら見ていただけると理解しやすいと思います。

データ構造側

名前 意味
IElement Visitor(訪問者)を受け入れることを保証するインターフェイス
ConcreteElement データ構造の具象クラス
f:id:hanaaaaaachiru:20210209203013p:plain
データ構造

処理の記述側

名前 意味
Visitor 処理を記述する抽象クラス
ConcreteVisitor 処理を記述する具象クラス
f:id:hanaaaaaachiru:20210209203037p:plain
訪問者側

データ構造側のコード

IElementインターフェイスは、Visitor(訪問者)を受け入れることを保証します。

/// <summary>
/// 訪問者を受け入れることを保証するインターフェイス
/// </summary>
public interface IElement
{
    public abstract void Accept(Visitor visitor);
}


ConcreteElementクラスは、データ構造を記述するクラスです。ちなみにConcrete○○は具体的な○○という意味で、別に一つのクラスである必要はありません。

/// <summary>
/// データ構造
/// </summary>
public class ConcreteElement : IElement
{
    public void Accept(Visitor visitor)
    {
        visitor.Visit(this);
    }
}

処理を記述するコード

Visitorクラスは、訪問者を表す抽象クラスです。引数は訪問先のデータ構造にしている箇所がキモで、オーバーロードを複数定義しておくことで型によって処理を変えることができます。

/// <summary>
/// 訪問者
/// </summary>
public abstract class Visitor
{
    public abstract void Visit(ConcreteElement element);
}


ConcreteVisitorクラスは、訪問者の具象クラスでデータ構造に対して行う処理を記述します。

/// <summary>
/// データへの処理をこのクラスに書く
/// </summary>
public class ConcreteVisitor : Visitor
{
    public override void Visit(ConcreteElement element)
    {
        Console.WriteLine("Visit " + element.GetType().Name);
    }
}

テスト

class Client
{
    static void Main(string[] args)
    {
        // データ構造
        var element = new ConcreteElement();
            
        // データ構造に対して処理を行う訪問者
        var visitor = new ConcreteVisitor();
            
        // データに対して処理を行う
        // 「Visit ConcreteElement」
        element.Accept(visitor);
    }
}

データの集合を扱うクラス

Visitorパターンはこれまで紹介してきたデータ構造と処理を分ける仕組みが本質ですが、データ(ConcreteElement)の集合クラス(ObjectStructure)を定義することも多いです。

/// <summary>
/// データの集合を扱うクラス
/// </summary>
public class ObjectStructure : IElement, IEnumerable<IElement>
{
    private readonly List<IElement> _elements = new List<IElement>();
        
    public void Accept(Visitor visitor)
    {
        visitor.Visit(this);
    }

    public void Add(IElement element)
        => _elements.Add(element);

    public IEnumerator<IElement> GetEnumerator()
    {
        foreach (var element in _elements)
            yield return element;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

コード全体像

using System;
using System.Collections;
using System.Collections.Generic;

namespace VisitorPattern
{
    class Program
    {
        static void Main(string[] args)
        {
            // データの集合
            var objectStructure = new ObjectStructure();
            objectStructure.Add(new ConcreteElementA());
            objectStructure.Add(new ConcreteElementA());
            objectStructure.Add(new ConcreteElementB());

            // データ構造に対して処理を行う訪問者
            var visitor = new ListVisitor();
            
            // Visit ObjectStructure
            // Visit ConcreteElementA
            // Visit ConcreteElementA
            // Visit ConcreteElementB
            objectStructure.Accept(visitor);
        }
    }

    /// <summary>
    /// 訪問者を受け入れることを保証するインターフェイス
    /// </summary>
    public interface IElement
    {
        public abstract void Accept(Visitor visitor);
    }

    /// <summary>
    /// データA
    /// </summary>
    public class ConcreteElementA : IElement
    {
        public void Accept(Visitor visitor)
        {
            visitor.Visit(this);
        }
    }

    /// <summary>
    /// データB
    /// </summary>
    public class ConcreteElementB : IElement
    {
        public void Accept(Visitor visitor)
        {
            visitor.Visit(this);
        }
    }
    
    /// <summary>
    /// データの集合を扱うクラス
    /// </summary>
    public class ObjectStructure : IElement, IEnumerable<IElement>
    {
        private readonly List<IElement> _elements = new List<IElement>();
        
        public void Accept(Visitor visitor)
        {
            visitor.Visit(this);
        }

        public void Add(IElement element)
            => _elements.Add(element);

        public IEnumerator<IElement> GetEnumerator()
        {
            foreach (var element in _elements)
                yield return element;
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }

    /// <summary>
    /// 訪問者
    /// </summary>
    public abstract class Visitor
    {
        public abstract void Visit(ConcreteElementA element);
        public abstract void Visit(ConcreteElementB element);

        public abstract void Visit(ObjectStructure objectStructure);
    }

    /// <summary>
    /// データへの処理をこのクラスに書く(データと処理の分離)
    /// </summary>
    public class ListVisitor : Visitor
    {
        public override void Visit(ConcreteElementA element)
        {
            Console.WriteLine("Visit " + element.GetType().Name);
        }

        public override void Visit(ConcreteElementB element)
        {
            Console.WriteLine("Visit " + element.GetType().Name);
        }

        public override void Visit(ObjectStructure objectStructure)
        {
            Console.WriteLine("Visit " + objectStructure.GetType().Name);
            foreach (var element in objectStructure)
                element.Accept(this);
        }
    }
}
f:id:hanaaaaaachiru:20210210142148p:plain
クラス図

正直初見でこれが実行されたときの流れを追うのは難しいと思います。

f:id:hanaaaaaachiru:20210210143052p:plain
シーケンス図

さいごに

今回は抽象的な表現にて紹介させていただきましたが、本来であればデータ側にフィールド・プロパティがあることが予想されます。

またこのパターンはデータ構造の変更には手間がかかるが、処理の追加は容易であるという特徴があります。

なぜならConcreteElement(データ)を増やしたり,変更を加えようとするとVisitor(処理)側にも変更を余儀なくされることが想像できます。

対してConcreteVisitor(処理)を変えたい・増やしたい場合は、データ側になんら変更を加える必要はないので容易です。

この特性を考えながら、本当にVisitorパターンを用いるべきかどうか判断すると良いかもしれません。

ではまた。