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

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

ですので登場人物にも、データ側と処理の記述側の2つを意識しながら見ていただけると理解しやすいと思います。
データ構造側
名前 | 意味 |
---|---|
IElement | Visitor(訪問者)を受け入れることを保証するインターフェイス |
ConcreteElement | データ構造の具象クラス |

処理の記述側
名前 | 意味 |
---|---|
Visitor | 処理を記述する抽象クラス |
ConcreteVisitor | 処理を記述する具象クラス |

データ構造側のコード
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); } } }

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

さいごに
今回は抽象的な表現にて紹介させていただきましたが、本来であればデータ側にフィールド・プロパティがあることが予想されます。
またこのパターンはデータ構造の変更には手間がかかるが、処理の追加は容易であるという特徴があります。
なぜならConcreteElement
(データ)を増やしたり,変更を加えようとするとVisitor
(処理)側にも変更を余儀なくされることが想像できます。
対してConcreteVisitor
(処理)を変えたい・増やしたい場合は、データ側になんら変更を加える必要はないので容易です。
この特性を考えながら、本当にVisitor
パターンを用いるべきかどうか判断すると良いかもしれません。
ではまた。