設計模式學習筆記(二十一)訪問者模式及其實現

歸斯君發表於2022-04-11

訪問者模式(Visitor Pattern)指將作用域某種資料結構中的各元素的操作分離出來封裝成獨立的類,使其在不改變資料結構的前提下可以新增作用於這些元素的新的操作。借用《Java設計模式》中的例子說明:在醫院醫生開具藥單後,劃價人員拿到藥單後會根據藥單上的藥品名稱和數量計算總價,而藥房工作人員則根據藥品名稱和數量準備藥品。如下圖所示:

image-20220411081209135

那麼藥品處方可以看成是一個藥品資訊的集合,裡面包含了一種或多種不同型別的藥品資訊,不同型別的工作人員在操作統一藥品資訊集合時將提供不同的處理方式,而且可能還會增加新型別的工作人員來操作處方單。這就是訪問者模式的典型應用場景。

一、訪問者模式介紹

1.1 訪問者模式的結構

訪問者模式是一種較為複雜的行為型模式,它包含訪問者(Visitor)和被訪問元素(Element)兩個主要組成部分。下面就來看看訪問者模式的具體結構:

image-20220411095609773

  • Visitor:抽象訪問者,為物件結構中的每個具體元素類宣告一個訪問操作
  • ConcreteVisitor1、ConcreteVisitor2:具體訪問者,實現抽象訪問者宣告的操作
  • Element:抽象元素,定義一個accept()方法
  • ConcreteElement1、ConcreteElement2:具體元素,實現抽象元素中的accept()方法,在accept()方法中呼叫訪問者的訪問方法以便完成對一個元素的操作。
  • ObjectStructure:物件結構,它是一個元素的集合,用於存放元素物件,並且提供了遍歷其內部元素的方法。
  • Client:客戶端

1.2 訪問者模式的實現

根據上面的類圖,首先是抽象訪問者,為每一種具體型別物件都會提供一個訪問方法:

public interface Visitor {

    void visit(ConcreteElementA elementA);
    void visit(ConcreteElementB elementB);
}

接下來是具體訪問者,實現抽象訪問者的宣告方法

public class ConcreteVisitor1 implements Visitor{

    @Override
    public void visit(ConcreteElementA elementA) {
        System.out.println("ConcreteVisitor1 訪問 ConcreteElementA: " + elementA.operationA());
    }

    @Override
    public void visit(ConcreteElementB elementB) {
        System.out.println("ConcreteVisitor1 訪問 ConcreteElementB: " + elementB.operationB());
    }
}
public class ConcreteVisitor2 implements Visitor {

    @Override
    public void visit(ConcreteElementA elementA) {
        System.out.println("ConcreteVisitor2 訪問 ConcreteElementA: " + elementA.operationA());
    }

    @Override
    public void visit(ConcreteElementB elementB) {
        System.out.println("ConcreteVisitor2 訪問 ConcreteElementB: " + elementB.operationB());
    }
}

然後是抽象元素介面,定義一個accept()方法,用於接受訪問者的訪問:

public interface Element {

    void accept(Visitor visitor);
}

下面是實現抽象元素介面的具體元素類,除了過載accept()方法,還實現對應的具體操作方法

public class ConcreteElementA implements Element{

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    public String operationA() {
        return "ConcreteElementA的操作方法";
    }
}
public class ConcreteElementB implements Element{

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    public String operationB() {
        return "ConcreteElementB的操作方法";
    }
}

最後是物件結構類,是一個對元素進行操作的容器,提供訪問者遍歷容器中所有元素的方法

public class ObjectStructure {

    private List<Element> elementList = new ArrayList<>();

    public void accept(Visitor visitor) {
        Iterator<Element> it = elementList.iterator();
        while(it.hasNext()) {
            it.next().accept(visitor);
        }
    }

    public void add(Element element) {
        elementList.add(element);
    }

    public void remove(Element element) {
        elementList.remove(element);
    }
}

客戶端測試類:

public class Client {
    public static void main(String[] args) {
        //將具體元素注入物件結構中
        ObjectStructure objectStructure = new ObjectStructure();
        objectStructure.add(new ConcreteElementA());
        objectStructure.add(new ConcreteElementB());
        //具體訪問者訪問具體元素
        objectStructure.accept(new ConcreteVisitor1());
        objectStructure.accept(new ConcreteVisitor2());

    }
}

測試結果:

ConcreteVisitor1 訪問 ConcreteElementA: ConcreteElementA的操作方法
ConcreteVisitor1 訪問 ConcreteElementB: ConcreteElementB的操作方法
ConcreteVisitor2 訪問 ConcreteElementA: ConcreteElementA的操作方法
ConcreteVisitor2 訪問 ConcreteElementB: ConcreteElementB的操作方法

二、訪問者模式的應用場景

在下面的情況可以考慮使用訪問者模式:

  • 一個物件結構中包含多個型別的物件,希望對這些物件實施一些依賴其具體型別的操作
  • 需要對一個物件結構中的物件進行很多不同的並且不相關的操作
  • 物件結構中物件對應的類很少改變,但經常需要在此物件結構上定義新的操作

三、訪問者模式實戰

本案例模擬學校中學生和老師對於不同使用者的訪問視角(案例來源於《重學Java設計模式》)

這個案例場景我們模擬校園中有學⽣和⽼師兩種身份的⽤戶,那麼對於家⻓和校⻓關⼼的⻆度來看,他 們的視⻆是不同的。家⻓更關⼼孩⼦的成績和⽼師的能⼒,校⻓更關⼼⽼師所在班級學⽣的⼈數和升學 率

從前面第一節的結構圖和實現程式碼就可以知道,訪問者模式的整體類結構相對複雜,下面就來看看該案例的核心邏輯實現:

  1. 需要建立使用者抽象類和抽象訪問方法,再由不同的使用者實現(相當於前面的元素),這裡的使用者指看老師和學生;
  2. 建立訪問者介面,用於不同人員的訪問操作,這裡的訪問者指校長和家長;
  3. 最終是對資料的看板建設,用於實現不同視角的訪問結果輸出(相當於前面的物件結構);

具體程式碼實現

  1. 使用者抽象類及具體實現類

先來看看使用者抽象類,類似於第一節中的抽象元素類

public abstract class User {

    /**姓名*/
    public String name;
    /**使用者的身份,包括學生和教師*/
    public String identity;
    /**所屬班級*/
    public String clazz;

    public User(String name, String identity, String clazz) {
        this.name = name;
        this.identity = identity;
        this.clazz = clazz;
    }

    public abstract void accept(Visitor visitor);
}

具體使用者類,包括學生和老師,每個具體使用者可以實現對應不同的方法,比如學生的排名,老師的升學率方法。

public class Student extends User{

    public Student(String name, String identity, String clazz) {
        super(name, identity, clazz);
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    public int ranking() {
        return (int) (Math.random()*100);
    }
}
public class Teacher extends User {

    public Teacher(String name, String identity, String clazz) {
        super(name, identity, clazz);
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    //升學率
    public double entranceRatio() {
        return BigDecimal.valueOf(Math.random() * 100).setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();
    }

}
  1. 抽象訪問者及具體實現類

先看看抽象訪問者介面:

public interface Visitor {

    void visit(Student student);

    void visit(Teacher teacher);
}

對應的具體訪問者實現,包括父母和校長類。不同訪問者的訪問角度也不相同,校長看重教師的升學率,父母看重學生的排名

public class Parent implements Visitor{

    private Logger logger = LoggerFactory.getLogger(Parent.class);

    @Override
    public void visit(Student student) {
        logger.info("學生資訊 姓名:{} 班級:{} 排名:{}", student.name, student.clazz, student.ranking());
    }

    @Override
    public void visit(Teacher teacher) {
        logger.info("老師資訊 姓名:{} 班級:{}", teacher.name, teacher.clazz);
    }
}
public class Principal implements Visitor{

    private Logger logger = LoggerFactory.getLogger(Principal.class);

    @Override
    public void visit(Student student) {
        logger.info("學生資訊 姓名: {} 班級:{}", student.name, student.clazz);
    }

    @Override
    public void visit(Teacher teacher) {
        logger.info("老師資訊 姓名: {} 班級:{} 升學率:{}", teacher.name, teacher.clazz, teacher.entranceRatio());
    }
}
  1. 資料看板

資料看板就類似於第一節中的物件結構(ObjectStructure),初始化具體使用者的資訊,展示訪問者的訪問角度資訊:

public class DataView {

    List<User> userList = new ArrayList<>();

    public DataView() {
        userList.add(new Student("Ethan", "普通班", "高一1班"));
        userList.add(new Student("Tom", "重點班", "高一2班"));
        userList.add(new Student("Peter", "重點班", "高一3班"));
        userList.add(new Teacher("張三", "普通班", "高一1班"));
        userList.add(new Teacher("李四", "重點班", "高一2班"));
        userList.add(new Teacher("王五", "重點班", "高一3班"));
    }

    public void show(Visitor visitor) {
        for (User user : userList) {
            user.accept(visitor);
        }
    }
}
  1. 測試類

對整個流程進行測試:

public class ApiTest {

    private Logger logger = LoggerFactory.getLogger(ApiTest.class);

    @Test
    public void test() {
        DataView dataView = new DataView();

        logger.info("家長視角訪問:");
        dataView.show(new Parent());

        logger.info("校長視角訪問:");
        dataView.show(new Principal());
    }
}

結果為:

12:27:18.983 [main] INFO  ApiTest - 家長視角訪問:
12:27:18.983 [main] INFO  visitor.Parent - 學生資訊 姓名:Ethan 班級:高一1班 排名:22
12:27:18.983 [main] INFO  visitor.Parent - 學生資訊 姓名:Tom 班級:高一2班 排名:2
12:27:18.983 [main] INFO  visitor.Parent - 學生資訊 姓名:Peter 班級:高一3班 排名:0
12:27:18.983 [main] INFO  visitor.Parent - 老師資訊 姓名:張三 班級:高一1班
12:27:18.983 [main] INFO  visitor.Parent - 老師資訊 姓名:李四 班級:高一2班
12:27:18.983 [main] INFO  visitor.Parent - 老師資訊 姓名:王五 班級:高一3班
12:27:18.983 [main] INFO  ApiTest - 校長視角訪問:
12:27:18.983 [main] INFO  visitor.Principal - 學生資訊 姓名: Ethan 班級:高一1班
12:27:18.983 [main] INFO  visitor.Principal - 學生資訊 姓名: Tom 班級:高一2班
12:27:18.983 [main] INFO  visitor.Principal - 學生資訊 姓名: Peter 班級:高一3班
12:27:18.998 [main] INFO  visitor.Principal - 老師資訊 姓名: 張三 班級:高一1班 升學率:39.85
12:27:18.998 [main] INFO  visitor.Principal - 老師資訊 姓名: 李四 班級:高一2班 升學率:88.14
12:27:18.998 [main] INFO  visitor.Principal - 老師資訊 姓名: 王五 班級:高一3班 升學率:44.65

參考資料

《Java設計模式》

《重學Java設計模式》

http://c.biancheng.net/view/1397.html

相關文章