1. 程式人生 > >「補課」進行時:設計模式(18)——訪問者模式

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

![](https://cdn.geekdigging.com/DesignPatterns/java_design_pattern.jpg) ## 1. 前文彙總 [「補課」進行時:設計模式系列](https://www.geekdigging.com/category/%e8%ae%be%e8%ae%a1%e6%a8%a1%e5%bc%8f/) ## 2. 引言 訪問者模式也可以說是所有設計模式中最難的一種設計模式了,當然我們平常也很少會用到它。設計模式的作者是這麼評價訪問者模式的:大多情況下,你並不需要使用訪問者模式,但是一旦需要使用它時,那就真的需要使用了。 ## 3. 一個簡單的示例 又快到年底, CEO 和 CTO 開始評定員工一年的工作績效,員工分為工程師和經理, CTO 關注工程師的程式碼量、經理的新產品數量; CEO 關注的是工程師的KPI和經理的KPI以及新產品數量。 由於 CEO 和 CTO 對於不同員工的關注點是不一樣的,這就需要對不同員工型別進行不同的處理。訪問者模式此時可以派上用場了。 首先定義一個員工基類 Staff : ```java 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` 是個介面,傳入不同的實現類,可訪問不同的資料。 下面是工程師和經理的具體實現類: ```java 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 介面的定義: ```java public interface Visitor { // 訪問工程師型別 void visit(Engineer engineer); // 訪問經理型別 void visit(Manager manager); } ``` Visitor 聲明瞭兩個 visit 方法,分別是對工程師和經理對訪問函式。 接下來定義兩個具體的訪問者: CEO 和 CTO 。 ```java 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 通過這個報表檢視所有員工的業績: ```java public class BusinessReport { private List 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); } } } ``` 最後是一個場景類: ```java 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()); } } ``` 執行結果如下: ```java =========== 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 通用類圖 ![](https://cdn.geekdigging.com/DesignPatterns/18/Visitor_UML.png) - Visitor 抽象訪問者:抽象類或者介面,宣告訪問者可以訪問哪些元素。 - ConcreteVisitor 具體訪問者:它影響訪問者訪問到一個類後該怎麼幹, 要做什麼事情。 - Element 抽象元素:介面或者抽象類,宣告接受哪一類訪問者訪問。 - ConcreteElement 具體元素:實現方法。 - ObjectStruture 結構物件:元素產生者,一般容納在多個不同類、不同介面的容器。 ### 4.3 通用程式碼 抽象元素: ```java public abstract class Element { // 定義業務邏輯 abstract void doSomething(); // 定義允許訪問角色 abstract void accept(IVisitor visitor); } ``` 具體元素: ```java 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); } } ``` 抽象訪問者: ```java public interface IVisitor { void visit(ConcreteElement1 ele1); void visit(ConcreteElement2 ele2); } ``` 具體訪問者: ```java public class Visitor implements IVisitor{ @Override public void visit(ConcreteElement1 ele1) { ele1.doSomething(); } @Override public void visit(ConcreteElement2 ele2) { ele2.doSomething(); } } ``` 結構物件: ```java public class ObjectStruture { public static Element createElement() { Random random = new Random(); if (random.nextInt(100) > 50) { return new ConcreteElement1(); } else { return new ConcreteElement2(); } } } ``` 場景類: ```java 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 方法中,依賴了具體員工的具體