1. 簡介
訪問者模式(Visitor Pattern):表示一個作用在某物件結構中的元素的操作,它可以在不改變類的元素的前提下,定義作用於這些元素的新操作。這是《設計模式-可複用物件導向軟體的基礎》中的定義。換句通俗的話,就是類的結構元素不變
,可以根據訪問者重新定義元素的操作
。
2. 示例
2.1 水果套餐例子
假如有個水果套餐,是蘋果、香蕉、橘子的組合,套餐內的水果種類一般不改變,需要對該購買套餐的消費者實行優惠,個人總價打9折,公司團購打8折,要求在不改變原有套餐內部元素與內部方法的情況下,根據外部訪問者的變化,重新定義新的價格演算法,如圖所示
類圖如下:
FruitPackage水果介面,接受訪問者的方法accept,計算價格的方法getPrice
package com.wzj.visitor.example1;
/**
* @Author: wzj
* @Date: 2020/9/2 20:16
* @Desc: 水果套餐
*/
interface FruitPackage {
void accept(Visitor v);
double getPrice();
}
套餐內的每個元素蘋果、橘子、香蕉分別實現水果套餐介面FruitPackage,內部accept方法各種去訪問對應的元素,並把當前元素的例項this傳進去,Apple類
package com.wzj.visitor.example1;
/**
* @Author: wzj
* @Date: 2020/9/2 20:16
* @Desc: 蘋果
*/
public class Apple implements FruitPackage {
@Override
public void accept(Visitor visitor) {
visitor.visitApple(this);
}
@Override
public double getPrice() {
return 30;
}
}
Orange類
package com.wzj.visitor.example1;
import com.wzj.proxy.v8.Discount;
/**
* @Author: wzj
* @Date: 2020/8/4 20:45
* @Desc: 橘子
*/
public class Orange implements FruitPackage {
@Override
public void accept(Visitor visitor) {
visitor.visitOrange(this);
}
@Override
public double getPrice() {
return 50;
}
}
Banana類
package com.wzj.visitor.example1;
/**
* @Author: wzj
* @Date: 2020/9/2 20:16
* @Desc: 香蕉
*/
public class Banana implements FruitPackage {
@Override
public void accept(Visitor visitor) {
visitor.visitBanana(this);
}
@Override
public double getPrice() {
return 40;
}
}
定義訪問者介面,內部依次定義訪問每個元素的方法
package com.wzj.visitor.example1;
/**
* @Author: wzj
* @Date: 2020/9/2 20:14
* @Desc: 訪問者介面
*/
public interface Visitor {
void visitApple(Apple apple);
void visitOrange(Orange orange);
void visitBanana(Banana banana);
}
個人訪問者PersonelVisitor類
package com.wzj.visitor.example1;
/**
* @Author: wzj
* @Date: 2020/9/2 20:40
* @Desc: 個人訪問者--一律9折
*/
public class PersonelVisitor implements Visitor{
double totalPrice = 0.0;
@Override
public void visitApple(Apple apple) {
totalPrice += apple.getPrice() * 0.9;
}
@Override
public void visitOrange(Orange orange) {
totalPrice += orange.getPrice() * 0.9;
}
@Override
public void visitBanana(Banana banana) {
totalPrice += banana.getPrice() * 0.9;
}
}
團購訪問者GroupVisitor類
package com.wzj.visitor.example1;
/**
* @Author: wzj
* @Date: 2020/9/2 20:41
* @Desc: 團購訪問者--一律8折
*/
public class GroupVisitor implements Visitor{
double totalPrice = 0.0;
@Override
public void visitApple(Apple apple) {
totalPrice += apple.getPrice() * 0.8;
}
@Override
public void visitOrange(Orange orange) {
totalPrice += orange.getPrice() * 0.8;
}
@Override
public void visitBanana(Banana banana) {
totalPrice += banana.getPrice() * 0.8;
}
}
具體水果套餐類ConcretePackage,把包含三個水果的元素組裝起來
package com.wzj.visitor.example1;
/**
* @Author: wzj
* @Date: 2020/9/2 20:51
* @Desc: 具體套餐,蘋果、香蕉、橘子
*/
public class ConcretePackage implements FruitPackage{
Apple apple;
Orange orange;
Banana banana;
public ConcretePackage(Apple apple, Orange orange, Banana banana) {
this.apple = apple;
this.orange = orange;
this.banana = banana;
}
public void accept(Visitor visitor) {
this.apple.accept(visitor);
this.orange.accept(visitor);
this.banana.accept(visitor);
}
@Override
public double getPrice() {
return apple.getPrice() + orange.getPrice() + banana.getPrice();
}
}
客戶端類Client
package com.wzj.visitor.example1;
import org.aspectj.weaver.ast.Or;
/**
* @Author: wzj
* @Date: 2020/9/2 20:57
* @Desc:
*/
public class Client {
public static void main(String[] args) {
Apple apple = new Apple();
Orange orange = new Orange();
Banana banana = new Banana();
//個人套餐價格
PersonelVisitor p = new PersonelVisitor();
new ConcretePackage(apple, orange, banana).accept(p);
System.out.println("個人套餐價格:" + p.totalPrice);
//公司套餐價格
GroupVisitor g = new GroupVisitor();
new ConcretePackage(apple, orange, banana).accept(g);
System.out.println("公司套餐價格:" + g.totalPrice);
}
}
最後執行結果
個人套餐價格:108.0
公司套餐價格:96.0
2.2 桌上型電腦組裝例子
假如有個桌上型電腦組裝,為簡化起見,是組裝元素由固定的三部分組成,CPU、記憶體條、主機板,現對個人來訪者總價打9折,公司團購來訪者總價打8折,要求在不改變原有套餐內部元素與內部方法的情況下,根據外部訪問者的變化,重新定義新的價格演算法,如圖所示
類圖設計
電腦部件類ComputerPart
package com.wzj.visitor.example2;
/**
* @Author: wzj
* @Date: 2020/9/2 21:15
* @Desc: 電腦部件
*/
public interface ComputerPart {
void accept(Visitor v);
double getPrice();
}
CPU類、Memory類、Board類如下:
package com.wzj.visitor.example2;
/**
* @Author: wzj
* @Date: 2020/9/2 21:13
* @Desc: CPU
*/
public class CPU implements ComputerPart{
@Override
public void accept(Visitor v) {
v.visitCpu(this);
}
@Override
public double getPrice() {
return 1000;
}
}
package com.wzj.visitor.example2;
/**
* @Author: wzj
* @Date: 2020/9/2 21:13
* @Desc: 記憶體條
*/
public class Memory implements ComputerPart{
@Override
public void accept(Visitor v) {
v.visitMemory(this);
}
@Override
public double getPrice() {
return 500;
}
}
package com.wzj.visitor.example2;
/**
* @Author: wzj
* @Date: 2020/9/2 21:13
* @Desc: CPU
*/
public class Board implements ComputerPart{
@Override
public void accept(Visitor v) {
v.visitBoard(this);
}
@Override
public double getPrice() {
return 800;
}
}
個人訪問者PersonelVisitor類
package com.wzj.visitor.example2;
/**
* @Author: wzj
* @Date: 2020/9/2 21:21
* @Desc: 個人購買9折
*/
public class PersonelVisitor implements Visitor {
double totalPrice = 0.0;
@Override
public void visitCpu(CPU cpu) {
totalPrice += cpu.getPrice() * 0.9;
}
@Override
public void visitMemory(Memory memory) {
totalPrice += memory.getPrice() * 0.9;
}
@Override
public void visitBoard(Board board) {
totalPrice += board.getPrice() * 0.9;
}
}
團購訪問者GroupVisitor類
package com.wzj.visitor.example2;
/**
* @Author: wzj
* @Date: 2020/9/2 21:21
* @Desc: 公司團購8折
*/
public class GroupVisitor implements Visitor {
double totalPrice = 0.0;
@Override
public void visitCpu(CPU cpu) {
totalPrice += cpu.getPrice() * 0.8;
}
@Override
public void visitMemory(Memory memory) {
totalPrice += memory.getPrice() * 0.8;
}
@Override
public void visitBoard(Board board) {
totalPrice += board.getPrice() * 0.8;
}
}
電腦類Computer類
package com.wzj.visitor.example2;
/**
* @Author: wzj
* @Date: 2020/9/2 21:24
* @Desc: 電腦
*/
public class Computer {
CPU cpu;
Memory memory;
Board board;
public Computer(CPU cpu, Memory memory, Board board) {
this.cpu = cpu;
this.memory = memory;
this.board = board;
}
public void acccept(Visitor v) {
this.cpu.accept(v);
this.memory.accept(v);
this.board.accept(v);
}
}
客戶端Client類
package com.wzj.visitor.example2;
/**
* @Author: wzj
* @Date: 2020/9/2 21:24
* @Desc:
*/
public class Client {
public static void main(String[] args) {
CPU cpu = new CPU();
Memory memory = new Memory();
Board board = new Board();
PersonelVisitor p = new PersonelVisitor();
new Computer(cpu, memory, board).acccept(p);
System.out.println("個人套餐價格:" + p.totalPrice);
GroupVisitor g = new GroupVisitor();
new Computer(cpu, memory, board).acccept(g);
System.out.println("公司套餐價格:" + g.totalPrice);
}
}
結果:
個人套餐價格:2070.0
公司套餐價格:1840.0
3. 應用場景分析
訪問者模式一般用在特定的場景中,在經典的四人幫寫的【設計模式】一書中,舉了編譯器的例子,如果需要將源程式表示一個抽象語法樹
,編譯器需要對抽象語法樹實施某些操作,如型別檢查、程式碼優化、優化格式列印,大多數操作對於不同節點進行不同處理,但是對於編譯器來說,節點類的集合對於給定的語言,內部結構很少變化,如下圖
編譯器對使用訪問者對程式進行型別檢查,它將建立一個TypeCheckingVisitor物件,並以這個物件為傳入引數,在抽象語法樹上呼叫accept方法,每一個節點在實現accept方法時會回撥訪問者:一個賦值節點AssignmentNode物件會回撥visitAssignment(this)方法,一個變數引用節點VariableRefNode物件會呼叫visitVariableRef(this)方法。
筆者在【趣味設計模式系列】之【代理模式4--ASM框架解析】裡面分析的ASM框架也是運用的訪問者模式,幾個核心類的關係如下圖
成員變數節點FieldNode類、方法節點MethodNode類,擁有接收訪問者的方法accept,通過傳入的具體的訪問者ClassAdapter類或ClassWriter類,方法內部各自呼叫具體訪問者訪問自己部件的方法,對應途中的訪問屬性visitField,訪問方法visitMethod。
4. 總結
4.1 雙分派技術
講到訪問者模式,大部分書籍或者資料都會講到 Double Dispatch,中文翻譯為雙分派。為什麼支援雙分派的語言就不需要訪問者模式。
既然有 Double Dispatch,對應的就有 Single Dispatch。所謂 Single Dispatch,指的是執行哪個物件的方法,根據物件的執行時型別來決定;執行物件的哪個方法,根據方法引數的編譯時型別來決定。所謂 Double Dispatch,指的是執行哪個物件的方法,根據物件的執行時型別
來決定;執行物件的哪個方法,根據方法引數的執行時型別
來決定。
如何理解“Dispatch”這個單詞呢?在物件導向程式語言中,我們可以把方法呼叫理解為一種訊息傳遞,也就是“Dispatch”。一個物件呼叫另一個物件的方法,就相當於給它傳送一條訊息。這條訊息起碼要包含物件名、方法名、方法引數。如何理解“Single”“Double”這兩個單詞呢?“Single”“Double”指的是執行哪個物件的哪個方法,跟幾個因素的執行時型別有關。我們進一步解釋一下。Single Dispatch 之所以稱為“Single”,是因為執行哪個物件的哪個方法,只跟“物件”的執行時型別有關。Double Dispatch 之所以稱為“Double”,是因為執行哪個物件的哪個方法,跟“物件”和“方法引數”兩者的執行時型別有關。
Java 支援多型特性,程式碼可以在執行時獲得物件的實際型別(也就是前面提到的執行時型別),然後根據實際型別決定呼叫哪個方法。儘管 Java 支援函式過載,但 Java 設計的函式過載的語法規則是,並不是在執行時,根據傳遞進函式的引數的實際型別,來決定呼叫哪個過載函式,而是在編譯時,根據傳遞進函式的引數的宣告型別(也就是前面提到的編譯時型別),來決定呼叫哪個過載函式。也就是說,具體執行哪個物件的哪個方法,只跟物件的執行時型別有關,跟引數的執行時型別無關。所以,Java 語言只支援 Single Dispatch。
舉個例子來具體說明一下,程式碼如下所示:
public class ParentClass {
public void f() {
System.out.println("I am ParentClass's f().");
}
}
public class ChildClass extends ParentClass {
public void f() {
System.out.println("I am ChildClass's f().");
}
}
public class SingleDispatchClass {
public void polymorphismFunction(ParentClass p) {
p.f();
}
public void overloadFunction(ParentClass p) {
System.out.println("I am overloadFunction(ParentClass p).");
}
public void overloadFunction(ChildClass c) {
System.out.println("I am overloadFunction(ChildClass c).");
}
}
public class DemoMain {
public static void main(String[] args) {
SingleDispatchClass demo = new SingleDispatchClass();
ParentClass p = new ChildClass();
demo.polymorphismFunction(p);//執行哪個物件的方法,由物件的實際型別決定
demo.overloadFunction(p);//執行物件的哪個方法,由引數物件的宣告型別決定
}
}
//程式碼執行結果:
I am ChildClass's f().
I am overloadFunction(ParentClass p).
這也回答了為什麼支援 Double Dispatch 的語言不需要訪問者模式。
訪問者模式允許在不改變類的情況下,有效的增加新的操作,這是一種很著名的技術,意味著執行的操作取決於請求的種類與接受者型別,accept方法是一個雙分派操作, 取決於visitor型別與Node節點型別,使得訪問者可以對每一種型別的請求執行不用的操作。
4.2 優點
- 容易增加新的操作
如果有複雜物件結構,需要增加新的操作,只需要增加新的訪問者定義新操作即可。 - 集中相關操作分離無關操作
相關行為集中在訪問者中,無關行為被分離到各自訪問者的關子類中,所有與演算法相關的資料結構都被隱藏在訪問者中。
4.3 缺點
- 具體元素對訪問者公佈,破壞封裝;
- 訪問者依賴具體元素,而非依賴抽象,破壞了依賴倒置原則,導致具體元素的增加、刪除、修改比較困難。
綜上,訪問者模式一般都用在類似於編譯器
等比較窄卻很專業的場景中,如果自己非要使用,適合類的結構元素不變的情況下,需要重新定義元素操作。
附:githup原始碼下載地址:https://github.com/wuzhujun2006/design-patterns