大白話聊訪問者模式:從入門到實踐
阿新 • • 發佈:2021-02-18
# 文章首發於個人部落格 [shuyi.tech](shuyi.tech),歡迎訪問更多有趣有價值的文章。
訪問者模式,重點在於訪問者二字。說到訪問,我們腦海中必定會想起新聞訪談,兩個人面對面坐在一起。從字面上的意思理解:其實就相當於被訪問者(某個公眾人物)把訪問者(記者)當成了外人,不想你隨便動。你想要什麼,我弄好之後給你(呼叫你的方法)。
## 01 什麼是訪問者模式?
訪問者模式的定義如下所示,說的是在不改變資料結構的提前下,定義新操作。
> 封裝一些作用於某種資料結構中的各元素的操作,它可以在不改變資料結構的前提下定義作用於這些元素的新的操作。
但在實際的應用中,我發現有些例子並不是如此。有些例子中並沒有穩定的資料結構,而是穩定的演算法。**在樹義看來,訪問者模式是:把不變的固定起來,變化的開放出去。**
我們舉生活中一個例子來聊聊:某科學家接受記著訪談。我們都知道科學家接受訪問,肯定是有流程上的限制的,不可能讓你隨便問。我們假設這個過程是:先問科學家的學校經歷,再聊你的工作經歷,最後聊你的科研成果。那麼在這個過程中,固定的是什麼東西呢?固定的是接受採訪的流程。變化的是什麼呢?變化的是不同的記者,針對學校經歷,可能會提不同的問題。
根據我們之前的理解,訪問者模式其實就是要把不變的東西固定起來,變化的開放出去。那麼對於科學家接受訪談這個事情,我們可以這麼將其抽象化。
首先,我們需要有一個 Visitor 類,這裡定義了一些外部(記者)可以做的事情(提學校經歷、工作經歷、科研成就的問題)。
```
public interface Visitor {
public void askSchoolExperience(String name);
public void askWorkExperience(String name);
public void askScienceAchievement(String name);
}
```
# 文章首發於個人部落格 [shuyi.tech](shuyi.tech),歡迎訪問更多有趣有價值的文章。
接著宣告一個 XinhuaVisitor 類去實現 Visitor 類,這表示是新華社的一個記者(訪問者)想去訪問科學家。
```
public class XinhuaVisitor implements Visitor{
@Override
public void askSchoolExperience(String name) {
System.out.printf("請問%s:在學校取得的最大成就是什麼?\n", name);
}
@Override
public void askWorkExperience(String name) {
System.out.printf("請問%s:工作上最難忘的事情是什麼?\n", name);
}
@Override
public void askScienceAchievement(String name) {
System.out.printf("請問%s:最大的科研成果是什麼?", name);
}
}
```
接著宣告一個 Scientist 類,表明是一個科學家。科學家通過一個 accept() 方法接收記者(訪問者)的訪問申請,將其儲存起來。科學家定義了一個 interview 方法,將訪問的流程固定死了,只有教你問什麼的時候,我才會讓你(記者)提問。
```
public class Scientist {
private Visitor visitor;
private String name;
private Scientist(){}
public Scientist(String name) {
this.name = name;
}
public void accept(Visitor visitor) {
this.visitor = visitor;
}
public void interview(){
System.out.println("------------訪問開始------------");
System.out.println("---開始聊學校經歷---");
visitor.askSchoolExperience(name);
System.out.println("---開始聊工作經歷---");
visitor.askWorkExperience(name);
System.out.println("---開始聊科研成果---");
visitor.askScienceAchievement(name);
}
}
```
最後我們宣告一個場景類 Client,來模擬訪談這一過程。
```
public class Client {
public static void main(String[] args) {
Scientist yang = new Scientist("楊振寧");
yang.accept(new XinhuaVisitor());
yang.interview();
}
}
```
執行的結果為:
```
------------訪問開始------------
---開始聊學校經歷---
請問楊振寧:在學校取得的最大成就是什麼?
---開始聊工作經歷---
請問楊振寧:工作上最難忘的意見事情是什麼?
---開始聊科研成果---
請問楊振寧:最大的科研成果是什麼?
```
看到這裡,大家對於訪問者模式的本質有了更感性的認識(把不變的固定起來,變化的開放出去)。在這個例子中,不變的固定的就是訪談流程,變化的就是你可以提不同的問題。
一般來說,訪問者模式的類結構如下圖所示:
![](https://shuyi-tech-blog.oss-cn-shenzhen.aliyuncs.com/halo_blog_system_file/16136087485221.jpg)
* Visitor 訪問者介面。訪問者介面定義了訪問者可以做的事情。這個需要你去分析哪些是可變的,將這些可變的內容抽象成訪問者介面的方法,開放出去。而被訪問者的資訊,其實就是通過訪問者的引數傳遞過去。
* ConcreteVisitor 具體訪問者。具體訪問者定義了具體某一類訪問者的實現。對於新華社記者來說,他們更關心楊振寧科學成果方面的事情,於是他們提問的時候更傾向於挖掘成果。但對於青年報記者來說,他們的讀者是青少年,他們更關心楊振寧在學習、工作中的那種精神。
* Element 具體元素。這裡指的是具體被訪問的類,在我們這個例子中指的是 Scientist 類。一般情況下,我們會提供一個 accept() 方法,接收訪問者引數,將相當於接受其範文申請。但這個方法也不是必須的,只要你能夠拿到 visitor 物件,你怎麼定義這個引數傳遞都可以。
# 文章首發於個人部落格 [shuyi.tech](shuyi.tech),歡迎訪問更多有趣有價值的文章。
對於訪問者模式來說,最重要的莫過於 Visitor、ConcreteVisitor、Element 這三個類了。Visitor、ConcreteVisitor 定義訪問者具體能做的事情,被訪問者的引數通過引數傳遞給訪問者。Element 則通過各種方法拿到被訪問者物件,常用的是通過 accept() 方法,但這並不是絕對的。
*需要注意的是,我們學習設計模式重點是理解類與類之間的關係,以及他們傳遞的資訊。至於是通過什麼方式傳遞的,是通過 accept() 方法,還是通過建構函式,都不是重點。*
## 02 訪問者模式的實際應用
前面我們用一個生活的例子幫助大家理解訪問者模式,相信大家對訪問者模式應該有了個感性的理解了。為了迴歸程式設計實踐本身,讓大家對訪問者模式能有更好的實踐理解。下面我們將從軟體程式設計上講講訪問者模式在開源框架中的應用。
### 檔案樹遍歷
JDK 中有檔案操作,我們自然是清楚的。有檔案操作,那自然就會有資料夾的遍歷操作,即訪問某個資料夾下面的所有檔案或資料夾。試想一下,如果我們想要打印出某個資料夾下所有檔案及資料夾的名字,我們需要怎麼做?
很簡單的做法,其實就是直接做一個樹的遍歷,然後將名字打印出來呀!
沒錯,這確實是正確答案!
那麼如果我希望統計一下所有檔案及資料夾的個數呢?
那就再遍歷一次,然後用一個計數器去一直加一唄!
沒錯,這也是正確答案!
但你是否發現了這兩個過程中,我們有一個相同的操作:遍歷檔案樹。無論是列印檔名,還是計算檔案樹,我們都需要去遍歷檔案樹。而無論哪一個過程,我們最終要的其實就是訪問檔案。
還記得我們說過設計模式的本質是什麼嗎?**設計模式的本質是找出不變的東西,再找出變化的東西,然後找到合適的資料結構(設計模式)去承載這種變化。**
在這個例子裡,不變的東西是檔案樹的遍歷,變化的是對於檔案的不同訪問操作。很顯然,訪問者模式是比較適合承載這種變化的。我們可以把這種不變的東西(檔案樹的遍歷)固定起來,把變化的東西(檔案的具體操作)開放出去。JDK 對於檔案樹的遍歷,其實就是使用訪問者模式實現的。
JDK 中聲明瞭一個 FileVisitor 介面,定義了遍歷者可以做的操作。
```
public interface FileVisitor {
FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs);
FileVisitResult visitFile(T file, BasicFileAttributes attrs)
throws IOException;
FileVisitResult visitFileFailed(T file, IOException exc)
throws IOException;
FileVisitResult postVisitDirectory(T dir, IOException exc)
throws IOException;
}
```
FileVisitor 中定義的 visitFile() 方法,其實就是對於檔案的訪問。被訪問者(檔案)的資訊通過第一個引數 file 傳遞過來。這樣遍歷者就可以訪問檔案的內容了。
SimpleFileVisitor 則是對於 FileVisitor 介面的實現,該類中僅僅是做了簡單的引數校驗,並沒有太過的邏輯。
```
public class SimpleFileVisitor implements FileVisitor {
@Override
public FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
throws IOException
{
Objects.requireNonNull(dir);
Objects.requireNonNull(attrs);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(T file, BasicFileAttributes attrs)
throws IOException
{
Objects.requireNonNull(file);
Objects.requireNonNull(attrs);
return FileVisitResult.CONTINUE;
}
//....其他省略
}
```
FileVisitor 類和 SimpleFileVisitor 類對應的就是 UML 類圖中的 Visitor 和 ConcreteVisitor 類。而 Element 元素,對應的其實是 JDK 中的 Files 類。
![](https://shuyi-tech-blog.oss-cn-shenzhen.aliyuncs.com/halo_blog_system_file/16117971484525.jpg)
Files 檔案中遍歷檔案樹是通過 walkFileTree() 方法實現的。在 walkFileTree() 方法中實現了樹的遍歷,在遍歷到檔案的時候會通過 visitor 類的 visitFile 方法呼叫遍歷者的方法,將遍歷到的檔案傳遞給遍歷者,從而達到分離變化的目的。
### ASM修改位元組碼
ASM 是 Java 的位元組碼增強技術,這裡面就用到了訪問者模式,主要是用來進行位元組碼的修改。在 ASM 中於此相關的三個類分別是:ClassReader、ClassVisitor、ClassWriter。
ClassReader 類相當於訪問者模式中的 Element 元素。它將位元組陣列或 class 檔案讀入記憶體中,並以樹的資料結構表示。該類定義了一個 accept 方法用來和 visitor 互動。
![](https://shuyi-tech-blog.oss-cn-shenzhen.aliyuncs.com/halo_blog_system_file/16119728409207.jpg)
ClassVisitor 相當於抽象訪問者介面。ClassReader 物件建立之後,需要呼叫 accept() 方法,傳入一個 ClassVisitor 物件。在 ClassReader 的不同時期會呼叫 ClassVisitor 物件中不同的 visit() 方法,從而實現對位元組碼的修改。
![](https://shuyi-tech-blog.oss-cn-shenzhen.aliyuncs.com/halo_blog_system_file/16119730693265.jpg)
ClassWriter 是 ClassVisitor 的是實現類,它負責將修改後的位元組碼輸出為位元組陣列。
![](https://shuyi-tech-blog.oss-cn-shenzhen.aliyuncs.com/halo_blog_system_file/16119731854988.jpg)
**對於 ASM 這種場景而言,位元組碼規範是非常嚴格且穩定的,如果隨便更改可能出問題。但我們又需要對位元組碼進行動態修改,從而達到某些目的。在這種情況下,ASM 的設計者採用了訪問者模式將變化的部分隔離開來,將不變的部分固定下來,從而達到了靈活擴充套件的目的。**
## 03 我們該如何使用?
從上面幾個例子,我們大致可以明白訪問者模式的使用場景:**某些較為穩定的東西(資料結構或演算法),不想直接被改變但又想擴充套件功能,這時候適合用訪問者模式。**
說到對於訪問者模式使用場景的定義,我們會覺得模板方法模式與這個使用場景的定義很像。但它們還是有些許差別的。**訪問者模式的變化與非變化(即訪問者與被訪問者)之間,它們只是簡單的包含關係,而模板方法模式的變化與非變化則是繼承關係。** 但它們也確實有類似的地方,即都是封裝了固定不變的東西,開放了變動的東西。
訪問者模式的優點很明顯,即隔離了變化的東西,固定了不變的東西,使得整體的可維護性更強、具有更強的擴充套件性。但它也帶來了設計模式通用的一些缺點,例如:
* 類結構變得複雜。之前我們可是簡單的呼叫關係,現在則是多個類之間的繼承和組合關係。從一定程度上,提高了對開發人員的要求,提高了研發成本。
* 被訪問者的變更變得更加困難。例如我們上面科學家訪談的例子,如果科學家訪談希望新增一個環節,那麼 Scientist 類需要修改,Visitor 類、XinhuaVisitor 類都需要修改。
有這些多優點,但也有這麼多缺點,那實際工作中我們應該怎麼判斷是否用訪問者模式呢?
**總的原則就是揚長避短,即當場景完全利用了訪問者模式的優點,規避了訪問者模式的缺點的時候,就是使用訪問者模式的最佳時機。**
雖然使用訪問者模式會讓被訪問者的變更變得更加困難,但如果被訪問者很穩定,基本不會變更,那這個缺點不就去除了麼。例如在 ASM 的例子中,元素是 ClassReader,其儲存了位元組碼的結構。而位元組碼結構完全不會輕易改變,所以在這個「被訪問者的變更變得更加困難」的缺點也就不存在了。
而「類結構變得複雜」這個缺點,則是需要根據當時業務的複雜程度來看的。如果當時業務很簡單,而且變化也不大,那麼使用設計模式完全是多餘的。但是如果當時業務很複雜了,我們還是在一個類裡做修改,那麼很大可能性會出大問題。這時候就需要用設計模式來承載複雜的業務結構了。
## 04 參考資料
* [一文說透訪問者模式 - 犀牛飼養員部落格](http://www.machengyu.net/tech/2019/11/11/visitor.html)
* [訪問者設計模式](https://refactoringguru.cn/design-patterns/visitor)
* [訪問者模式一篇就夠了 - 簡書](https://www.jianshu.com/p/1f1049d0a0f4)
* [一文說透訪問者模式 - 犀牛飼養員部落格](http://www.machengyu.net/tech/2019/11/11/visitor.html)
* [訪問者模式在 ASM 框架中的使用](https://juejin.cn/post/6844904131182739469)
* [訪問者模式由淺入深及用例場景 加上 AMS 的簡單使用_a1032722788 的部落格 - CSDN 部落格](https://blog.csdn.net/a1032722788/article/details/113053323)
# 文章首發於個人部落格 [shuyi.tech](shuyi.tech),歡迎訪問更多有趣有價值的