引子:男人和女人#

小菜抄了一些「男人 vs. 女人」的對比段子:

  • 男人成功時,背後多半有一個偉大的女人;女人成功時,背後大多有一個不成功的男人
  • 男人失敗時,悶頭喝酒,誰也不用勸;女人失敗時,眼淚汪汪,誰也勸不了
  • 男人戀愛時,凡事不懂也要裝懂;女人戀愛時,遇事懂也裝作不懂

小菜一句話啟發了大鳥:「男女對比這麼多的原因主要是因為人類在性別上就只有男人和女人兩類」——這正是訪問者模式可以實施的前提:資料結構穩定

第一版:直接 print#

Console.WriteLine("男人成功時,背後多半有一個偉大的女人。");
Console.WriteLine("女人成功時,背後大多有一個不成功的男人。");
// ...

這跟印 Hello World 沒兩樣,沒有面向對象。

第二版:男人女人類 + 狀態欄位#

abstract class Person
{
    public string Action { get; set; }
    public abstract void GetConclusion();
}

class Man : Person
{
    public override void GetConclusion()
    {
        if (Action == "成功") Console.WriteLine($"{GetType().Name}{Action}時,背後多半有一個偉大的女人。");
        else if (Action == "失敗") Console.WriteLine($"{GetType().Name}{Action}時,悶頭喝酒,誰也不用勸。");
        else if (Action == "戀愛") Console.WriteLine($"{GetType().Name}{Action}時,凡事不懂也要裝懂。");
    }
}

class Woman : Person { /* 類似 */ }

新增「結婚」狀態時,ManWoman 兩個類都要修改——增加 if/else 分支,違反開放-封閉原則。

訪問者模式#

訪問者模式(Visitor Pattern):表示一個作用於某物件結構中的各元素的操作。它使你可以在不改變各元素的類別的前提下定義作用於這些元素的新操作。[DP]

關鍵思想:人只分為男人和女人——這個分類是穩定的;但作用在他們身上的「狀態」(成功、失敗、戀愛、結婚……)是易變的。

雙分派(Double Dispatch)#

訪問者模式核心是雙分派技術:

  1. 客戶端把具體狀態傳給「男人」類 → 第一次分派
  2. 「男人」類呼叫該狀態的 GetManConclusion,並把自己(this)傳進去 → 第二次分派

最終執行的操作同時取決於請求的種類與兩個接收者的類型

結構#

  • Visitor(抽象訪問者):為每個 ConcreteElement 類別宣告一個 Visit 操作
  • ConcreteVisitor:實作 Visitor 宣告的每個操作
  • Element(抽象元素):定義一個 Accept 操作,以訪問者為參數
  • ConcreteElement:實作 Accept 操作(雙分派)
  • ObjectStructure:能枚舉元素,提供讓訪問者訪問元素的高層介面
classDiagram
    class ObjectStructure
    class Action {
        <<abstract>>
        +GetManConclusion(Man)*
        +GetWomanConclusion(Woman)*
    }
    class Success
    class Failing
    class Amativeness
    class Person {
        <<abstract>>
        +Accept(Action)*
    }
    class Man
    class Woman
    Action <|-- Success
    Action <|-- Failing
    Action <|-- Amativeness
    Person <|-- Man
    Person <|-- Woman
    ObjectStructure o--> Person
    Person ..> Action : Accept
abstract class Action
{
    public abstract void GetManConclusion(Man concreteElementA);
    public abstract void GetWomanConclusion(Woman concreteElementB);
}

abstract class Person
{
    public abstract void Accept(Action visitor);
}

class Man : Person
{
    public override void Accept(Action visitor) => visitor.GetManConclusion(this);
}

class Woman : Person
{
    public override void Accept(Action visitor) => visitor.GetWomanConclusion(this);
}

class Success : Action
{
    public override void GetManConclusion(Man m)
        => Console.WriteLine($"{m.GetType().Name}{GetType().Name}時,背後多半有一個偉大的女人。");
    public override void GetWomanConclusion(Woman w)
        => Console.WriteLine($"{w.GetType().Name}{GetType().Name}時,背後大多有一個不成功的男人。");
}

class Failing : Action { /* ... */ }
class Amativeness : Action { /* ... */ }

物件結構:

class ObjectStructure
{
    private IList<Person> elements = new List<Person>();
    public void Attach(Person element) => elements.Add(element);
    public void Detach(Person element) => elements.Remove(element);
    public void Display(Action visitor)
    {
        foreach (var e in elements) e.Accept(visitor);
    }
}

客戶端:

ObjectStructure o = new ObjectStructure();
o.Attach(new Man());
o.Attach(new Woman());

o.Display(new Success());
o.Display(new Failing());
o.Display(new Amativeness());

擴展:新增「結婚」狀態#

class Marriage : Action
{
    public override void GetManConclusion(Man m) { /* ... */ }
    public override void GetWomanConclusion(Woman w) { /* ... */ }
}

// 客戶端
o.Display(new Marriage());

完美體現了開放-封閉原則——新增狀態只需新增一個訪問者類,所有元素類完全不動。

適用前提#

訪問者模式的目的是把處理從資料結構分離出來

適合:資料結構相對穩定、演算法易於變化的系統。

不適合:資料結構經常變化(要新增新的資料對象)的系統——因為新增 ConcreteElement 要修改所有 Visitor 子類。

優缺點#

優點#

  • 新增新操作很容易——只需新增一個新的訪問者
  • 將有關行為集中到一個訪問者物件中
  • ConcreteVisitor 可以單獨開發,不必跟 ConcreteElement 寫在一起
  • 提高 ConcreteElement 之間的獨立性

缺點#

新增新的資料結構變得困難——每新增一個 Element 要修改所有 Visitor。

GoF 的 Erich Gamma 警告:「大多時候你不需要訪問者模式,但當一旦你需要訪問者模式時,那就是真的需要它了。」

現實中很難找到資料結構不變化的情況,所以用訪問者模式的機會不多。

通用結構#

abstract class Visitor
{
    public abstract void VisitConcreteElementA(ConcreteElementA a);
    public abstract void VisitConcreteElementB(ConcreteElementB b);
}

abstract class Element
{
    public abstract void Accept(Visitor visitor);
}

class ConcreteElementA : Element
{
    public override void Accept(Visitor visitor) => visitor.VisitConcreteElementA(this);
    public void OperationA() { }
}

本章小結#

「比上不足,比下有餘」——男人與女人最大的區別。

訪問者模式的能力與複雜性是把雙刃劍,只有當你真正需要它的時候才考慮使用