生老病死乃常態,是我們每個人都逃脫不了的,所以進醫院就是一件再平常不過的事情了。在醫院看病,你首先的掛號,然后找到主治醫生,醫生呢?先給你稍微檢查下,然后就是各種處方單(什么驗血、CD、B超等等,太坑了。。。。),再然后就給你一個處方單要你去拿葯。拿葯我們可以分為兩步走,第一步,我們要去交錢,划價人員會根據你的處方單上面的葯進行划價,交錢。第二步,去葯房拿葯,葯房工作者同樣根據你的處方單給你相對應的葯。
這里我們就划價和拿葯兩個步驟進行討論。這里有三個類,處方單(葯)、划價人員、葯房工作者。同時划價人員和葯房工作者都各自有一個動作:划價、拿葯。這里進行最初步的設計如下:
划價人員public class Charge { public void action(){ public void action(){ if("A葯".equals(medicine)){ //A的價格 } if("B葯".equals(medicine)){ //B的價格 } if("C葯".equals(medicine)){ //C的價格 } if("D葯".equals(medicine)){ //D的價格 } if("E葯".equals(medicine)){ //E的價格 } ............ } } }
葯房工作者
public class WorkerOfPharmacy { public void action(){ if("A葯".equals(medicine)){ //給你A葯 } if("B葯".equals(medicine)){ //給你B葯 } if("C葯".equals(medicine)){ //給你C葯 } if("D葯".equals(medicine)){ //給你D葯 } if("E葯".equals(medicine)){ //給你E葯 } ............ } }
看到這樣的代碼,我們第一個想法就是,這TMD太亂來了吧,這么多的if…else,誰看了不頭暈,而且我們可以想象醫院里面的葯是那么多,而且隨時都會增加的,增加了葯就要改變划價人員和葯房工作者的代碼,這是我們最不希望改變的。那么有沒有辦法來解決呢?有,訪問者模式提供一中比較好的解決方案。
在我們實際的軟件開發過程中,有時候我們對同一個對象可能會有不同的處理,對相同元素對象也可能存在不同的操作方式,如處方單,划價人員要根據它來划價,葯房工作者要根據它來給葯。而且可能會隨時增加新的操作,如醫院增加新的葯物。但是這里有兩個元素是保持不變的,或者說很少變:划價人員和葯房工作中,變的只不過是他們的操作。所以我們想如果能夠將他們的操作抽象化就好了。這里訪問者模式就是一個值得考慮的解決方案了。
訪問者模式的目的是封裝一些施加於某種數據結構元素之上的操作,一旦這些操作需要修改的話,接受這個操作的數據結構可以保持不變。為不同類型的元素提供多種訪問操作方式,且可以在不修改原有系統的情況下增加新的操作方式,這就是訪問者模式的模式動機。
一、模式定義
訪問者模式即表示一個作用於某對象結構中的各元素的操作,它使我們可以在不改變各元素的類的前提下定義作用於這些元素的新操作。
首先我們要明確一點就是訪問者模式適用於數據結構相對穩定的系統。它是將數據的操作與數據結構進行分離了,如果某個系統的數據結構相對穩定,但是操作算法易於變化的話,就比較適用適用訪問者模式,因為訪問者模式使得算法操作的增加變得比較簡單了。
二、模式結構
下圖是訪問者模式的UML結構圖:
訪問者模式主要包含如下幾個角色:
Vistor: 抽象訪問者。為該對象結構中的ConcreteElement的每一個類聲明的一個操作。
ConcreteVisitor: 具體訪問者。實現Visitor申明的每一個操作,每一個操作實現算法的一部分。
Element: 抽象元素。定義一個Accept操作,它以一個訪問者為參數。
ConcreteElement: 具體元素 。實現Accept操作。
ObjectStructure: 對象結構。能夠枚舉它的元素,可以提供一個高層的接口來允許訪問者訪問它的元素。
在訪問者模式中對象結構存儲了不同類型的對象,以便不同的訪問者來訪問。從上面的UML結構圖中我們可以看出,訪問者模式主要分為兩個層次結構,一個是訪問者層次結構,提供了抽象訪問者和具體訪問者,主要用於什么一些操作。一個是元素層次結構,提供了抽象元素和具體元素,主要用於聲明Accept操作。
在訪問者模式中相同的訪問者可以以不同的方式訪問不同的元素,所以在訪問者模式中增加新的訪問者無需修改現有代碼,可擴展行強。
同時在訪問者模式用到了一種雙分派的技術,所謂雙分派技術就是在選擇一個方法的時候,不僅僅要根據消息接收者(receiver)的運行時區別(Run time type),還要根據參數的運行時區別。在訪問者模式中,客戶端將具體狀態當做參數傳遞給具體訪問者,這里完成第一次分派,然后具體訪問者作為參數的“具體狀態”中的方法,同時也將自己this作為參數傳遞進去,這里就完成了第二次分派。雙分派意味着得到的執行操作決定於請求的種類和接受者的類型。
三、模式實現
同樣以上面在醫院付費、取葯為實例。在這個實例中划價員和葯房工作者作為訪問者,葯品作為訪問元素、處方單作為對象結構,所以整個UML結構圖如下:
抽象訪問者:Visitor.java
public abstract class Visitor { protected String name; public void setName(String name) { this.name = name; } public abstract void visitor(MedicineA a); public abstract void visitor(MedicineB b); }
具體訪問者:划價員、Charger.java
public class Charger extends Visitor{ public void visitor(MedicineA a) { System.out.println("划價員:" + name +"給葯" + a.getName() +"划價:" + a.getPrice()); } public void visitor(MedicineB b) { System.out.println("划價員:" + name +"給葯" + b.getName() +"划價:" + b.getPrice()); } }
具體訪問者:葯房工作者、WorkerOfPharmacy.java
public class WorkerOfPharmacy extends Visitor{ public void visitor(MedicineA a) { System.out.println("葯房工作者:" + name + "拿葯 :" + a.getName()); } public void visitor(MedicineB b) { System.out.println("葯房工作者:" + name + "拿葯 :" + b.getName()); } }
抽象元素:Medicine.java
public abstract class Medicine { protected String name; protected double price; public Medicine (String name,double price){ this.name = name; this.price = price; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } public abstract void accept(Visitor visitor); }
具體元素:MedicineA.java
public class MedicineA extends Medicine{ public MedicineA(String name, double price) { super(name, price); } public void accept(Visitor visitor) { visitor.visitor(this); } }
具體元素:MedicineB.java
public class MedicineB extends Medicine{ public MedicineB(String name, double price) { super(name, price); } public void accept(Visitor visitor) { visitor.visitor(this); } }
public class Presciption { List<Medicine> list = new ArrayList<Medicine>(); public void accept(Visitor visitor){ Iterator<Medicine> iterator = list.iterator(); while (iterator.hasNext()) { iterator.next().accept(visitor); } } public void addMedicine(Medicine medicine){ list.add(medicine); } public void removeMedicien(Medicine medicine){ list.remove(medicine); } }
public class Client { public static void main(String[] args) { Medicine a = new MedicineA("板藍根", 11.0); Medicine b = new MedicineB("感康", 14.3); Presciption presciption = new Presciption(); presciption.addMedicine(a); presciption.addMedicine(b); Visitor charger = new Charger(); charger.setName("張三"); Visitor workerOfPharmacy = new WorkerOfPharmacy(); workerOfPharmacy.setName("李四"); presciption.accept(charger); System.out.println("-------------------------------------"); presciption.accept(workerOfPharmacy); } }
運行結果
四、模式優缺點
優點
1、使得新增新的訪問操作變得更加簡單。
2、能夠使得用戶在不修改現有類的層次結構下,定義該類層次結構的操作。
3、將有關元素對象的訪問行為集中到一個訪問者對象中,而不是分散搞一個個的元素類中。
缺點
1、增加新的元素類很困難。在訪問者模式中,每增加一個新的元素類都意味着要在抽象訪問者角色中增加一個新的抽象操作,並在每一個具體訪問者類中增加相應的具體操作,違背了“開閉原則”的要求。
2、破壞封裝。當采用訪問者模式的時候,就會打破組合類的封裝。
3、比較難理解。貌似是最難的設計模式了。
五、模式適用場景
1、對象結構中對象對應的類很少改變,但經常需要在此對象結構上定義新的操作。
2、需要對一個對象結構中的對象進行很多不同的並且不相關的操作,而需要避免讓這些操作“污染”這些對象的類,也不希望在增加新操作時修改這些類。
六、模式總結
1、訪問者模式封裝了對象結構元素之上的操作,使得新增元素的操作變得非常簡單。所以它比較適用於那么對象結構很少變化的類。
2、訪問者模式中對象結構存儲了不同類型的元素對象,以供不同訪問者訪問。