基本需求:
- 電腦需要鍵盤滑鼠等固定的元件組成
- 現在分為個人,組織等去買電腦,而同一種元件對不同的人(訪問者)做出不同的折扣,從而電腦的價格也不一樣
- 傳統的解決方法:在元件內部進行判斷訪問人的型別,從而進行不同打出不同的折扣
- 缺陷:如果訪問者的型別增加了,則需要改變元件內部的判斷程式碼,違反了開閉原則,訪問者的型別太多,判斷的程式碼也會很龐大
基本介紹:
-
在訪問者模式(Visitor)中,我們使用了一個訪問者類,它改變了元素類的執行演算法。通過這種方式,元素的執行演算法可以隨著訪問者改變而改變。這種型別的設計模式屬於行為型模式。根據模式,元素物件已接受訪問者物件,這樣訪問者物件就可以處理元素物件上的操作
-
也就是說,被訪問者可以根據不同的訪問者做出不同的響應
-
封裝一些作用於某種資料結構的各元素的操作,它可以在不改變資料結構的前提下定義作用於這些元素的新的操作
-
主要將資料結構與資料操作分離,解決 資料結構和操作耦合性問題
-
訪問者模式的基本工作原理是:在被訪問的類裡面加一個對外提供接待訪問者的介面
-
訪問者模式主要應用場景是:需要對一個物件結構中的物件進行很多不同操作(這些操作彼此沒有關聯),同時需要避免讓這些操作"汙染"這些物件的類,可以選用訪問者模式解決
-
UML類圖(原理)
-
說明
- Visitor是抽象訪問者,為該物件結構中的ConcreteElement的每一個類宣告一個visit操作
- ConcreteVisitor是一個具體的訪問值實現每個有Visitor宣告的操作,是每個操作實現的部分
- ObjectStructure能列舉它的元素,可以提供一個高層的介面,用來允許訪問者訪問元素
- Element定義一個accept 方法,接收一個訪問者物件
- ConcreteElement為具體元素,實現了accept方法
- 核心思想就是:不同訪問者通過accept訪問相同的被訪問者,被訪問者根據訪問者攜帶的方法做出具體的動作,從而達到相同的被訪問者再不同的場景下做出不同的響應,對被訪問者沒有任何影響,也就是雙分派
- 雙分派說明
- 雙分派是指不管類怎麼變化,我們都能找到期望的方法執行。雙分派意味著得到執行的操作取決於請求的種類和兩個接收者的型別
- 即首先在客戶端程式中,accept將具體狀態作為引數傳遞Element中,第一次分派
- 然後Element類呼叫作為引數的 "具體方法" 中方法operation1, 同時將自己(this)作為引數,完成第二次分派
- 所以,要求了visitor必須要有第二次分派使用的方法(感覺就像踢皮球,我呼叫你,最終呼叫的還是我的方法)
- 雙分派說明
-
UML類圖(案例)
-
說明
- ComputerPart是被訪問者的抽象父類,提供接受訪問者的方法,Keyboard和Mouse是其具體實現
- Visitor是訪問者父類,其中包含了訪問Keyboard和Mouse類的訪問回撥方法,所以這就要求了被訪問的子類是固定的,如果不固定,增加或者刪除,就要修改Visitor類中方法數量
- Computer充當了ObjectStructure,聚合了被訪問者並使用
-
程式碼實現
-
public abstract class ComputerPart { // 電腦元件抽象父類 也就是被訪問者 提供接收訪問者方法 protected String name; protected double price; public ComputerPart(String name, double price) { this.name = name; this.price = price; } // 接收訪問者,雙分派中第一次分派 public abstract double accept(Visitor visitor); } // 子類一 class Keyboard extends ComputerPart{ public Keyboard(String name, double price) { super(name, price); } @Override public double accept(Visitor visitor) { // 呼叫訪問者的訪問回撥方法,將自身再傳遞給訪問者,第二次分派,對不同的訪問者做出不同的響應 // 這個this是關鍵,也是重點 return visitor.visitKeyboard(this); } } // 子類二 class Mouse extends ComputerPart{ public Mouse(String name, double price) { super(name, price); } @Override public double accept(Visitor visitor) { // 呼叫訪問者的訪問回撥方法,將自身再傳遞給訪問者,第二次分派,對不同的訪問者做出不同的響應 // 這個this是關鍵,也是重點 return visitor.visitMouse(this); } }
-
public interface Visitor { // 訪問者抽象父介面 其中的訪問回撥方法(引數為具體的被訪問者)需要包含所有的訪問者實現 // 所以這就要求了被訪問的子類是固定的,如果不固定,增加或者刪除,就要修改Visitor類中方法數量 // 訪問Keyboard的訪問回撥 這樣不同的訪問者訪問同一個被訪問者得到的結果都不同 double visitKeyboard(Keyboard keyboard); // 訪問Mouse的訪問回撥 double visitMouse(Mouse mouse); } // 子類一 個人使用者 為方便都是九折 class PersonVisitor implements Visitor { @Override public double visitKeyboard(Keyboard keyboard) { return keyboard.price * 0.9; } @Override public double visitMouse(Mouse mouse) { return mouse.price * 0.9; } } // 子類二 群體使用者 為方便都是八折 class GroupVisitor implements Visitor { @Override public double visitKeyboard(Keyboard keyboard) { return keyboard.price * 0.8; } @Override public double visitMouse(Mouse mouse) { return mouse.price * 0.8; } }
-
public class Computer { // 此類作為 ObjectStructure,聚合了被訪問者並使用 private Keyboard keyboard; private Mouse mouse; public Computer(Keyboard keyboard, Mouse mouse) { this.keyboard = keyboard; this.mouse = mouse; } // 根據不同的訪問者獲取電腦的價格 public double getPrice(Visitor visitor) { // 被訪問者就收訪問者 return keyboard.accept(visitor) + mouse.accept(visitor); } }
-
public class Client { public static void main(String[] args) { // 建立滑鼠鍵盤和電腦 Keyboard keyboard = new Keyboard("keyboard", 100d); Mouse mouse = new Mouse("mouse", 100d); Computer computer = new Computer(keyboard, mouse); // 使用不同的訪問者獲取電腦的價格 PersonVisitor personVisitor = new PersonVisitor(); System.out.println("個人獲得的電腦價格是:" + computer.getPrice(personVisitor)); GroupVisitor groupVisitor = new GroupVisitor(); System.out.println("群體獲得的電腦價格是:" + computer.getPrice(groupVisitor)); // 如果還有其他的訪問者,直接增加其實現類即可,不用改動其他程式碼,被訪問者就可以做出不同的響應 } }
-
注意事項:
- 訪問者模式符合單一職責原則,讓程式有優秀的擴充套件性、靈活性非常高
- 訪問者模式可以對功能進行統一,可以做報表、ui、攔截器與過濾器,適用於資料結構相對穩定的系統
- 具體元素對訪問者公佈細節,也就是說訪問者關注了其他類的內部細節,這是迪米特法則所不建議的,這樣造成了具體元素變更比較困難
- 違反了依賴倒轉原則,訪問者依賴的是具體元素,而不是抽象元素
- 如果一個系統有比較穩定的資料結構,又有經常變化的功能需求,那麼訪問者模式是比較適合的