標籤:運行 它的 xtend abstract dispatch strong [] nts ret
Java 設計模式系列(二三)訪問者模式(Vistor)
訪問者模式是對象的行為模式。訪問者模式的目的是封裝一些施加於某種資料結構元素之上的操作。一旦這些操作需要修改的話,接受這個操作的資料結構則可以保持不變。
一、訪問者模式結構
訪問者模式適用於資料結構相對未定的系統,它把資料結構和作用於結構上的操作之間的耦合解脫開,使得操作集合可以相對自由地演化。
資料結構的每一個節點都可以接受一個訪問者的調用,此節點向訪問者對象傳入節點對象,而訪問者對象則反過來執行節點對象的操作。這樣的過程叫做“雙重指派”。節點調用訪問者,將它自己傳入,訪問者則將某演算法針對此節點執行。訪問者模式的示意性類圖如下所示:
訪問者模式涉及到的角色如下:
-
抽象訪問者(Visitor)角色:聲明了一個或者多個方法操作,形成所有的具體訪問者角色必須實現的介面。
-
具體訪問者(ConcreteVisitor)角色:實現抽象訪問者所聲明的介面,也就是抽象訪問者所聲明的各個訪問操作。
-
抽象節點(Node)角色:聲明一個接受操作,接受一個訪問者對象作為一個參數。
-
具體節點(ConcreteNode)角色:實現了抽象節點所規定的接受操作。
-
結構對象(ObjectStructure)角色:有如下的責任,可以遍曆結構中的所有元素;如果需要,提供一個高層次的介面讓訪問者對象可以訪問每一個元素;如果需要,可以設計成一個綜合物件或者一個聚集,如 List 或 Set。
原始碼
(1) Vistor
可以看到,抽象訪問者角色為每一個具體節點都準備了一個訪問操作。由於有兩個節點,因此,對應就有兩個訪問操作。
public interface Visitor { /** 對應於NodeA的訪問操作 */ public void visit(NodeA node); /** 對應於NodeB的訪問操作 */ public void visit(NodeB node);}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()); }}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()); }}
(2) Node
public abstract class Node { /** 接受操作 */ public abstract void accept(Visitor visitor);}public class NodeA extends Node{ /** 接受操作 */ @Override public void accept(Visitor visitor) { visitor.visit(this); } /** NodeA特有的方法 */ public String operationA(){ return "NodeA"; }}public class NodeB extends Node{ /** 接受方法 */ @Override public void accept(Visitor visitor) { visitor.visit(this); } /** NodeB特有的方法 */ public String operationB(){ return "NodeB"; }}
(3) ObjectStructure
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); }}
(4) 測試
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); }}
二、指派的概念
變數被聲明時的類型叫做變數的靜態類型(Static Type),有些人又把靜態類型叫做明顯類型(Apparent Type);而變數所引用的對象的真實類型又叫做變數的實際類型(Actual Type)。比如:
List list = null;list = new ArrayList();
聲明了一個變數 list,它的靜態類型(也叫明顯類型)是 List,而它的實際類型是 ArrayList。
根據對象的類型而對方法進行的選擇,就是指派(Dispatch),指派(Dispatch)又分為兩種,即靜態指派和動態指派。
-
靜態指派(Static Dispatch)發生在編譯時間期,指派根據靜態類型資訊發生。靜態指派對於我們來說並不陌生,方法重載就是靜態指派。
-
動態指派(Dynamic Dispatch)發生在運行時期,動態指派動態地置換掉某個方法。
(1) 靜態指派
Java 通過方法重載支援靜態指派。
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 類型。
重載方法的指派是根據靜態類型進行的,這個指派過程在編譯時間期就完成了。
(2) 動態指派
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() 方法,列印的是“黑馬吃草”。
(3) 指派的類型
一個方法所屬的對象叫做方法的接收者,方法的接收者與方法的參數統稱做方法的宗量。比如下面例子中的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語言裡面,如果一個操作是作用於某個類型不明的對象上面,那麼對這個對象的真實類型測試僅會發生一次,這就是動態單指派的特徵。
(4) 雙重指派
一個方法根據兩個宗量的類型來決定執行不同的代碼,這就是“雙重指派”。Java 語言不支援動態多指派,也就意味著 Java 不支援動態雙指派。但是通過使用設計模式,也可以在 Java 語言裡實現動態雙重指派。
在 Java 中可以通過兩次方法調用來達到兩次指派的目的。
三、總結(1) 訪問者模式的優缺點
訪問者模式的優點
-
好的擴充性:能夠在不修改對象結構中的元素的情況下,為對象結構中的元素添加新的功能。
-
好的複用性:可以通過訪問者來定義整個對象結構通用的功能,從而提高複用程度。
-
分離無關行為:可以通過訪問者來分離無關的行為,把相關的行為封裝在一起,構成一個訪問者,這樣每一個訪問者的功能都比較單一。
訪問者模式的缺點
每天用心記錄一點點。內容也許不重要,但習慣很重要!
Java 設計模式系列(二三)訪問者模式(Vistor)