JAVA設計形式之拜訪者形式詳解。本站提示廣大學習愛好者:(JAVA設計形式之拜訪者形式詳解)文章只能為提供參考,不一定能成為您想要的結果。以下是JAVA設計形式之拜訪者形式詳解正文
在閻宏博士的《JAVA與形式》一書中開首是如許描寫拜訪者(Visitor)形式的:
拜訪者形式是對象的行動形式。拜訪者形式的目標是封裝一些施加於某種數據構造元素之上的操作。一旦這些操作須要修正的話,接收這個操作的數據構造則可以堅持不變。
分配的概念
變量被聲明時的類型叫做變量的靜態類型(Static Type),有些人又把靜態類型叫做顯著類型(Apparent Type);而變量所援用的對象的真實類型又叫做變量的現實類型(Actual Type)。好比:
List list = null;
list = new ArrayList();
聲清楚明了一個變量list,它的靜態類型(也叫顯著類型)是List,而它的現實類型是ArrayList。
依據對象的類型而對辦法停止的選擇,就是分配(Dispatch),分配(Dispatch)又分為兩種,即靜態分配和靜態分配。
靜態分配(Static Dispatch)產生在編譯時代,分配依據靜態類型信息產生。靜態分配關於我們來講其實不生疏,辦法重載就是靜態分配。
靜態分配(Dynamic Dispatch)產生在運轉時代,靜態分配靜態地置換失落某個辦法。
靜態分配
Java經由過程辦法重載支撐靜態分配。用墨子騎馬的故事作為例子,墨子可以騎白馬或許黑馬。墨子與白馬、黑馬和馬的類圖以下所示:
在這個體系中,墨子由Mozi類代表
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,這損壞了對象的封裝性。