# 24. Visitor ###### tags: `DesignPatterns` ## 關於 Visitor 本篇將討論以下幾個問題 > ### 1. 關於 Visitor > ### 2. UML > ### 3. 將 UML 轉為程式碼 > ### 4. 情境 --- ## 測試環境: >OS:Windows 10 >IDE:Visual Studio 2019 --- ## 1. 關於 Visitor > Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates. > > by [Gang of Four](https://en.wikipedia.org/wiki/Design_Patterns) - 表示在物件結構的元素(e.g. 性別類中的男、女兩個元素)中執行的操作(邏輯) - 透過訪問者可在定義新操作(邏輯)時,無需更改執行操作(邏輯)的元素的類別 Visitor(訪問者)屬於行為型(Behavioral Patterns),當遇到**類別內元素固定但操作邏輯時常變動**時,可藉由 Visitor 將操作邏輯抽離元素,且在新增操作邏輯時不需修改元素本身。 優點: - 符合 單一職責原則(Single Responsibility Principle) - 符合 開閉原則(Open Closed Principle) 缺點: - 新增元素則牽一髮動全身,全部 Visitor 都要修改 --- ## 2. UML ![](https://i.imgur.com/DjihitP.jpg) Class 間關聯: - Client 關聯 Visitor & ObjectStructure - ObjectStructure 關聯 Element - ConcreteVisitor 繼承 Visitor - ConcreteElement 繼承 Element Class: - Client:呼叫端 - Visitor:訪問者的抽象類別或介面 - ConcreteVisitor:訪問者實作,定義元素的操作邏輯 - ObjectStructure:定義元素與欲執行的操作邏輯(Visitor) - Element:元素的抽象類別或介面 - ConcreteEelement:元素實作,經由 ObjectStructure 指定操作邏輯(Visitor) --- ## 3. 將 UML 轉為程式碼 定義元素與欲執行的操作邏輯(Visitor) ```C# /// <summary> /// 定義元素與欲執行的操作邏輯(Visitor) /// </summary> public class ObjectStructure { private List<IElement> _elements = new List<IElement>(); public void Attach(IElement element) { _elements.Add(element); } public void Detach(IElement element) { _elements.Remove(element); } public void Accept(IVisitor visitor) { foreach (IElement element in _elements) { element.Accept(visitor); } } } ``` 訪問者的介面 ```C# /// <summary> /// 訪問者的介面 /// </summary> public interface IVisitor { void VisitConcreteElementA(ConcreteElementA concreteElementA); void VisitConcreteElementB(ConcreteElementB concreteElementB); } ``` 訪問者實作,定義元素的操作邏輯 ```C# /// <summary> /// 訪問者實作,定義元素的操作邏輯 /// </summary> public class ConcreteVisitor1 : IVisitor { public void VisitConcreteElementA(ConcreteElementA concreteElementA) { Console.WriteLine($"{concreteElementA.GetType().Name} visited by {this.GetType().Name}"); } public void VisitConcreteElementB(ConcreteElementB concreteElementB) { Console.WriteLine($"{concreteElementB.GetType().Name} visited by {this.GetType().Name}"); } } /// <summary> /// 訪問者實作,定義元素的操作邏輯 /// </summary> public class ConcreteVisitor2 : IVisitor { public void VisitConcreteElementA(ConcreteElementA concreteElementA) { Console.WriteLine($"{concreteElementA.GetType().Name} visited by {this.GetType().Name}"); } public void VisitConcreteElementB(ConcreteElementB concreteElementB) { Console.WriteLine($"{concreteElementB.GetType().Name} visited by {this.GetType().Name}"); } } ``` 元素的介面 ```C# /// <summary> /// 元素的介面 /// </summary> public interface IElement { void Accept(IVisitor visitor); } ``` 元素實作,經由 ObjectStructure 指定操作邏輯(Visitor) ```C# /// <summary> /// 元素實作,經由 ObjectStructure 指定操作邏輯(Visitor) /// </summary> public class ConcreteElementA : IElement { public void Accept(IVisitor visitor) { visitor.VisitConcreteElementA(this); } public void OperationA() { } } /// <summary> /// 元素實作,經由 ObjectStructure 指定操作邏輯(Visitor) /// </summary> public class ConcreteElementB : IElement { public void Accept(IVisitor visitor) { visitor.VisitConcreteElementB(this); } public void OperationB() { } } ``` 1. 建立`objectStructure` 2. 將元素 A & B 附加至`objectStructure` 3. 指定操作邏輯(Visitor) 1 & 2 ```C# static void Main(string[] args) { Default.ObjectStructure objectStructure = new Default.ObjectStructure(); objectStructure.Attach(new Default.ConcreteElementA()); objectStructure.Attach(new Default.ConcreteElementB()); objectStructure.Accept(new Default.ConcreteVisitor1()); objectStructure.Accept(new Default.ConcreteVisitor2()); Console.ReadLine(); } ``` 執行結果 ```Console ConcreteElementA visited by ConcreteVisitor1 ConcreteElementB visited by ConcreteVisitor1 ConcreteElementA visited by ConcreteVisitor2 ConcreteElementB visited by ConcreteVisitor2 ``` --- ## 4. 情境 我們接到了一依據是否營業顯示門市看板的需求 - 營業時間分為「營業中」與「非營業時間」 - 看板內容分為「折扣資訊」與「會員資訊」,且未來會新增 定義元素(是否營業)與欲顯示看板資訊 ```C# /// <summary> /// 定義元素(是否營業)與欲顯示看板資訊 /// </summary> public class ObjectStructure { private List<IElement> _elements = new List<IElement>(); public void Attach(IElement element) { _elements.Add(element); } public void Detach(IElement element) { _elements.Remove(element); } public void Accept(IVisitor visitor) { foreach (IElement element in _elements) { element.Accept(visitor); } } } ``` 訪問者的介面 ```C# /// <summary> /// 訪問者的介面 /// </summary> public interface IVisitor { void OpenMessage(Open open); void CloseMessage(Close close); } ``` 訪問者實作,折扣資訊 & 會員資訊 ```C# /// <summary> /// 訪問者實作,折扣資訊 /// </summary> public class DiscountMessage : IVisitor { public void OpenMessage(Open open) { Console.WriteLine($"營業中:今日咖啡第二杯半價"); } public void CloseMessage(Close close) { Console.WriteLine($"非營業時間:X 月 X 日 咖啡買一送一"); } } /// <summary> /// 訪問者實作,會員資訊 /// </summary> public class MemberMessage : IVisitor { public void OpenMessage(Open open) { Console.WriteLine($"營業中:今日會員集點兩倍送"); } public void CloseMessage(Close close) { Console.WriteLine($"非營業時間:會員申辦只要 200 元"); } } ``` 元素的介面 ```C# /// <summary> /// 元素的介面 /// </summary> public interface IElement { void Accept(IVisitor visitor); } ``` 元素實作,營業中 & 非營業時間 ```C# /// <summary> /// 元素實作,營業中 /// </summary> public class Open : IElement { public void Accept(IVisitor visitor) { visitor.OpenMessage(this); } } /// <summary> /// 元素實作,非營業時間 /// </summary> public class Close : IElement { public void Accept(IVisitor visitor) { visitor.CloseMessage(this); } } /// <summary> /// 定義元素(是否營業)與欲顯示看板資訊 /// </summary> public class ObjectStructure { private List<IElement> _elements = new List<IElement>(); public void Attach(IElement element) { _elements.Add(element); } public void Detach(IElement element) { _elements.Remove(element); } public void Accept(IVisitor visitor) { foreach (IElement element in _elements) { element.Accept(visitor); } } } ``` 1. 建立`objectStructure` 2. 將元素 營業中 & 非營業時間 附加至`objectStructure` 3. 指定操作邏輯(Visitor) 折扣資訊 & 會員資訊 ```C# static void Main(string[] args) { Situation.ObjectStructure objectStructure = new Situation.ObjectStructure(); objectStructure.Attach(new Situation.Open()); objectStructure.Attach(new Situation.Close()); Console.WriteLine($"折扣資訊"); objectStructure.Accept(new Situation.DiscountMessage()); Console.WriteLine($"\n會員資訊"); objectStructure.Accept(new Situation.MemberMessage()); Console.ReadLine(); } ``` 執行結果 ```Console 折扣資訊 營業中:今日咖啡第二杯半價 非營業時間:X 月 X 日 咖啡買一送一 會員資訊 營業中:今日會員集點兩倍送 非營業時間:會員申辦只要 200 元 ``` --- ## 完整程式碼 GitHub:[Behavioral_11_Visitor](https://github.com/darionnnnnn/blog/tree/master/Blog/Behavioral_11_Visitor) --- ## 總結 ### 由於每個 ConcreteVisitor 都包含所有元素,所以在套用 Visitor 之前要先確認元素是否不會變動,否則之後邏輯日漸複雜,卻又需要新增元素時,會面臨要調整整體架構或是將元素一一加入 ConcreteVisitor 的窘境。 --- ## 參考資料 1. [Design Patterns](https://en.wikipedia.org/wiki/Design_Patterns) 2. [大話設計模式](https://www.tenlong.com.tw/products/9789866761799) 2. [dofactory](https://www.dofactory.com/) 3. [Refactoring.Guru](https://refactoring.guru/) --- ## 新手上路,若有錯誤還請告知,謝謝