定義
訪問者模式是封裝一些施加於某種數據結構之上的操作。一旦這些操作需要修改的話,接受這個操作的數據結構則可以保持不變。訪問者模式適用於數據結構相對穩定的系統,它把數據結構和作用於數據結構之上的操作之間的耦合度降低,使得操作集合可以相對自由地改變。
數據結構的每一個節點都可以接受一個訪問者的調用,此節點訪問者對象傳入節點對象,而訪問者對象則反過來執行節點對象的操作。這樣的過長叫做“雙重分流”。節點調用訪問者,將它自己傳入,訪問者則將某種算符針對此節點執行。
結構
訪問者模式是用來封裝某種數據結構中的方法。具體封裝過程是:每個元素接收一個訪問者的調用,每個元素的指定方法接收訪問者對象作爲參數傳入,訪問者對象則反過來調用元素對象的操作。這裏要明確一點:訪問者模式中具體訪問者的數目和具體節點的數目沒有任何關係
訪問者模式涉及到的角色:
- 抽象訪問者角色:聲明一個或多個訪問操作,使得所有具體訪問者必須實現的接口。
- 具體訪問者角色:實現抽象訪問者角色中聲明的所有接口。
- 抽象節點角色:聲明一個接受操作,接受一個訪問者對象作爲參數。
- 具體節點角色:實現抽象元素所規定的接收操作。
- 結構對象角色:節點的容器,可以包含多個不同類或接口的容器。
實現
在將實現之前,我想先不用訪問者模式的方式來實現某個場景——現在我想遍歷每個元素對象,然後調用每個元素對象的Print方法來打印該元素對象的信息。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace _38VistorPatternDemo
{
//抽象元素角色
public abstract class Element
{
public abstract void Print();
}
//具體元素
public class ElementA:Element
{
public override void Print()
{
Console.WriteLine("我是元素A");
}
}
public class ElementB : Element
{
public override void Print()
{
Console.WriteLine("我是元素B");
}
}
//對象結構
public class ObjectStructure
{
private ArrayList elements = new ArrayList();
public ArrayList Elements
{
get { return elements; }
}
public ObjectStructure()
{
Random ran = new Random();
for (int i = 0; i < 6; i++)
{
int ranNum = ran.Next(10);
if(ranNum>5)
{
elements.Add(new ElementA());
}
else
{
elements.Add(new ElementB());
}
}
}
}
class Program
{
static void Main(string[] args)
{
ObjectStructure objstructure = new ObjectStructure();
//遍歷對象結構中的對象集合,訪問每個元素的Print方法打印元素
foreach (Element e in objstructure.Elements)
{
e.Print();
}
}
}
}
上面的代碼很準確的解決了我們剛纔提出的場景,但是需求在時刻變化,如果此時我們除了想打印元素的信息外,還想打印出訪問元素的時間,此時我們不得不去修改每個元素的Print方法,再加入相對應的訪問時間。這樣設計顯然不符合開閉原則,即某個方法的改變,要去改變每個元素類。既然,這裏變化的點是操作的改變,而每個數據元素的數據結構是不變的。所以此時就思考——能不能把操作於元素的操作和元素本身的數據結構分開呢?解開這兩者的耦合度,如果這樣是操作發生變化,就不需要去修改元素本身了。但是如果是元素數據結構發生變化,就不得不去修改元素類了。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace _38VistorPatternDemo
{
//抽象元素角色
public abstract class Element
{
public abstract void Accept(IVistor vistor);
public abstract void Print();
}
//具體元素
public class ElementA:Element
{
public override void Accept(IVistor vistor)
{
vistor.Visit(this);
}
public override void Print()
{
Console.WriteLine("我是元素A");
}
}
public class ElementB : Element
{
public override void Accept(IVistor vistor)
{
vistor.Visit(this);
}
public override void Print()
{
Console.WriteLine("我是元素B");
}
}
//抽象訪問者
public interface IVistor
{
void Visit(ElementA a);
void Visit(ElementB b);
}
//具體訪問者
public class ConcreteVistor:IVistor
{
//visit方法是再去調用元素的Accept方法
public void Visit(ElementA a)
{
a.Print();
}
public void Visit(ElementB b)
{
b.Print();
}
}
//對象結構
public class ObjectStructure
{
private ArrayList elements = new ArrayList();
public ArrayList Elements
{
get { return elements; }
}
public ObjectStructure()
{
Random ran = new Random();
for (int i = 0; i < 6; i++)
{
int ranNum = ran.Next(10);
if(ranNum>5)
{
elements.Add(new ElementA());
}
else
{
elements.Add(new ElementB());
}
}
}
}
class Program
{
static void Main(string[] args)
{
ObjectStructure objstructure = new ObjectStructure();
//遍歷對象結構中的對象集合,訪問每個元素的Print方法打印元素
foreach (Element e in objstructure.Elements)
{
//單個元素接受訪問者訪問
e.Accept(new ConcreteVistor());
}
}
}
}
從上面代碼可知,使用訪問者模式實現上面場景後,元素Print方法的訪問封裝到訪問者對象中了,此時客戶端與元素的Print方法就隔離開了。如果需要添加打印訪問時間的需求,此時只需要在添加一個具體的訪問者類即可。
適用場景
- 如果系統有比較穩定的數據結構,而又有易於變化的算法時,此時可以考慮使用訪問者模式。因爲訪問者模式使得算法操作的添加比較容易。
- 如果一組類中,存在着相似的操作,爲了避免出現大量重複的代碼,可以考慮把重複的操作封裝到訪問者中。(當然也可以考慮使用抽象類了)
- 如果一個對象存在着一些與本身對象不相干,或關係比較弱的操作時,爲了避免操作污染這個對象,則可以考慮把這些操作封裝到訪問者對象中。
優缺點
優點
- 訪問者模式使得添加新的操作變的容易。如果一些操作依賴於一個複雜的結構對象的話,那麼一般而言,添加新的操作會變得很複雜。而適用訪問者模式,增加新的操作就意味着添加一個新的訪問者類。因此,使得添加新的操作變的容易
- 訪問者模式使得有關的行爲操作集中到一個訪問者對象中,而不是分散到一個個元素類中。這點類似於中介者模式
- 訪問者模式可以訪問屬於不同的等級結構的成員對象,而迭代只能訪問屬於同一個等級結構的成員對象
缺點
- 增加新的元素變的困難。每增加一個新的元素意味着要在抽象訪問者角色中增加一個新的抽象操作,並在每一個具體訪問者類中添加對應的具體操作。
總結
訪問者模式是用來封裝一些作用於某種數據結構之上的操作。它使得可以在不改變元素本身的前提下增加作用於這些元素的新操作,訪問者模式的目的是把操作從數據結構中分離出來。