24. Visitor


Posted by WayneCheng on 2021-01-31

關於 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

  • 表示在物件結構的元素(e.g. 性別類中的男、女兩個元素)中執行的操作(邏輯)
  • 透過訪問者可在定義新操作(邏輯)時,無需更改執行操作(邏輯)的元素的類別

Visitor(訪問者)屬於行為型(Behavioral Patterns),當遇到類別內元素固定但操作邏輯時常變動時,可藉由 Visitor 將操作邏輯抽離元素,且在新增操作邏輯時不需修改元素本身。

優點:

  • 符合 單一職責原則(Single Responsibility Principle)
  • 符合 開閉原則(Open Closed Principle)

缺點:

  • 新增元素則牽一髮動全身,全部 Visitor 都要修改

2. UML

Class 間關聯:

  • Client 關聯 Visitor & ObjectStructure
  • ObjectStructure 關聯 Element
  • ConcreteVisitor 繼承 Visitor
  • ConcreteElement 繼承 Element

Class:

  • Client:呼叫端
  • Visitor:訪問者的抽象類別或介面
  • ConcreteVisitor:訪問者實作,定義元素的操作邏輯
  • ObjectStructure:定義元素與欲執行的操作邏輯(Visitor)
  • Element:元素的抽象類別或介面
  • ConcreteEelement:元素實作,經由 ObjectStructure 指定操作邏輯(Visitor)

3. 將 UML 轉為程式碼

定義元素與欲執行的操作邏輯(Visitor)

/// <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);
        }
    }
}

訪問者的介面

/// <summary>
/// 訪問者的介面
/// </summary>
public interface IVisitor
{
    void VisitConcreteElementA(ConcreteElementA concreteElementA);
    void VisitConcreteElementB(ConcreteElementB concreteElementB);
}

訪問者實作,定義元素的操作邏輯

/// <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}");
    }
}

元素的介面

/// <summary>
/// 元素的介面
/// </summary>
public interface IElement
{
    void Accept(IVisitor visitor);
}

元素實作,經由 ObjectStructure 指定操作邏輯(Visitor)

/// <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
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();
}

執行結果

ConcreteElementA visited by ConcreteVisitor1
ConcreteElementB visited by ConcreteVisitor1
ConcreteElementA visited by ConcreteVisitor2
ConcreteElementB visited by ConcreteVisitor2

4. 情境

我們接到了一依據是否營業顯示門市看板的需求

  • 營業時間分為「營業中」與「非營業時間」
  • 看板內容分為「折扣資訊」與「會員資訊」,且未來會新增

定義元素(是否營業)與欲顯示看板資訊

/// <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);
        }
    }
}

訪問者的介面

/// <summary>
/// 訪問者的介面
/// </summary>
public interface IVisitor
{
    void OpenMessage(Open open);
    void CloseMessage(Close close);
}

訪問者實作,折扣資訊 & 會員資訊

/// <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 元");
    }
}

元素的介面

/// <summary>
/// 元素的介面
/// </summary>
public interface IElement
{
    void Accept(IVisitor visitor);
}

元素實作,營業中 & 非營業時間

/// <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) 折扣資訊 & 會員資訊
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();
}

執行結果

折扣資訊
營業中:今日咖啡第二杯半價
非營業時間:X 月 X 日 咖啡買一送一

會員資訊
營業中:今日會員集點兩倍送
非營業時間:會員申辦只要 200 元

完整程式碼

GitHub:Behavioral_11_Visitor


總結

由於每個 ConcreteVisitor 都包含所有元素,所以在套用 Visitor 之前要先確認元素是否不會變動,否則之後邏輯日漸複雜,卻又需要新增元素時,會面臨要調整整體架構或是將元素一一加入 ConcreteVisitor 的窘境。


參考資料

  1. Design Patterns
  2. 大話設計模式
  3. dofactory
  4. Refactoring.Guru

新手上路,若有錯誤還請告知,謝謝


#designpattern #CSharp







Related Posts

MTR04_1013

MTR04_1013

Day01 Transfer Learning 遷移式學習

Day01 Transfer Learning 遷移式學習

D31_eslint 與 LIOJ 的愛恨情仇 + 開始第四週

D31_eslint 與 LIOJ 的愛恨情仇 + 開始第四週


Comments