摘要:概述訪問者模式預留通路,回調實現。具體訪問者給出對每一個元素類訪問時所產生的具體行為。靜態分派通過方法重載支持靜態分派。上古上古第一代子孫上古第二代子孫顯然,類的方法是由三個方法重載而成的。因為訪問者模式使得算法操作增加變得容易。
訪問者模式(Visitor Pattern)屬于對象行為型模式的一種,表示一個作用于其對象結構中的各元素的操作,它使你可以在不改變各元素類的前提下定義作用于這些元素的新操作。概述
訪問者模式: 預留通路,回調實現。它的實現主要就是通過預先定義好調用的通路,在被訪問的對象上定義accept方法,在訪問者的對象上定義visit方法;然后在調用真正發生的時候,通過兩次分發的技術,利用預先定義好的通路,回調到訪問者具體的實現上。
主要解決
現已穩定的數據結構和易變的操作耦合問題,把數據結構和作用于結構上的操作解耦合,使得操作集合可相對自由地演化。
UML結構圖
模式結構
抽象訪問者(Visitor): 定義了對每一個元素(Element)訪問的行為,它的參數就是可以訪問的元素,它的方法個數理論上來講與元素個數(Element的實現類個數)是一樣的,從這點不難看出,訪問者模式要求元素類的個數不能改變(不能改變的意思是說,如果元素類的個數經常改變,則說明不適合使用訪問者模式)。
具體訪問者(ConcreteVisitor): 給出對每一個元素類訪問時所產生的具體行為。
抽象節點(Element): 定義了一個接受訪問者(accept)的方法,其意義是指,每一個元素都要可以被訪問者訪問。
具體節點(ConcreteElement): 提供接受訪問方法的具體實現,而這個具體的實現,通常情況下是使用訪問者提供的訪問該元素類的方法。
結構對象角色(ObjectStructure): 定義當中所提到的對象結構,對象結構是一個抽象表述,具體點可以理解為一個具有容器性質或者復合對象特性的類,它會含有一組元素(Element),并且可以迭代這些元素,供訪問者訪問。
案例場景:很多人都有養寵物的習慣,這里就以此為例
訪問者角色:給寵物喂食的人
具體訪問者角色:主人、其他人
抽象元素角色:動物抽象類
具體元素角色:寵物狗、寵物貓
結構對象角色:主人家
結構圖如下:
1.創建抽象訪問者接口
interface Person { void feed(Cat cat); void feed(Dog dog); }
2.創建不同的具體訪問者角色 -- 主人/其他人,同時實現 Person接口
class Owner implements Person { @Override public void feed(Cat cat) { System.out.println("主人喂食貓"); } @Override public void feed(Dog dog) { System.out.println("主人喂食狗"); } } class Someone implements Person { @Override public void feed(Cat cat) { System.out.println("其他人喂食貓"); } @Override public void feed(Dog dog) { System.out.println("其他人喂食狗"); } }
3.創建 抽象節點 -- 寵物
interface Animal { void accept(Person person); }
4.創建實現Animal接口的 具體節點(元素)
class Dog implements Animal { @Override public void accept(Person person) { person.feed(this); System.out.println("好好吃,汪汪汪!!!"); } } /** * 具體節點(元素)角色 -- 寵物貓 */ class Cat implements Animal { @Override public void accept(Person person) { person.feed(this); System.out.println("好好吃,喵喵喵!!!"); } }
5.創建實現Animal接口的 具體節點(元素)
class Home { private ListnodeList = new ArrayList<>(); void action(Person person) { for (Animal node : nodeList) { node.accept(person); } } /** * 添加操作 * * @param animal 動物 */ void add(Animal animal) { nodeList.add(animal); } }
6.創建客戶端,用于測試
public class Client { public static void main(String[] args) { Home home = new Home(); home.add(new Dog()); home.add(new Cat()); Owner owner = new Owner(); home.action(owner); Someone someone = new Someone(); home.action(someone); } }
7.運行結果
主人喂食狗 好好吃,汪汪汪!!! 主人喂食貓 好好吃,喵喵喵!!! 其他人喂食狗 好好吃,汪汪汪!!! 其他人喂食貓 好好吃,喵喵喵!!!分派
變量被聲明時的類型叫做變量的靜態類型(Static Type),有些人又把靜態類型叫做明顯類型(Apparent Type);而變量所引用的對象的真實類型又叫做變量的實際類型(Actual Type)。
比如:
Map map = null; map = new HashMap();
聲明了一個變量map,它的靜態類型(也叫明顯類型)是Map,而它的實際類型是HashMap。
根據對象的類型而對方法進行的選擇,就是分派(Dispatch),分派(Dispatch)又分為兩種,即靜態分派和動態分派。
靜態分派(Static Dispatch) 發生在編譯時期,分派根據靜態類型信息發生。靜態分派對于我們來說并不陌生,方法重載就是靜態分派。
動態分派(Dynamic Dispatch) 發生在運行時期,動態分派動態地置換掉某個方法。
靜態分派:Java通過方法重載支持靜態分派。
動態分派:Java通過方法的重寫支持動態分派。
動態分派通過方法的重寫支持動態分派。
class Dog { public void execute() { System.out.println("上古 Dog"); } } class DogBaby1 extends Dog { @Override public void execute() { System.out.println("上古Dog第一代子孫"); } } class DogBaby2 extends Dog { @Override public void execute() { System.out.println("上古Dog第二代子孫"); } } /** * @author Levin */ public class Client { public static void main(String[] args) { Dog baby1 = new DogBaby1(); baby1.execute(); Dog baby2 = new DogBaby2(); baby2.execute(); } }
變量baby1的靜態類型是Dog,而真實類型是DogBaby1。
execute()方法調用的是DogBaby1類的execute()方法,那么上面打印的就是 上古Dog第一代子孫;
變量baby2的靜態類型是Dog,而真實類型是DogBaby2。
execute()方法調用的是DogBaby2類的execute()方法,那么上面打印的就是 上古Dog第二代子孫;
所以,問題的核心就是Java編譯器在編譯時期并不總是知道哪些代碼會被執行,因為編譯器僅僅知道對象的靜態類型,而不知道對象的真實類型;而方法的調用則是根據對象的真實類型,而不是靜態類型。
靜態分派通過方法重載支持靜態分派。
class Dog { } class DogBaby1 extends Dog { } class DogBaby2 extends Dog { } class Execute { public void execute(Dog dog) { System.out.println("上古 Dog"); } public void execute(DogBaby1 baby1) { System.out.println("上古Dog第一代子孫"); } public void execute(DogBaby2 baby2) { System.out.println("上古Dog第二代子孫"); } } /** * @author Levin * @create 2017/12/19 0019 */ public class Client { public static void main(String[] args) { Dog dog = new Dog(); Dog baby1 = new DogBaby1(); Dog baby2 = new DogBaby2(); Execute exe = new Execute(); exe.execute(dog); exe.execute(baby1); exe.execute(baby2); } }
顯然,Execute類的excute()方法是由三個方法重載而成的。這三個方法分別接受狗(Dog)、狗baby1(DogBaby1)、狗baby2(DogBaby2)等類型的參數。
運行結果
上古 Dog 上古 Dog 上古 Dog
為什么呢?三次對execute()方法的調用傳入的是不同的參數,分別是dog、baby1、baby2。它們雖然具有不同的真實類型,但是它們的靜態類型都是一樣的,均是Dog類型。
重載方法的分派是根據靜態類型進行的,這個分派過程在編譯時期就完成了。
雙(重)分派Java是靜態多分派、動態單分派的語言。
Java不支持動態的雙分派。但是通過使用設計模式,也可以在Java語言里實現動態的雙重分派。
首先,什么是雙分派?還記得 設計模式解密(22)- 訪問者模式 中舉的例子嗎?
訪問者模式用到了一種雙分派的技術,所謂雙分派技術就是在選擇一個方法的時候,不僅僅要根據消息接收者(receiver)的運行時區別(Run time type),還要根據參數的運行時區別。
在訪問者模式中,客戶端將具體狀態當做參數傳遞給具體訪問者,這里完成第一次分派,然后具體訪問者作為參數的具體狀態中的方法,同時也將自己this作為參數傳遞進去,這里就完成了第二次分派。雙分派意味著得到的執行操作決定于請求的種類和接受者的類型。
雙分派的核心就是這個this對象。
說到這里,我們已經明白雙分派是怎么回事了,但是它有什么效果呢?就是可以實現方法的動態綁定,我們可以對上面的程序進行修改。
class Dog { public void accept(Execute exe) { exe.execute(this); } } class DogBaby1 extends Dog { @Override public void accept(Execute exe) { exe.execute(this); } } class DogBaby2 extends Dog { @Override public void accept(Execute exe) { exe.execute(this); } } class Execute { public void execute(Dog dog) { System.out.println("上古 Dog"); } public void execute(DogBaby1 baby1) { System.out.println("上古Dog第一代子孫"); } public void execute(DogBaby2 baby2) { System.out.println("上古Dog第二代子孫"); } } /** * 雙重分派 * * @author Levin * @create 2017/12/19 0019 */ public class Client { public static void main(String[] args) { Dog dog = new Dog(); Dog baby1 = new DogBaby1(); Dog baby2 = new DogBaby2(); Execute exe = new Execute(); dog.accept(exe); baby1.accept(exe); baby2.accept(exe); } }
運行結果
上古 Dog 上古Dog第一代子孫 上古Dog第二代子孫
從結果可以看出:雙分派實現動態綁定的本質,就是在重載方法委派的前面加上了繼承體系中覆蓋的環節,由于覆蓋是動態的,所以重載就是動態的了!!!
總結訪問者模式把數據結構和作用于結構上的操作解耦合,使得操作集合可相對自由地演化。訪問者模式適用于數據結構相對穩定算法又易變化的系統。因為訪問者模式使得算法操作增加變得容易。若系統數據結構對象易于變化,經常有新的數據對象增加進來,則不適合使用訪問者模式。
優點
擴展性好: 在不修改對象結構中的元素的情況下,為對象結構中的元素添加新的功能。
復用性好: 通過訪問者來定義整個對象結構通用的功能,從而提高復用程度。
分離無關行為: 通過訪問者來分離無關的行為,把相關的行為封裝在一起,構成一個訪問者,這樣每一個訪問者的功能都比較單一。
缺點
對象結構變化很困難: 不適用于對象結構中的類經常變化的情況,因為對象結構發生了改變,訪問者的接口和訪問者的實現都要發生相應的改變,代價太高。
破壞封裝: 訪問者模式通常需要對象結構開放內部數據給訪問者和ObjectStructrue,這破壞了對象的封裝性。
使用場景
數據結構穩定,作用于數據結構的操作經常變化的時候。
當一個數據結構中,一些元素類需要負責與其不相關的操作的時候,為了將這些操作分離出去,以減少這些元素類的職責時,可以使用訪問者模式。
有時在對數據結構上的元素進行操作的時候,需要區分具體的類型,這時使用訪問者模式可以針對不同的類型,在訪問者類中定義不同的操作,從而去除掉類型判斷。
說點什么參考文獻:http://www.cnblogs.com/JsonShare/p/7380772.html
全文代碼:https://gitee.com/battcn/design-pattern/tree/master/Chapter19/battcn-visitor
個人QQ:1837307557
battcn開源群(適合新手):391619659
微信公眾號:battcn(歡迎調戲)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/70843.html
摘要:組合模式的圖組成部分組合對象為組合中的對象聲明接口,在適當的情況下,實現所有類共有接口的默認行為,聲明用于訪問和管理其子組件的接口。組合模式對單個對象葉子對象和組合對象容器對象的使用具有一致性。 組合模式(Composite Pattern)屬于結構型模式的一種,組合多個對象形成樹形結構來表示部分 - 整體的結構層次,對單個對象(葉子對象)和組合對象(容器對象)的使用具有一致性 概述...
摘要:備忘錄模式常常與命令模式和迭代子模式一同使用。自述歷史所謂自述歷史模式實際上就是備忘錄模式的一個變種。在備忘錄模式中,發起人角色負責人角色和備忘錄角色都是獨立的角色。 備忘錄模式(Memento Pattern)屬于行為型模式的一種,在不破壞封裝特性的前提下,捕獲一個對象的內部狀態,并在該對象之外保存這個狀態。這樣就可以將該對象恢復到原先保存的狀態。 概述 備忘錄模式又叫做快照模式(...
摘要:迭代器模式屬于行為型模式的一種,提供一種方法訪問一個容器中各個元素,而又不需要暴露該對象的內部細節。迭代器模式把在元素之間游走的責任交給迭代器,而不是聚合對象。 迭代器模式(Iterator Pattern)屬于行為型模式的一種,提供一種方法訪問一個容器中各個元素,而又不需要暴露該對象的內部細節。 概述 迭代器模式聽起來可能感覺很陌生,但是實際上,迭代器模式是所有設計模式中最簡單也是...
摘要:懶漢非線程安全,需要用一定的風騷操作控制,裝逼失敗有可能導致看一周的海綿寶寶餓漢天生線程安全,的時候就已經實例化好,該操作過于風騷會造成資源浪費單例注冊表初始化的時候,默認單例用的就是該方式特點私有構造方法,只能有一個實例。 單例設計模式(Singleton Pattern)是最簡單且常見的設計模式之一,主要作用是提供一個全局訪問且只實例化一次的對象,避免多實例對象的情況下引起邏輯性錯...
摘要:適配器是將接口轉換為不同接口,而外觀模式是提供一個統一的接口來簡化接口。 外觀模式(Facade Pattern)屬于結構型模式的一種,為子系統中的一組接口提供一個統一的入口,它通過引入一個外觀角色來簡化客戶端與子系統之間的交互... 概述 外觀模式是一種使用頻率非常高的結構型設計模式,當你要為一個復雜子系統提供一個簡單接口時。子系統往往因為不斷演化而變得越來越復雜。大多數模式使用時...
閱讀 1971·2019-08-30 15:54
閱讀 3596·2019-08-29 13:07
閱讀 3124·2019-08-29 12:39
閱讀 1789·2019-08-26 12:13
閱讀 1547·2019-08-23 18:31
閱讀 2159·2019-08-23 18:05
閱讀 1844·2019-08-23 18:00
閱讀 1043·2019-08-23 17:15