java設計模式-訪問者模式

定義

訪問者模式是對象的行為模式。訪問者模式的目的是封裝一些施加于某種數據結構元素之上的操作。一旦這些操作需要修改的話,接收這個操作的數據結構則可以保持不變。

分派的概念

變量被聲明時的類型叫做變量的靜態類型Static Type,有些人又把靜態類型叫做明顯類型Apparent Type;而變涼做引用的對象的真實類型又叫作變量的實際類型Actual Type。比如:

List list = null;
list = new ArrayList();

聲明了一個變量list,它的靜態類型(也成為明顯類型)是List,而它的實際類型是ArrayList

根據對象的類型而對方法進行的選擇,就是分派Dispatch,分派Dispatch有分為兩種:靜態分派動態分派

靜態分派(Static Dispatch)發生在編譯時期,分派根據靜態類型信息發生。靜態分派對于我們來說并不陌生,方法重載就是靜態分派。

動態分派(Dynamic Dispatch)發生在運行期間,動態分派動態的置換掉某個方法。

靜態分派

Java通過方法重載支持靜態分派。用墨子騎馬的故事作為例子,墨子可以騎白馬或者黑馬。墨子與白馬、黑馬和馬的類圖如下所示:

墨子騎馬類圖

馬的定義如下

public class Horse {
    public void eat() {
        System.out.println("馬吃草");
    }
}

白馬定義如下

public class WhiteHorse extends Horse {
    @Override
    public void eat() {
        System.out.println("白馬吃草");
    }
}

黑馬定義如下

public class BlackHorse extends Horse {
    @Override
    public void eat() {
        System.out.println("黑馬吃草");
    }
}

在這個系統中,墨子由Mozi類代表

public class MoZi {
    public void ride(Horse horse) {
        System.out.println("騎馬");
    }
    public void ride(WhiteHorse horse) {
        System.out.println("騎白馬");
    }
    public void ride(BlackHorse horse) {
        System.out.println("騎黑馬");
    }
    public static void main(String[] args) {
        Horse horse1 = new WhiteHorse();
        Horse horse2 = new BlackHorse();
        MoZi moZi = new MoZi();
        moZi.ride(horse1);
        moZi.ride(horse2);
    }
}

顯然,MoZi類的ride()方法是由三個方法重載而成的。這三個方法,分別接收的參數類型為馬Horse、白馬WhiteHorse、黑馬BlockHorse

然而在實際運行的時候,程序會打印出兩行相同的結果:“騎馬”。換言之,墨子所騎的都是馬。

為什么呢?兩次對ride()方法的調用傳入的是不同的參數,也就是horse1horse2。它們雖然具有不同的真實類型,但是它們的靜態類型都是一樣的,均為Horse類型。

重載方法的分派是根據靜態類型進行的,這個分派過程在編譯時期就完成了。

動態分派

我們關注到,在Horse類下定義了eat()方法,WhiteHorseBlockHorse方法重寫了這個方法,由此,當客戶端這樣調用時

public class Client {
    public static void main(String[] args) {
        Horse horse = new WhiteHorse();
        horse.eat();
    }
}

在這里,變量horse的靜態類型是Horse,而真實類型是WhiteHorse。如果上邊代碼中的eat()方法調用的是WhiteHorse類的eat()方法,那么打印的結果就是“白馬吃草”;相反,如果上面的eat()方法調用的是Horse類的eat()方法,那么打印的結果就是“馬吃草”。

所以,問題的核心就是Java編譯器在編譯時期并不總是知道哪些代碼會被執行,因為編譯器僅僅知道對象的靜態類型,而不知道對象的真實類型;而方法的調用則是根據對象的真實類型,而不是靜態類型。這樣以來,上邊的eat()方法調用的是WhiteHorse類的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語言中可以通過兩次調用方法來達到兩次分派的目的。類圖如下所示:

Java中雙重分派的實現方式
Java中雙重分派的實現方式

在圖中有兩個對象,左邊的叫做West,右邊的叫做East。現在West對象首先調用East對象的goEast()方法,并將它自己傳入。在East對象被調用時,立即根據傳入的參數知道了調用者是誰,于是反過來調用“調用者”對象的getWest()方法。通過兩次調用將程序控制權輪番交給兩個對象,其時序圖如下所示:

Java中雙重分派的實現時序圖

這樣就出現了兩次方法調用,程序控制權被兩個對象像傳球一樣,首先由West對象傳給了East對象,然后又被返傳給了West對象。

但是僅僅返傳了一下球,并不能解決雙重分派的問題。關鍵是怎么利用這兩次調用,以及Java語言的動態單分派功能,使得在這種傳球的過程中,能夠出發兩次單分派。

動態單分派在Java語言中是在子類重寫父類的方法時發生的。換言之,WestEast都必須分別置身于自己的類型等級結構中,如下圖所示:

示例代碼

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) {
        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

系統運行時,會首先創建SubWest1SubEast1對象,然后客戶端調用SubEast1goEast()方法,并將SubWest1對象傳入。由于SubEast1對象重寫了其超類East的goEast()方法,因此,這個時候就發生了一次動態的單分派。SubEast1對象接到調用時,會從參數中得到SubWest1對象,所以它就立即調用這個對象的goWest1方法,并將自己傳入。由于SubEast1對象有權選擇調用哪一個對象,因此,在此時又進行一次動態的方法分派。

這個時候SubWest1對象就得到SubEast1對象。通過調用這個對象的myName1()方法,就可以打印出自己的名字和SubEast對象的名字,其時序圖如下所示:

調用時序圖

由于這兩個名字一個來自East等級結構。另一個來自West等級結構中。因此,它們的組合是動態決定的。這就是動態雙重分派的實現機制。

訪問者模式的結構

訪問者模式適用于數據結構相對穩定的系統,它把數據結構和作用于結構上的操作之間的耦合解脫開,使得操作集合可以相對自由的演化。訪問者模式的簡略圖如下所示:

數據結構的每一個節點都可以接受一個訪問者的調用,此節點向訪問者對象傳入節點對象,而訪問者對象則反過來執行節點對象的操作。這樣的過程叫做“雙重分派”。節點調用訪問者,將它自己傳入,訪問者將某算法針對此節點執行。訪問者模式的示意性類圖如下所示:

訪問者模式的示意性類圖

訪問者模式涉及到的角色如下:

  • 抽象訪問者角色(Visitor):聲明了一個或者多個方法操作,行程所有的具體訪問者角色必須實現的接口。
  • 具體訪問者角色(ConcreteVisitor):實現抽象訪問者所聲明的接口,也就是抽象訪問者所聲明的各個訪問操作。
  • 抽象節點角色(Node):聲明一個接受操作,接受一個訪問者對象作為一個參數。
  • 具體節點角色(ConcreteNode):實現了抽象節點所規定的接受操作。
  • 結構對象角色(ObjectStructure):有如下的責任:可以遍歷結構中的所有元素;如果需要,提供一個高層次的接口讓訪問者對象可以訪問每一個元素;如果需要,可以設計成一個復合對象或者一個聚集,如ListSet

示例代碼

可以看出,抽象訪問者角色為每一個具體節點都準備了一個訪問操作。由于有兩個節點,因此,對應就有兩個訪問操作。

public interface Visitor {
    /**
     * 對于NodeA的訪問操作
     * @param nodeA
     */
    public void visit(NodeA nodeA);

    /**
     * 對于NodeB的訪問操作
     * @param nodeB
     */
    public void visit(NodeB nodeB);
}

具體訪問者VisitorA

public class VisitorA implements Visitor {
    @Override
    public void visit(NodeA nodeA) {
        System.out.println("VisitorA + " + nodeA.operationA());
    }

    @Override
    public void visit(NodeB nodeB) {
        System.out.println("VisitorA + " + nodeB.operationB());
    }
}

具體訪問者VisitorB

public class VisitorB implements Visitor {
    @Override
    public void visit(NodeA nodeA) {
        System.out.println("VisitorB + " + nodeA.operationA());
    }

    @Override
    public void visit(NodeB nodeB) {
        System.out.println("VisitorB + " + nodeB.operationB());
    }
}

抽象節點類

public abstract class Node {
    /**
     * 接受操作
     * @param visitor
     */
    public abstract void accept(Visitor visitor);
}

具體節點類NodeA

public class NodeA extends Node {
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    /**
     * NodeA特有的操作
     * @return
     */
    public String operationA() {
        return "NodeA";
    }
}

具體節點類NodeB

public class NodeB extends Node {
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    /**
     * NodeB特有的操作
     * @return
     */
    public String operationB() {
        return "NodeB";
    }
}

結構對象角色類,這個結構對象持有一個聚集,并向外界提供add()方法作為對聚集的管理操作。通過調用這個方法,可以動態的增加一個新的節點。

public class ObjectStructure {
    private List<Node> nodes = new ArrayList<>();

    /**
     * 執行方法操作
     * @param visitor
     */
    public void action(Visitor visitor) {
        for (Node node :
                nodes) {
            node.accept(visitor);
        }
    }

    /**
     * 添加一個新元素
     * @param node
     */
    public void add(Node node) {
        nodes.add(node);
    }
}

客戶端類

public class Client {
    public static void main(String[] args) {
        //創建一個結構對象
        ObjectStructure objectStructure = new ObjectStructure();
        //給結構增加節點
        objectStructure.add(new NodeA());
        objectStructure.add(new NodeB());
        //創建一個訪問者
        Visitor visitor = new VisitorA();
        objectStructure.action(visitor);
    }
}

雖然在這個示意性的實現里并沒有出現一個復雜的具有多個樹枝節點的對象樹結構,但是,在實際系統中訪問者模式通常是用來處理復雜的對象樹結構的,而且訪問者模式可以用來處理跨多個等級結構的樹結構問題。這正是訪問者模式的功能強大之處。

準備過程時序圖

首先,這個示意性的客戶端創建了一個結構對象,然后將一個新的NodeA對象和一個新的NodeB對象傳入。

其次,客戶端創建了一個VisitorA對象,并將此對象傳給結構對象。

然后,客戶端調用結構對象聚集管理方法,將NodeANodeB節點加入到結構對象中去。

最后,客戶端調用結構對象的行為方法action()方法,啟動訪問過程。

準備過程時序圖

訪問過程時序圖

訪問過程時序圖

結構對象會遍歷它自己所保存的聚集中的所有節點,在本系統中就是節點NodeANodeB。首先NodeA會被訪問到,這個訪問是由一下的操作組成的:

  1. NodeA對象的接受方法accept()被調用,并將VisitorA對象本身傳入;
  2. NodeA對象反過來調用VisitorA對象的訪問方法,并將NodeA對象本身傳入;
  3. VisitorA對象調用NodeA對象的特有方法operationA()

從而就完成了雙重分派過程,接著,NodeB會被訪問,這個訪問的過程跟NodeA被訪問的過程是一樣的。

訪問者模式的優點

  • 好的擴展性
    能夠在不修改對象結構中的元素的情況下,為對象結構中的元素添加新的功能。
  • 好的復用性
    可以通過訪問者定義整個結果對象通用的功能,從而提高復用程度。
  • 分離無關行為
    可以通過訪問者來分離無關的行為,把相關的行為封裝在一起,構成一個訪問者,這樣沒個訪問者的功能都比較單一。

訪問者模式的缺點

  • 對象結構變化很困難
    不適用于對象結構中的類經常變化的情況,因為對象結構發生了改變,訪問者的接口和訪問者的實現都要發生相應的改變,代價太高。
  • 破壞封裝
    訪問者模式通常需要對象結構開放內部數據給訪問者和ObjectStructure,這破壞了對象的封裝性

參考

《JAVA與模式》之訪問者模式

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容