C#設計模式(22)——訪問者模式(Vistor Pattern)
一、引言
在上一篇博文中分享了責任鏈模式,責任鏈模式主要應用在系統中的某些功能需要多個對象參與才能完成的場景。在這篇博文中,我將為大家分享我對訪問者模式的理解。
二、訪問者模式介紹
2.1 訪問者模式的定義
訪問者模式是封裝一些施加於某種數據結構之上的操作。一旦這些操作需要修改的話,接受這個操作的數據結構則可以保存不變。訪問者模式適用於數據結構相對穩定的系統, 它把數據結構和作用於數據結構之上的操作之間的耦合度降低,使得操作集合可以相對自由地改變。
數據結構的每一個節點都可以接受一個訪問者的調用,此節點向訪問者對象傳入節點對象,而訪問者對象則反過來執行節點對象的操作。這樣的過程叫做“雙重分派”。節點調用訪問者,將它自己傳入,訪問者則將某算法針對此節點執行。
2.2 訪問者模式的結構圖
從上面描述可知,訪問者模式是用來封裝某種數據結構中的方法。具體封裝過程是:每個元素接受一個訪問者的調用,每個元素的Accept方法接受訪問者對象作為參數傳入,訪問者對象則反過來調用元素對象的操作。具體的訪問者模式結構圖如下所示。
這裏需要明確一點:訪問者模式中具體訪問者的數目和具體節點的數目沒有任何關系。從訪問者的結構圖可以看出,訪問者模式涉及以下幾類角色。
- 抽象訪問者角色(Vistor):聲明一個活多個訪問操作,使得所有具體訪問者必須實現的接口。
- 具體訪問者角色(ConcreteVistor):實現抽象訪問者角色中所有聲明的接口。
- 抽象節點角色(Element):聲明一個接受操作,接受一個訪問者對象作為參數。
- 具體節點角色(ConcreteElement):實現抽象元素所規定的接受操作。
- 結構對象角色(ObjectStructure):節點的容器,可以包含多個不同類或接口的容器。
2.3 訪問者模式的實現
在講訴訪問者模式的實現時,我想先不用訪問者模式的方式來實現某個場景。具體場景是——現在我想遍歷每個元素對象,然後調用每個元素對象的Print方法來打印該元素對象的信息。如果此時不采用訪問者模式的話,實現這個場景再簡單不過了,具體實現代碼如下所示:
1 namespace DonotUsevistorPattern 2 { 3 // 抽象元素角色4 public abstract class Element 5 { 6 public abstract void Print(); 7 } 8 9 // 具體元素A 10 public class ElementA : Element 11 { 12 public override void Print() 13 { 14 Console.WriteLine("我是元素A"); 15 } 16 } 17 18 // 具體元素B 19 public class ElementB : Element 20 { 21 public override void Print() 22 { 23 Console.WriteLine("我是元素B"); 24 } 25 } 26 27 // 對象結構 28 public class ObjectStructure 29 { 30 private ArrayList elements = new ArrayList(); 31 32 public ArrayList Elements 33 { 34 get { return elements; } 35 } 36 37 public ObjectStructure() 38 { 39 Random ran = new Random(); 40 for (int i = 0; i < 6; i++) 41 { 42 int ranNum = ran.Next(10); 43 if (ranNum > 5) 44 { 45 elements.Add(new ElementA()); 46 } 47 else 48 { 49 elements.Add(new ElementB()); 50 } 51 } 52 } 53 } 54 55 class Program 56 { 57 static void Main(string[] args) 58 { 59 ObjectStructure objectStructure = new ObjectStructure(); 60 // 遍歷對象結構中的對象集合,訪問每個元素的Print方法打印元素信息 61 foreach (Element e in objectStructure.Elements) 62 { 63 e.Print(); 64 } 65 66 Console.Read(); 67 } 68 } 69 }
上面代碼很準確了解決了我們剛才提出的場景,但是需求在時刻變化的,如果此時,我除了想打印元素的信息外,還想打印出元素被訪問的時間,此時我們就不得不去修改每個元素的Print方法,再加入相對應的輸入訪問時間的輸出信息。這樣的設計顯然不符合“開-閉”原則,即某個方法操作的改變,會使得必須去更改每個元素類。既然,這裏變化的點是操作的改變,而每個元素的數據結構是不變的。所以此時就思考——能不能把操作於元素的操作和元素本身的數據結構分開呢?解開這兩者的耦合度,這樣如果是操作發現變化時,就不需要去更改元素本身了,但是如果是元素數據結構發現變化,例如,添加了某個字段,這樣就不得不去修改元素類了。此時,我們可以使用訪問者模式來解決這個問題,即把作用於具體元素的操作由訪問者對象來調用。具體的實現代碼如下所示:
1 namespace VistorPattern 2 { 3 // 抽象元素角色 4 public abstract class Element 5 { 6 public abstract void Accept(IVistor vistor); 7 public abstract void Print(); 8 } 9 10 // 具體元素A 11 public class ElementA :Element 12 { 13 public override void Accept(IVistor vistor) 14 { 15 // 調用訪問者visit方法 16 vistor.Visit(this); 17 } 18 public override void Print() 19 { 20 Console.WriteLine("我是元素A"); 21 } 22 } 23 24 // 具體元素B 25 public class ElementB :Element 26 { 27 public override void Accept(IVistor vistor) 28 { 29 vistor.Visit(this); 30 } 31 public override void Print() 32 { 33 Console.WriteLine("我是元素B"); 34 } 35 } 36 37 // 抽象訪問者 38 public interface IVistor 39 { 40 void Visit(ElementA a); 41 void Visit(ElementB b); 42 } 43 44 // 具體訪問者 45 public class ConcreteVistor :IVistor 46 { 47 // visit方法而是再去調用元素的Accept方法 48 public void Visit(ElementA a) 49 { 50 a.Print(); 51 } 52 public void Visit(ElementB b) 53 { 54 b.Print(); 55 } 56 } 57 58 // 對象結構 59 public class ObjectStructure 60 { 61 private ArrayList elements = new ArrayList(); 62 63 public ArrayList Elements 64 { 65 get { return elements; } 66 } 67 68 public ObjectStructure() 69 { 70 Random ran = new Random(); 71 for (int i = 0; i < 6; i++) 72 { 73 int ranNum = ran.Next(10); 74 if (ranNum > 5) 75 { 76 elements.Add(new ElementA()); 77 } 78 else 79 { 80 elements.Add(new ElementB()); 81 } 82 } 83 } 84 } 85 86 class Program 87 { 88 static void Main(string[] args) 89 { 90 ObjectStructure objectStructure = new ObjectStructure(); 91 foreach (Element e in objectStructure.Elements) 92 { 93 // 每個元素接受訪問者訪問 94 e.Accept(new ConcreteVistor()); 95 } 96 97 Console.Read(); 98 } 99 } 100 }
從上面代碼可知,使用訪問者模式實現上面場景後,元素Print方法的訪問封裝到了訪問者對象中了(我覺得可以把Print方法封裝到具體訪問者對象中。),此時客戶端與元素的Print方法就隔離開了。此時,如果需要添加打印訪問時間的需求時,此時只需要再添加一個具體的訪問者類即可。此時就不需要去修改元素中的Print()方法了。
三、訪問者模式的應用場景
每個設計模式都有其應當使用的情況,那讓我們看看訪問者模式具體應用場景。如果遇到以下場景,此時我們可以考慮使用訪問者模式。
- 如果系統有比較穩定的數據結構,而又有易於變化的算法時,此時可以考慮使用訪問者模式。因為訪問者模式使得算法操作的添加比較容易。
- 如果一組類中,存在著相似的操作,為了避免出現大量重復的代碼,可以考慮把重復的操作封裝到訪問者中。(當然也可以考慮使用抽象類了)
- 如果一個對象存在著一些與本身對象不相幹,或關系比較弱的操作時,為了避免操作汙染這個對象,則可以考慮把這些操作封裝到訪問者對象中。
四、訪問者模式的優缺點
訪問者模式具有以下優點:
- 訪問者模式使得添加新的操作變得容易。如果一些操作依賴於一個復雜的結構對象的話,那麽一般而言,添加新的操作會變得很復雜。而使用訪問者模式,增加新的操作就意味著添加一個新的訪問者類。因此,使得添加新的操作變得容易。
- 訪問者模式使得有關的行為操作集中到一個訪問者對象中,而不是分散到一個個的元素類中。這點類似與"中介者模式"。
- 訪問者模式可以訪問屬於不同的等級結構的成員對象,而叠代只能訪問屬於同一個等級結構的成員對象。
訪問者模式也有如下的缺點:
- 增加新的元素類變得困難。每增加一個新的元素意味著要在抽象訪問者角色中增加一個新的抽象操作,並在每一個具體訪問者類中添加相應的具體操作。
五、總結
訪問者模式是用來封裝一些施加於某種數據結構之上的操作。它使得可以在不改變元素本身的前提下增加作用於這些元素的新操作,訪問者模式的目的是把操作從數據結構中分離出來。
C#設計模式(22)——訪問者模式(Vistor Pattern)