「補課」進行時:設計模式(18)——訪問者模式

極客挖掘機發表於2020-12-14

1. 前文彙總

「補課」進行時:設計模式系列

2. 引言

訪問者模式也可以說是所有設計模式中最難的一種設計模式了,當然我們平常也很少會用到它。設計模式的作者是這麼評價訪問者模式的:大多情況下,你並不需要使用訪問者模式,但是一旦需要使用它時,那就真的需要使用了。

3. 一個簡單的示例

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

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

首先定義一個員工基類 Staff :

public abstract class Staff {
    public String name;
    // 員工KPI
    public int kpi;

    public Staff(String name) {
        this.name = name;
        kpi = new Random().nextInt(10);
    }
    // 核心方法,接受Visitor的訪問
    abstract void accept(Visitor visitor);
}

Staff 類定義了員工基本資訊及一個 accept() 方法, accept() 方法表示接受訪問者的訪問,由子類具體實現。

Visitor 是個介面,傳入不同的實現類,可訪問不同的資料。

下面是工程師和經理的具體實現類:

public class Engineer extends Staff {
    public Engineer(String name) {
        super(name);
    }
    @Override
    void accept(Visitor visitor) {
        visitor.visit(this);
    }
    // 工程師一年的程式碼數量
    public int getCodeLines() {
        return new Random().nextInt(10 * 10000);
    }
}

public class Manager extends Staff {
    public Manager(String name) {
        super(name);
    }
    @Override
    void accept(Visitor visitor) {
        visitor.visit(this);
    }
    // 一年做的產品數量
    public int getProducts() {
        return new Random().nextInt(10);
    }
}

工程師是程式碼數量,經理是產品數量,他們的職責不一樣,也就是因為差異性,才使得訪問模式能夠發揮它的作用。

下面是 Visitor 介面的定義:

public interface Visitor {
    // 訪問工程師型別
    void visit(Engineer engineer);
    // 訪問經理型別
    void visit(Manager manager);
}

Visitor 宣告瞭兩個 visit 方法,分別是對工程師和經理對訪問函式。

接下來定義兩個具體的訪問者: CEO 和 CTO 。

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());
    }
}

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 和 CTO 通過這個報表檢視所有員工的業績:

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 Manager("經理-B"));
        mStaffs.add(new Engineer("工程師-C"));
    }
    /**
     * 為訪問者展示報表
     * @param visitor 公司高層,如 CEO、CTO
     */
    public void showReport(Visitor visitor) {
        for (Staff staff : mStaffs) {
            staff.accept(visitor);
        }
    }
}

最後是一個場景類:

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: 7, 新產品數量: 8
工程師: 工程師-A, KPI: 6
工程師: 工程師-B, KPI: 3
經理: 經理-B, KPI: 4, 新產品數量: 4
工程師: 工程師-C, KPI: 2
=========== CTO看報表 ===========
經理: 經理-A, 產品數量: 6
工程師: 工程師-A, 程式碼行數: 61280
工程師: 工程師-B, 程式碼行數: 10353
經理: 經理-B, 產品數量: 5
工程師: 工程師-C, 程式碼行數: 65827

4. 訪問者模式

4.1 定義

訪問者模式(Visitor Pattern) 是一個相對簡單的模式, 其定義如下:

Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates. (封裝一些作用於某種資料結構中的各元素的操作, 它可以在不改變資料結構的前提下定義作用於這些元素的新的操作。 )

4.2 通用類圖

  • Visitor 抽象訪問者:抽象類或者介面,宣告訪問者可以訪問哪些元素。
  • ConcreteVisitor 具體訪問者:它影響訪問者訪問到一個類後該怎麼幹, 要做什麼事情。
  • Element 抽象元素:介面或者抽象類,宣告接受哪一類訪問者訪問。
  • ConcreteElement 具體元素:實現方法。
  • ObjectStruture 結構物件:元素產生者,一般容納在多個不同類、不同介面的容器。

4.3 通用程式碼

抽象元素:

public abstract class Element {
    // 定義業務邏輯
    abstract void doSomething();
    // 定義允許訪問角色
    abstract void accept(IVisitor visitor);
}

具體元素:

public class ConcreteElement1 extends Element{
    @Override
    void doSomething() {

    }

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

public class ConcreteElement2 extends Element{
    @Override
    void doSomething() {

    }

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

抽象訪問者:

public interface IVisitor {
    void visit(ConcreteElement1 ele1);
    void visit(ConcreteElement2 ele2);
}

具體訪問者:

public class Visitor implements IVisitor{
    @Override
    public void visit(ConcreteElement1 ele1) {
        ele1.doSomething();
    }

    @Override
    public void visit(ConcreteElement2 ele2) {
        ele2.doSomething();
    }
}

結構物件:

public class ObjectStruture {
    public static Element createElement() {
        Random random = new Random();
        if (random.nextInt(100) > 50) {
            return new ConcreteElement1();
        } else {
            return new ConcreteElement2();
        }
    }
}

場景類:

public class Client {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Element e1 = ObjectStruture.createElement();
            e1.accept(new Visitor());
        }
    }
}

4.4 優點

  1. 各角色職責分離,符合單一職責原則。

    通過UML類圖和上面的示例可以看出來,Visitor、ConcreteVisitor、Element 、ObjectStructure,職責單一,各司其責。

  2. 具有優秀的擴充套件性。

    如果需要增加新的訪問者,增加實現類 ConcreteVisitor 就可以快速擴充套件。

  3. 使得資料結構和作用於結構上的操作解耦,使得操作集合可以獨立變化。

    員工屬性(資料結構)和CEO、CTO訪問者(資料操作)的解耦。

  4. 靈活性。

4.5 缺點

  1. 具體元素對訪問者公佈細節,違反了迪米特原則。

    CEO、CTO需要呼叫具體員工的方法。

  2. 具體元素變更時導致修改成本大。

    變更員工屬性時,多個訪問者都要修改。

  3. 違反了依賴倒置原則,為了達到「區別對待」而依賴了具體類,沒有用來抽象。

    訪問者 visit 方法中,依賴了具體員工的具體方法。

相關文章