Home [閱讀筆記][Design Pattern] Ch28.訪問者模式(Visitor)
Post
Cancel

[閱讀筆記][Design Pattern] Ch28.訪問者模式(Visitor)

訪問者模式(Visitor)

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

訪問者模式(Visitor)比較麻煩,訪問者模式(Visitor)的能力和複雜性是把雙刃劍,只有當你真正需要它的時候,才考慮使用它。

事實上,用訪問者模式(Visitor)的機會其實並不太多,因為我們很難找到資料結構不變化的情況。(比如,人類性別「男女」這樣的資料結構是不會變化的。)

應用場景

訪問者模式(Visitor)適用於資料結構相對穩定的系統。它把資料結構和作用於結構上的操作之間的耦合解脫開,使得操作集合可以相對自由地演化。

目的

訪問者模式(Visitor)的目的,是要把資料從資料結構分離出來。

如果系統的資料結構易於變化,經常要有新的資料物件增加進來,就不適合使用訪問模式。

優缺點

優點

訪問者模式(Visitor)優點是增加新的操作很容易,因為增加新的操作就意味著增加一個新的訪問者。訪問者模式將有關的行為集中到一個訪問者物件中。

ConcreteVisitor 具體訪問者,通常可以獨立開發,不必跟ConcreteElement 具體元素寫在一起。

缺點

訪問者模式(Visitor)缺點其實也就是使增加新的資料結構變得困難了。

結構

  • Visitor 為該物件結構中ConcreteElement的每一個類別宣告一個Visit操作。
  • ConcreteVisitor 具體訪問者,實現每個由Visitor宣告的操作,每個操作實現演算法的一部分,而該演算法片斷乃是對應於結構中物件的類別。
  • ObjectStructure 能枚舉它的元素,可以提供一個高層的介面以允許訪問者使用它的元素。
  • Element 定義一個Accept操作,它以一個訪問者為參數。
  • ConcreteElementA 具體元素,實現Accept操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Client

    Visitor  為該物件結構中ConcreteElement的每一個類別宣告一個Visit操作。 (狀態)
    + VisitorConcreteElementA(in 具體元素A: ConcreteElementA)
    + VisitorConcreteElementB(in 具體元素B: ConcreteElementB)

        ConcreteVisitorA 具體訪問者,實現每個由Visitor宣告的操作,每個操作實現演算法的一部分,而該演算法片斷乃是對應於結構中物件的類別。
        + VisitorConcreteElementA(in 具體元素A: ConcreteElementA)

        ConcreteVisitorB (成功、失敗、戀愛)
        + VisitorConcreteElementB(in 具體元素B: ConcreteElementB)

    ObjectStructure 能枚舉它的元素,可以提供一個高層的介面以允許訪問者使用它的元素。(物件結構)

        Element 定義一個Accept操作,它以一個訪問者為參數。(人)
        + Accept(in visitor: Visitor)

            ConcreteElementA 具體元素,實現Accept操作。(男人)
            + Accept(in visitor: Visitor)
            + OperationA()

            ConcreteElementB  (女人)
            + Accept(in visitor: Visitor)
            + OperationB()

基本程式碼

Visitor訪問者

Visitor 為該物件結構中ConcreteElement的每一個類別宣告一個Visit操作。

(Visitor為「狀態」類別)

1
2
3
4
5
//訪問者
abstract class Visitor {
    public abstract void VisitorConcreteElementA(ConcreteElementA concreteElementA);
    public abstract void VisitorConcreteElementB(ConcreteElementB concreteElementB);
}

ConcreteVisitor具體訪問者

ConcreteVisitor 具體訪問者,實現每個由Visitor宣告的操作,每個操作實現演算法的一部分,而該演算法片斷乃是對應於結構中物件的類別。

(具體的ConcreteVisitor就是那些「成功」、「失敗」、「戀愛」等狀態)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//ConcreteVisitorA
class ConcreteVisitorA: Visitor {
    public override void VisitorConcreteElementA(ConcreteElementA concreteElementA) {
        Console.WriteLine($"{concreteElementA.GetType().Name}{this.GetType().Name}存取");
    }
    public override void VisitorConcreteElementB(ConcreteElementB concreteElementB) {
        Console.WriteLine($"{concreteElementB.GetType().Name}{this.GetType().Name}存取");
    }
}
//ConcreteVisitorB
class ConcreteVisitorB: Visitor {
    public override void VisitorConcreteElementA(ConcreteElementA concreteElementA) {
        Console.WriteLine($"{concreteElementA.GetType().Name}{this.GetType().Name}存取");
    }
    public override void VisitorConcreteElementB(ConcreteElementB concreteElementB) {
        Console.WriteLine($"{concreteElementB.GetType().Name}{this.GetType().Name}存取");
    }
}

Element元素

Element 定義一個Accept操作,它以一個訪問者為參數。
(比如,「人」類別)

1
2
3
4
//元素抽象類別
abstract class Element {
    public abstract void Accept(Visitor visitor);
}

ConcreteElement具體元素

ConcreteElement 具體元素,實現Accept操作。

(而ConcreteElementAConcreteElementB是「男人」和「女人」)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//具體元素A
class ConcreteElementA: Element {
    //充分利用雙分派技術,實現處理與資料結構的分離
    public override void Accept(Visitor visitor) {
        visitor.VisitorConcreteElementA(this);
    }
    //其他的相關方法
    public void OperationA() { }
}
//具體元素B
class ConcreteElementB: Element {
    //充分利用雙分派技術,實現處理與資料結構的分離
    public override void Accept(Visitor visitor) {
        visitor.VisitorConcreteElementB(this);
    }
    //其他的相關方法
    public void OperationB() { }
}

ObjectStructure物件結構

ObjectStructure 能枚舉它的元素,可以提供一個高層的介面以允許訪問者使用它的元素。

(ObjectStructure 就是「物件結構」類別)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System.Collections.Generic;
class ObjectStructure {
    IList<Element> elements = new List<Element>();

    public void Attach(Element element) {
        elements.Add(element);
    }

    public void Detach(Element element) {
        elements.Remove(element);
    }

    public void Accept(Visitor visitor) {
        foreach(Element e in elements) {
            e.Accept(visitor);
        }
    }
}

用戶端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void Main()
{
    ObjectStructure o = new ObjectStructure();
    o.Attach(new ConcreteElementA());
    o.Attach(new ConcreteElementB());

    ConcreteVisitorA v1 = new ConcreteVisitorA();
    ConcreteVisitorB v2 = new ConcreteVisitorB();

    o.Accept(v1);
    o.Accept(v2);
}

/*
ConcreteElementA 被 ConcreteVisitorA存取
ConcreteElementB 被 ConcreteVisitorA存取
ConcreteElementA 被 ConcreteVisitorB存取
ConcreteElementB 被 ConcreteVisitorB存取
*/

v1.0 男人和女人

例:程式要求實現當男人和女人在不同狀態下的反應。

1
2
3
4
5
6
男人成功的時候,背後多半有一個偉大的女人。
女人成功的時候,背後多半有一個偉大的男人。
男人失敗時,悶頭喝酒,誰也用勸。
失敗女人時,眼淚汪汪,誰也勸不了。
男人戀愛時,凡是不懂也裝懂。
女人戀愛時,遇事懂也裝不懂。

對比這麼多的原因就是因為人類在性別上就只有男人和女人兩類。

1
2
3
4
5
6
7
8
9
public static void Main()
{
    Console.WriteLine("男人成功的時候,背後多半有一個偉大的女人。");
    Console.WriteLine("女人成功的時候,背後多半有一個偉大的男人。");
    Console.WriteLine("男人失敗時,悶頭喝酒,誰也用勸。");
    Console.WriteLine("女人失敗時,眼淚汪汪,誰也勸不了。");
    Console.WriteLine("男人戀愛時,凡是不懂也裝懂。");
    Console.WriteLine("女人戀愛時,遇事懂也裝不懂。");
}

v2.0 男人和女人(簡單的物件導向實現)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
//人-抽象類別
abstract class Person {
    public string Action {get;set;}

    //得到結論或反應
    public abstract void GetConclusion();
}

//男人
class Man: Person {
    //得到結論或反應
    public override void GetConclusion() {
        switch(Action) {
            case "成功":
                Console.WriteLine($"{this.GetType().Name}{Action}的時候,背後多半有一個偉大的女人。");
            break;
            case "失敗":
                Console.WriteLine($"{this.GetType().Name}{Action}時,悶頭喝酒,誰也用勸。");
            break;
            case "戀愛":
                Console.WriteLine($"{this.GetType().Name}{Action}時,凡是不懂也裝懂。");
            break;
        }
    }
}

//女人
class Woman: Person {
    //得到結論或反應
    public override void GetConclusion() {
        switch(Action) {
            case "成功":
                Console.WriteLine($"{this.GetType().Name}{Action}的時候,背後多半有一個偉大的男人。");
            break;
            case "失敗":
                Console.WriteLine($"{this.GetType().Name}{Action}時,眼淚汪汪,誰也勸不了。");
            break;
            case "戀愛":
                Console.WriteLine($"{this.GetType().Name}{Action}時,遇事懂也裝不懂。");
            break;
        }
    }
}

//用戶端
public static void Main()
{
    IList<Person> persons = new List<Person>();

    Person man1 = new Man();
    man1.Action = "成功";
    persons.Add(man1);

    Person woman1 = new Woman();
    woman1.Action = "成功";
    persons.Add(woman1);

    Person man2 = new Man();
    man2.Action = "失敗";
    persons.Add(man2);

    Person woman2 = new Woman();
    woman2.Action = "失敗";
    persons.Add(woman2);

    Person man3 = new Man();
    man3.Action = "戀愛";
    persons.Add(man3);

    Person woman3 = new Woman();
    woman3.Action = "戀愛";
    persons.Add(woman3);

    foreach(Person p in persons) {
        p.GetConclusion();
    }
}

/* 執行結果:

Man成功的時候,背後多半有一個偉大的女人。
Woman成功的時候,背後多半有一個偉大的男人。
Man失敗時,悶頭喝酒,誰也用勸。
Woman失敗時,眼淚汪汪,誰也勸不了。
Man戀愛時,凡是不懂也裝懂。
Woman戀愛時,遇事懂也裝不懂。
*/

this.GetType().Name是獲得類別的名稱

v3.0 男人和女人(訪問者模式)

結構

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Client

    狀態
    + 男人反應(in 具體元素A: 男人)
    + 女人反應(in 具體元素B: 女人)

        成功
        + 男人反應(in 具體元素A: 男人)
        + 女人反應(in 具體元素B: 女人)

        失敗
        + 男人反應(in 具體元素A: 男人)
        + 女人反應(in 具體元素B: 女人)

    物件結構

        人
        + 接受(in visitor: 狀態)

            男人
            + 接受(in Visitor: 狀態)

            女人
            + 接受(in Visitor: 狀態)

「狀態」和「人」的抽象類別

1
2
3
4
5
6
7
8
9
10
11
12
13
//狀態 抽象類別
abstract class Action {
    //得到男人的結論或反應
    public abstract void GetManConcluion(Man concreteElementA);
    //得到女人的結論或反應
    public abstract void GetWomanConcluion(Woman concreteElementB);
}

//人 抽象類別
abstract class Person {
    //接受
    public abstract void Accept(Action visitor); //visitor用來獲得「狀態」的物件
}

具體「狀態」類別

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//成功
class Success: Action {
    //得到男人的結論或反應
    public override void GetManConcluion(Man concreteElementA) {
        Console.WriteLine($"{concreteElementA.GetType().Name} {this.GetType().Name}的時候,背後多半有一個偉大的女人。");
    }
    //得到女人的結論或反應
    public override void GetWomanConcluion(Woman concreteElementB) {
        Console.WriteLine($"{concreteElementB.GetType().Name} {this.GetType().Name}的時候,背後多半有一個偉大的男人。");
    }
}

//失敗
class Failing: Action {
    //得到男人的結論或反應
    public override void GetManConcluion(Man concreteElementA) {
        Console.WriteLine($"{concreteElementA.GetType().Name} {this.GetType().Name}的時候,悶頭喝酒,誰也用勸。");
    }
    //得到女人的結論或反應
    public override void GetWomanConcluion(Woman concreteElementB) {
        Console.WriteLine($"{concreteElementB.GetType().Name} {this.GetType().Name}的時候,眼淚汪汪,誰也勸不了。");
    }
}

//戀愛
class Amativeness: Action {
    //得到男人的結論或反應
    public override void GetManConcluion(Man concreteElementA) {
        Console.WriteLine($"{concreteElementA.GetType().Name} {this.GetType().Name}的時候,凡是不懂也裝懂。");
    }
    //得到女人的結論或反應
    public override void GetWomanConcluion(Woman concreteElementB) {
        Console.WriteLine($"{concreteElementB.GetType().Name} {this.GetType().Name}的時候,遇事懂也裝不懂。");
    }
}

男人、女人類別(具體狀態)

當中用到一種「雙分派」的技術,首先在客戶程式中將具體將態作為參數傳遞給「男人類別」完成了一次分派,然後「男人類別」呼叫作為參數的「具體狀態」中的方法「男人反應」visitor.GetManConcluion(),同時將自己(this)作為參數傳遞進去。這便完成了第二次分派。

「雙分派」意味著得到執行的操作決定於請求的種類和兩個接收者的類型,「接受方法Accept()」就是一個雙分派的操作,它得到執行的操作不僅決定於「狀態類別」的具體狀態,還決定於它存取的「人的類別」。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//男人
class Man: Person {
    //接受, visitor用來獲得「狀態」的物件
    public override void Accept(Action visitor) {
        //首先將"具體狀態"作為參數傳遞給"男人類別"完成了一次分派
        //然後"男人類別"呼叫作為參數的「具體狀態」中的方法「男人反應」,同時將自己(this)作為參數傳進去。這便完成了第二次分派
        visitor.GetManConcluion(this);
    }
}
//女人
class Woman: Person {
    //接受, visitor用來獲得「狀態」的物件
    public override void Accept(Action visitor) {
        visitor.GetWomanConcluion(this);
    }
}

物件結構類別

由於總是需要「男人」和「女人」在不同狀態的對比,所以我們需要一個「物件結構」類別來針對不同的「狀態」走遍「男人」和「女人」,得到不同的反應。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//物件結構
class ObjectStructure {
    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(Person e in elements) {
            e.Accept(visitor);
        }
    }
}

用戶端程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public static void Main()
{
    //在物件結構中加入要對比的「男人」和「女人」
    ObjectStructure o = new ObjectStructure();
    o.Attach(new Man());
    o.Attach(new Woman());

    //成功時的反應
    Success v1 = new Success();
    o.Display(v1);

    //失敗時的反應
    Failing v2 = new Failing();
    o.Display(v2);

    //戀愛時的反應
    Amativeness v3 = new Amativeness();
    o.Display(v3);
}

/* 執行結果:

Man Success的時候,背後多半有一個偉大的女人。
Woman Success的時候,背後多半有一個偉大的男人。
Man Failing的時候,悶頭喝酒,誰也用勸。
Woman Failing的時候,眼淚汪汪,誰也勸不了。
Man Amativeness的時候,凡是不懂也裝懂。
Woman Amativeness的時候,遇事懂也裝不懂。
*/

這樣做有什麼好處?

這樣做就意味著,如果我們現在要增加「結婚」的狀態來考查「男人」和「女人」的反應,只需要怎麼就可以了?

由於用了雙分派,使得我只需要增加一個「狀態」子類別,就可以在用戶端調用來查看,不需要改動其他任何類別的程式碼。

新增一個結婚狀態

結婚狀態類別

1
2
3
4
5
6
7
8
9
10
11
//結婚狀態
class Marriage: Action {
    //得到男人的結論或反應
    public override void GetManConcluion(Man concreteElementA) {
        Console.WriteLine($"{concreteElementA.GetType().Name} {this.GetType().Name}時,戀愛遊戲終結...");
    }
    //得到女人的結論或反應
    public override void GetWomanConcluion(Woman concreteElementB) {
        Console.WriteLine($"{concreteElementB.GetType().Name} {this.GetType().Name}時,愛情長跑路漫慢...。");
    }
}

用戶端程式碼

只要加下面一段程式碼就可以完成。

完美的展現了開放-封閉原則

1
2
3
//結婚時的反應
Marriage v4 = new Marriage();
o.Display(v4);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static void Main()
{
    //在物件結構中加入要對比的「男人」和「女人」
    ObjectStructure o = new ObjectStructure();
    o.Attach(new Man());
    o.Attach(new Woman());

    //成功時的反應
    Success v1 = new Success();
    o.Display(v1);

    //失敗時的反應
    Failing v2 = new Failing();
    o.Display(v2);

    //戀愛時的反應
    Amativeness v3 = new Amativeness();
    o.Display(v3);

    //新增一個结婚状態
    //結婚時的反應
    Marriage v4 = new Marriage();
    o.Display(v4);
}

/* 執行結果:

Man Success的時候,背後多半有一個偉大的女人。
Woman Success的時候,背後多半有一個偉大的男人。
Man Failing的時候,悶頭喝酒,誰也用勸。
Woman Failing的時候,眼淚汪汪,誰也勸不了。
Man Amativeness的時候,凡是不懂也裝懂。
Woman Amativeness的時候,遇事懂也裝不懂。
Man Marriage時,戀愛遊戲終結...
Woman Marriage時,愛情長跑路漫慢...。
*/
This post is licensed under CC BY 4.0 by the author.

[閱讀筆記][Design Pattern] Ch27.解譯器模式(Interpreter)

[閱讀筆記][Design Pattern] 模式總結