【趣味設計模式系列】之【訪問者模式】

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章