訪問者模式是對象的行爲模式。訪問者模式的目的是封裝一些施加於某種數據結構元素之上的操做。一旦這些操做須要修改的話,接受這個操做的數據結構則能夠保持不變。node
變量被聲明時的類型叫作變量的靜態類型(Static Type),有些人又把靜態類型叫作明顯類型(Apparent Type);而變量所引用的對象的真實類型又叫作變量的實際類型(Actual Type)。好比:算法
List list = null; list = new ArrayList();
聲明瞭一個變量list,它的靜態類型(也叫明顯類型)是List,而它的實際類型是ArrayList。設計模式
根據對象的類型而對方法進行的選擇,就是分派(Dispatch),分派(Dispatch)又分爲兩種,即靜態分派和動態分派。數據結構
靜態分派(Static Dispatch)發生在編譯時期,分派根據靜態類型信息發生。靜態分派對於咱們來講並不陌生,方法重載就是靜態分派。ide
動態分派(Dynamic Dispatch)發生在運行時期,動態分派動態地置換掉某個方法。測試
Java經過方法重載支持靜態分派。用墨子騎馬的故事做爲例子,墨子能夠騎白馬或者黑馬。墨子與白馬、黑馬和馬的類圖以下所示:this
在這個系統中,墨子由Mozi類表明spa
public class Mozi { public void ride(Horse h){ System.out.println("騎馬"); } public void ride(WhiteHorse wh){ System.out.println("騎白馬"); } public void ride(BlackHorse bh){ System.out.println("騎黑馬"); } public static void main(String[] args) { Horse wh = new WhiteHorse(); Horse bh = new BlackHorse(); Mozi mozi = new Mozi(); mozi.ride(wh); mozi.ride(bh); } }
顯然,Mozi類的ride()方法是由三個方法重載而成的。這三個方法分別接受馬(Horse)、白馬(WhiteHorse)、黑馬(BlackHorse)等類型的參數。設計
那麼在運行時,程序會打印出什麼結果呢?結果是程序會打印出相同的兩行「騎馬」。換言之,墨子發現他所騎的都是馬。對象
爲何呢?兩次對ride()方法的調用傳入的是不一樣的參數,也就是wh和bh。它們雖然具備不一樣的真實類型,可是它們的靜態類型都是同樣的,均是Horse類型。
重載方法的分派是根據靜態類型進行的,這個分派過程在編譯時期就完成了。
Java經過方法的重寫支持動態分派。用馬吃草的故事做爲例子,代碼以下所示:
public class Horse { public void eat(){ System.out.println("馬吃草"); } }
public class BlackHorse extends Horse { @Override public void eat() { System.out.println("黑馬吃草"); } }
public class Client { public static void main(String[] args) { Horse h = new BlackHorse(); h.eat(); } }
變量h的靜態類型是Horse,而真實類型是BlackHorse。若是上面最後一行的eat()方法調用的是BlackHorse類的 eat()方法,那麼上面打印的就是「黑馬吃草」;相反,若是上面的eat()方法調用的是Horse類的eat()方法,那麼打印的就是「馬吃草」。
因此,問題的核心就是Java編譯器在編譯時期並不老是知道哪些代碼會被執行,由於編譯器僅僅知道對象的靜態類型,而不知道對象的真實類型;而 方法的調用則是根據對象的真實類型,而不是靜態類型。這樣一來,上面最後一行的eat()方法調用的是BlackHorse類的eat()方法,打印的是 「黑馬吃草」。
一個方法所屬的對象叫作方法的接收者,方法的接收者與方法的參數統稱作方法的宗量。好比下面例子中的Test類
public class Test { public void print(String str){ System.out.println(str); } }
在上面的類中,print()方法屬於Test對象,因此它的接收者也就是Test對象了。print()方法有一個參數是str,它的類型是String。
根據分派能夠基於多少種宗量,能夠將面向對象的語言劃分爲單分派語言(Uni-Dispatch)和多分派語言(Multi-Dispatch)。單分派語言根據一個宗量的類型進行對方法的選擇,多分派語言根據多於一個的宗量的類型對方法進行選擇。
C++和Java均是單分派語言,多分派語言的例子包括CLOS和Cecil。按照這樣的區分,Java就是動態的單分派語言,由於這種語言的動態分派僅僅會考慮到方法的接收者的類型,同時又是靜態的多分派語言,由於這種語言對重載方法的分派會考慮到方法的接收者的類型以及方法的全部參數的類型。
在一個支持動態單分派的語言裏面,有兩個條件決定了一個請求會調用哪個操做:一是請求的名字,二是接收者的真實類型。單分派限制了方法的選擇 過程,使得只有一個宗量能夠被考慮到,這個宗量一般就是方法的接收者。在Java語言裏面,若是一個操做是做用於某個類型不明的對象上面,那麼對這個對象 的真實類型測試僅會發生一次,這就是動態的單分派的特徵。
一個方法根據兩個宗量的類型來決定執行不一樣的代碼,這就是「雙重分派」。Java語言不支持動態的多分派,也就意味着Java不支持動態的雙分派。可是經過使用設計模式,也能夠在Java語言裏實現動態的雙重分派。
在Java中能夠經過兩次方法調用來達到兩次分派的目的。類圖以下所示:
在圖中有兩個對象,左邊的叫作West,右邊的叫作East。如今West對象首先調用East對象的goEast()方法,並將它本身傳入。 在East對象被調用時,當即根據傳入的參數知道了調用者是誰,因而反過來調用「調用者」對象的goWest()方法。經過兩次調用將程序控制權輪番交給 兩個對象,其時序圖以下所示:
這樣就出現了兩次方法調用,程序控制權被兩個對象像傳球同樣,首先由West對象傳給了East對象,而後又被返傳給了West對象。
可是僅僅返傳了一下球,並不能解決雙重分派的問題。關鍵是怎樣利用這兩次調用,以及Java語言的動態單分派功能,使得在這種傳球的過程當中,可以觸發兩次單分派。
動態單分派在Java語言中是在子類重寫父類的方法時發生的。換言之,West和East都必須分別置身於本身的類型等級結構中,以下圖所示:
West類
public abstract class West { public abstract void goWest1(SubEast1 east); public abstract void goWest2(SubEast2 east); }
SubWest1類
public class SubWest1 extends West{ @Override public void goWest1(SubEast1 east) { System.out.println("SubWest1 + " + east.myName1()); } @Override public void goWest2(SubEast2 east) { System.out.println("SubWest1 + " + east.myName2()); } }
SubWest2類
public class SubWest2 extends West{ @Override public void goWest1(SubEast1 east) { System.out.println("SubWest2 + " + east.myName1()); } @Override public void goWest2(SubEast2 east) { System.out.println("SubWest2 + " + east.myName2()); } }
East類
public abstract class East { public abstract void goEast(West west); }
SubEast1類
public class SubEast1 extends East{ @Override public void goEast(West west) { west.goWest1(this); } public String myName1(){ return "SubEast1"; } }
SubEast2類
public class SubEast2 extends East{ @Override public void goEast(West west) { west.goWest2(this); } public String myName2(){ return "SubEast2"; } }
客戶端類
public class Client { public static void main(String[] args) { //組合1 East east = new SubEast1(); West west = new SubWest1(); east.goEast(west); //組合2 east = new SubEast1(); west = new SubWest2(); east.goEast(west); } }
運行結果以下
SubWest1 + SubEast1
SubWest2 + SubEast1
系統運行時,會首先建立SubWest1和SubEast1對象,而後客戶端調用SubEast1的goEast()方法,並將SubWest1對象傳入。因爲SubEast1對象重寫了其超類East的goEast()方法,所以,這個時候就發生了一次動態的單分派。當SubEast1對象接到調用時,會從參數中獲得SubWest1對象,因此它就當即調用這個對象的goWest1()方法,並將本身傳入。因爲SubEast1對象有權選擇調用哪個對象,所以,在此時又進行一次動態的方法分派。
這個時候SubWest1對象就獲得了SubEast1對象。經過調用這個對象myName1()方法,就能夠打印出本身的名字和SubEast對象的名字,其時序圖以下所示:
因爲這兩個名字一個來自East等級結構,另外一個來自West等級結構中,所以,它們的組合式是動態決定的。這就是動態雙重分派的實現機制。
訪問者模式適用於數據結構相對未定的系統,它把數據結構和做用於結構上的操做之間的耦合解脫開,使得操做集合能夠相對自由地演化。訪問者模式的簡略圖以下所示:
數據結構的每個節點均可以接受一個訪問者的調用,此節點向訪問者對象傳入節點對象,而訪問者對象則反過來執行節點對象的操做。這樣的過程叫作「雙重分派」。節點調用訪問者,將它本身傳入,訪問者則將某算法針對此節點執行。訪問者模式的示意性類圖以下所示:
訪問者模式涉及到的角色以下:
● 抽象訪問者(Visitor)角色:聲明瞭一個或者多個方法操做,造成全部的具體訪問者角色必須實現的接口。
● 具體訪問者(ConcreteVisitor)角色:實現抽象訪問者所聲明的接口,也就是抽象訪問者所聲明的各個訪問操做。
● 抽象節點(Node)角色:聲明一個接受操做,接受一個訪問者對象做爲一個參數。
● 具體節點(ConcreteNode)角色:實現了抽象節點所規定的接受操做。
● 結構對象(ObjectStructure)角色:有以下的責任,能夠遍歷結構中的全部元素;若是須要,提供一個高層次的接口讓訪問者對象能夠訪問每個元素;若是須要,能夠設計成一個複合對象或者一個彙集,如List或Set。
能夠看到,抽象訪問者角色爲每個具體節點都準備了一個訪問操做。因爲有兩個節點,所以,對應就有兩個訪問操做。
public interface Visitor { /** * 對應於NodeA的訪問操做 */ public void visit(NodeA node); /** * 對應於NodeB的訪問操做 */ public void visit(NodeB node); }
具體訪問者VisitorA類
public class VisitorA implements Visitor { /** * 對應於NodeA的訪問操做 */ @Override public void visit(NodeA node) { System.out.println(node.operationA()); } /** * 對應於NodeB的訪問操做 */ @Override public void visit(NodeB node) { System.out.println(node.operationB()); } }
具體訪問者VisitorB類
public class VisitorB implements Visitor { /** * 對應於NodeA的訪問操做 */ @Override public void visit(NodeA node) { System.out.println(node.operationA()); } /** * 對應於NodeB的訪問操做 */ @Override public void visit(NodeB node) { System.out.println(node.operationB()); } }
抽象節點類
public abstract class Node { /** * 接受操做 */ public abstract void accept(Visitor visitor); }
具體節點類NodeA
public class NodeA extends Node{ /** * 接受操做 */ @Override public void accept(Visitor visitor) { visitor.visit(this); } /** * NodeA特有的方法 */ public String operationA(){ return "NodeA"; } }
具體節點類NodeB
public class NodeB extends Node{ /** * 接受方法 */ @Override public void accept(Visitor visitor) { visitor.visit(this); } /** * NodeB特有的方法 */ public String operationB(){ return "NodeB"; } }
結構對象角色類,這個結構對象角色持有一個彙集,並向外界提供add()方法做爲對彙集的管理操做。經過調用這個方法,能夠動態地增長一個新的節點。
public class ObjectStructure { private List<Node> nodes = new ArrayList<Node>(); /** * 執行方法操做 */ public void action(Visitor visitor){ for(Node node : nodes) { node.accept(visitor); } } /** * 添加一個新元素 */ public void add(Node node){ nodes.add(node); } }
客戶端類
public class Client { public static void main(String[] args) { //建立一個結構對象 ObjectStructure os = new ObjectStructure(); //給結構增長一個節點 os.add(new NodeA()); //給結構增長一個節點 os.add(new NodeB()); //建立一個訪問者 Visitor visitor = new VisitorA(); os.action(visitor); } }
雖然在這個示意性的實現裏並無出現一個複雜的具備多個樹枝節點的對象樹結構,可是,在實際系統中訪問者模式一般是用來處理複雜的對象樹結構的,並且訪問者模式能夠用來處理跨越多個等級結構的樹結構問題。這正是訪問者模式的功能強大之處。
首先,這個示意性的客戶端建立了一個結構對象,而後將一個新的NodeA對象和一個新的NodeB對象傳入。
其次,客戶端建立了一個VisitorA對象,並將此對象傳給結構對象。
而後,客戶端調用結構對象彙集管理方法,將NodeA和NodeB節點加入到結構對象中去。
最後,客戶端調用結構對象的行動方法action(),啓動訪問過程。
結構對象會遍歷它本身所保存的彙集中的全部節點,在本系統中就是節點NodeA和NodeB。首先NodeA會被訪問到,這個訪問是由如下的操做組成的:
(1)NodeA對象的接受方法accept()被調用,並將VisitorA對象自己傳入;
(2)NodeA對象反過來調用VisitorA對象的訪問方法,並將NodeA對象自己傳入;
(3)VisitorA對象調用NodeA對象的特有方法operationA()。
從而就完成了雙重分派過程,接着,NodeB會被訪問,這個訪問的過程和NodeA被訪問的過程是同樣的,這裏再也不敘述。
● 好的擴展性
可以在不修改對象結構中的元素的狀況下,爲對象結構中的元素添加新的功能。
● 好的複用性
能夠經過訪問者來定義整個對象結構通用的功能,從而提升複用程度。
● 分離無關行爲
能夠經過訪問者來分離無關的行爲,把相關的行爲封裝在一塊兒,構成一個訪問者,這樣每個訪問者的功能都比較單一。
● 對象結構變化很困難
不適用於對象結構中的類常常變化的狀況,由於對象結構發生了改變,訪問者的接口和訪問者的實現都要發生相應的改變,代價過高。
● 破壞封裝
訪問者模式一般須要對象結構開放內部數據給訪問者和ObjectStructrue,這破壞了對象的封裝性。