設計模式之訪問者模式詳解(Visitor Pattern)

最複雜的設計模式,並且使用頻率不高,《設計模式》的作者評價爲:大多情況下,你不需要使用訪問者模式,但是一旦需要使用它時,那就真的需要使用了。
訪問者模式是一種將數據操作和數據結構分離的設計模式。(覺得太抽象,可以看下面的例子)。

模式的定義與特點

訪問者(Visitor)模式的定義:將作用於某種數據結構中的各元素的操作分離出來封裝成獨立的類,使其在不改變數據結構的前提下可以添加作用於這些元素的新的操作,爲數據結構中的每個元素提供多種訪問方式。它將對數據的操作與數據結構進行分離,是行爲類模式中最複雜的一種模式。

訪問者(Visitor)模式是一種對象行爲型模式,其主要優點如下:

  1. 擴展性好。能夠在不修改對象結構中的元素的情況下,爲對象結構中的元素添加新的功能。
  2. 複用性好。可以通過訪問者來定義整個對象結構通用的功能,從而提高系統的複用程度。
  3. 靈活性好。訪問者模式將數據結構與作用於結構上的操作解耦,使得操作集合可相對自由地演化而不影響系統的數據結構。
  4. 符合單一職責原則。訪問者模式把相關的行爲封裝在一起,構成一個訪問者,使每一個訪問者的功能都比較單一。

訪問者(Visitor)模式的主要缺點如下。

  1. 增加新的元素類很困難。在訪問者模式中,每增加一個新的元素類,都要在每一個具體訪問者類中增加相應的具體操作,這違背了“開閉原則”。
  2. 破壞封裝。訪問者模式中具體元素對訪問者公佈細節,這破壞了對象的封裝性。
  3. 違反了依賴倒置原則。訪問者模式依賴了具體類,而沒有依賴抽象類。

模式的原理類圖與角色說明

訪問者模式的原理類圖

角色說明

  • 抽象訪問者(Visitor)角色:定義一個訪問具體元素的接口,爲每個具體元素類對應一個訪問操作 visit() ,該操作中的參數類型標識了被訪問的具體元素。
  • 具體訪問者(ConcreteVisitor)角色:實現抽象訪問者角色中聲明的各個訪問操作,確定訪問者訪問一個元素時該做什麼。
  • 抽象元素(Element)角色:聲明一個包含接受操作 accept() 的接口,被接受的訪問者對象作爲 accept() 方法的參數。
  • 具體元素(ConcreteElement)角色:實現抽象元素角色提供的 accept() 操作,其方法體通常都是 visitor.visit(this) ,另外具體元素中可能還包含本身業務邏輯的相關操作。
  • 對象結構(Object Structure)角色:是一個包含元素角色的容器,提供讓訪問者對象遍歷容器中的所有元素的方法,通常由 List、Set、Map 等聚合類實現。

模式的應用實例

年底,CEO和CTO開始評定員工一年的工作績效,員工分爲工程師和經理,CTO關注工程師的代碼量、經理的新產品數量;CEO關注的是工程師的KPI和經理的KPI以及新產品數量。

由於CEO和CTO對於不同員工的關注點是不一樣的,這就需要對不同員工類型進行不同的處理。訪問者模式此時可以派上用場了。

抽象元素(Element)角色
//員工基類
public abstract class Staff {
    public String name;
    public int kpi;

    public Staff(String name) {
        this.name = name;
        kpi = new Random().nextInt(10);
    }
    //核心方法,接受Visitor的訪問
    public abstract void accept(Visitor visitor);
}
具體元素(ConcreteElement)角色
//經理
public class Manager extends Staff {
    public Manager(String name) {
        super(name);
    }
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    // 一年做的產品數量
    public int getProducts() {
        return new Random().nextInt(10);
    }
}
//工程師
public class Engineer extends Staff {
    public Engineer(String name) {
        super(name);
    }
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    // 工程師一年的代碼數量
    public int getCodeLines() {
        return new Random().nextInt(10 * 10000);
    }
}

工程師是代碼數量,經理是產品數量,他們的職責不一樣,也就是因爲差異性,才使得訪問模式能夠發揮它的作用。Staff、Engineer、Manager 3個類型就是對象結構這些類型相對穩定,不會發生變化。

對象結構(Object Structure)角色

將這些員工添加到一個業務報表類中,公司高層可以通過該報表類的 showReport 方法查看所有員工的業績,具體代碼如下:

// 員工業務報表類
public class BusinessReport {

    private List<Staff> mStaffs = new LinkedList<>();

    public BusinessReport() {
        mStaffs.add(new Manager("經理-A"));
        mStaffs.add(new Engineer("工程師-A"));
        mStaffs.add(new Engineer("工程師-B"));
        mStaffs.add(new Engineer("工程師-C"));
        mStaffs.add(new Manager("經理-B"));
        mStaffs.add(new Engineer("工程師-D"));
    }

    /**
     * 爲訪問者展示報表
     * @param visitor 公司高層,如CEO、CTO
     */
    public void showReport(Visitor visitor) {
        for (Staff staff : mStaffs) {
            staff.accept(visitor);
        }
    }
}
抽象訪問者(Visitor)角色
public interface Visitor {
    // 訪問工程師類型
    void visit(Engineer engineer);

    // 訪問經理類型
    void visit(Manager manager);
}

該接口有兩個 visit 函數,參數分別是 Engineer、Manager,也就是說對於 Engineer、Manager 的訪問會調用兩個不同的方法,以此達成區別對待、差異化處理。具體實現類爲 CEOVisitor、CTOVisitor類

具體訪問者(ConcreteVisitor)角色
// CEO訪問者
public class CEOVisitor implements Visitor {
    @Override
    public void visit(Engineer engineer) {
        System.out.println("工程師: " + engineer.name + ", KPI: " + engineer.kpi);
    }

    @Override
    public void visit(Manager manager) {
        System.out.println("經理: " + manager.name + ", KPI: " + manager.kpi +
                ", 新產品數量: " + manager.getProducts());
    }
}
//CTO訪問者
public class CTOVisitor implements Visitor {
    @Override
    public void visit(Engineer engineer) {
        System.out.println("工程師: " + engineer.name + ", 代碼行數: " + engineer.getCodeLines());
    }

    @Override
    public void visit(Manager manager) {
        System.out.println("經理: " + manager.name + ", 產品數量: " + manager.getProducts());
    }
}

在CEO的訪問者中,CEO關注工程師的 KPI,經理的 KPI 和新產品數量,通過兩個 visitor 方法分別進行處理。

重載的 visit 方法會對元素進行不同的操作,而通過注入不同的 Visitor 又可以替換掉訪問者的具體實現,使得對元素的操作變得更靈活,可擴展性更高,同時也消除了類型轉換、if-else 等“醜陋”的代碼。

客戶端使用
public class Client {

    public static void main(String[] args) {
        // 構建報表
        BusinessReport report = new BusinessReport();
        System.out.println("=========== CEO看報表 ===========");
        report.showReport(new CEOVisitor());
        System.out.println("=========== CTO看報表 ===========");
        report.showReport(new CTOVisitor());
    }
}

具體輸出如下:

=========== CEO看報表 ===========
經理: 經理-A, KPI: 0, 新產品數量: 2
工程師: 工程師-A, KPI: 5
工程師: 工程師-B, KPI: 4
工程師: 工程師-C, KPI: 2
經理: 經理-B, KPI: 2, 新產品數量: 0
工程師: 工程師-D, KPI: 4
=========== CTO看報表 ===========
經理: 經理-A, 產品數量: 9
工程師: 工程師-A, 代碼行數: 14704
工程師: 工程師-B, 代碼行數: 41123
工程師: 工程師-C, 代碼行數: 64598
經理: 經理-B, 產品數量: 5
工程師: 工程師-D, 代碼行數: 42490

訪問者模式最大的優點就是增加訪問者非常容易,我們從代碼中可以看到,如果要增加一個訪問者,只要新實現一個 Visitor 接口的類,從而達到數據對象與數據操作相分離的效果。如果不實用訪問者模式,而又不想對不同的元素進行不同的操作,那麼必定需要使用 if-else 和類型轉換,這使得代碼難以升級維護。

模式的應用場景

通常在以下情況可以考慮使用訪問者(Visitor)模式。

  1. 對象結構相對穩定,但其操作算法經常變化的程序。
  2. 對象結構中的對象需要提供多種不同且不相關的操作,而且要避免讓這些操作的變化影響對象的結構。
  3. 對象結構包含很多類型的對象,希望對這些對象實施一些依賴於其具體類型的操作。

參考

訪問者模式一篇就夠了
訪問者模式(Visitor模式)詳解

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