1. 程式人生 > >設計模式:訪問者(Visitor)模式

設計模式:訪問者(Visitor)模式

tlist 情況下 面向 font ati pap 書籍 技術 原理

設計模式:訪問者(Visitor)模式

一、前言

什麽叫做訪問,如果大家學過數據結構,對於這點就很清晰了,遍歷就是訪問的一般形式,單獨讀取一個元素進行相應的處理也叫作訪問,讀取到想要查看的內容+對其進行處理就叫做訪問,那麽我們平常是怎麽訪問的,基本上就是直接拿著需要訪問的地址(引用)來讀寫內存就可以了。

為什麽還要有一個訪問者模式呢,這就要放到OOP之中了,在面向對象編程的思想中,我們使用類來組織屬性,以及對屬性的操作,那麽我們理所當然的將訪問操作放到了類的內部,這樣看起來沒問題,但是當我們想要使用另一種遍歷方式要怎麽辦呢,我們必須將這個類進行修改,這在設計模式中是大忌,在設計模式中就要保證,對擴展開放,對修改關閉的開閉原則。

因此,我們思考,可不可以將訪問操作獨立出來變成一個新的類,當我們需要增加訪問操作的時候,直接增加新的類,原來的代碼不需要任何的改變,如果可以這樣做,那麽我們的程序就是好的程序,因為可以擴展,符合開閉原則。而訪問者模式就是實現這個的,使得使用不同的訪問方式都可以對某些元素進行訪問。

技術分享圖片

二、代碼

Element 接口:
1 package zyr.dp.visitor;
2 
3 public interface Element {
4 
5     public abstract void accept(Visitor visitor);
6     
7 }
Entry 類:
 1
package zyr.dp.visitor; 2 3 import java.util.Iterator; 4 5 public abstract class Entry implements Element{ 6 public abstract String getName(); 7 public abstract int getSize(); 8 public abstract void printList(String prefix); 9 public void printList(){ 10 printList("");
11 } 12 public Entry add(Entry entry) throws RuntimeException{ 13 throw new RuntimeException(); 14 } 15 public Iterator iterator() throws RuntimeException{ 16 throw new RuntimeException(); 17 } 18 public String toString(){ 19 return getName()+"<"+getSize()+">"; 20 } 21 }
File 類:
 1 package zyr.dp.visitor;
 2 
 3 public class File extends Entry {
 4 
 5     private String name;
 6     private int size;
 7     public File(String name,int size){
 8         this.name=name;
 9         this.size=size;
10     }
11     public String getName() {
12         return name;
13     }
14 
15     public int getSize() {
16         return size;
17     }
18 
19     public void printList(String prefix) {
20         System.out.println(prefix+"/"+this);
21     }
22     public void accept(Visitor visitor) {
23        //  System.out.println("開始訪問文件:"+this);
24         visitor.visit(this);
25        // System.out.println("結束訪問文件:"+this);
26        // System.out.println();
27     }
28 
29 }

Directory類:

 1 package zyr.dp.visitor;
 2 
 3 import java.util.ArrayList;
 4 import java.util.Iterator;
 5 
 6 public class Directory extends Entry {
 7 
 8     String name;
 9     ArrayList entrys=new ArrayList();
10     public Directory(String name){
11         this.name=name;
12     }
13     public String getName() {
14         return name;
15     }
16 
17     public int getSize() {
18         int size=0;
19         Iterator it=entrys.iterator();
20         while(it.hasNext()){
21             size+=((Entry)it.next()).getSize();
22         }
23         return size;
24     }
25 
26     public Entry add(Entry entry) {
27         entrys.add(entry);
28         return this;
29     }
30     
31     public Iterator iterator() {
32         return entrys.iterator();
33     }
34     
35     public void printList(String prefix) {
36         System.out.println(prefix+"/"+this);
37         Iterator it=entrys.iterator();
38         Entry entry;
39         while(it.hasNext()){
40             entry=(Entry)it.next();
41             entry.printList(prefix+"/"+name);
42         }
43     }
44     public void accept(Visitor visitor) {
45       //  System.out.println("開始訪問文件夾:"+this);
46         visitor.visit(this);
47      //   System.out.println("結束訪問文件夾:"+this);
48      //   System.out.println();
49     }
50 
51 }
Visitor 類:
1 package zyr.dp.visitor;
2 
3 public abstract class Visitor {
4 
5     public abstract void visit(File file);
6     public abstract void visit(Directory directory);
7     
8 }

ListVisitor類:

 1 package zyr.dp.visitor;
 2 
 3 import java.util.Iterator;
 4 
 5 public class ListVisitor extends Visitor {
 6 
 7     String currentDir = "";
 8     public void visit(File file) {
 9         System.out.println(currentDir+"/"+file);
10     }
11 
12     public void visit(Directory directory) {
13         System.out.println(currentDir+"/"+directory);
14         String saveDir=currentDir;
15         currentDir+=("/"+directory.getName());
16         Iterator it=directory.iterator();
17         while(it.hasNext()){
18             Entry entry=(Entry)it.next();
19             entry.accept(this);
20         }
21         currentDir=saveDir;
22     }
23 
24 }
FileVisitor 類:
 1 package zyr.dp.visitor;
 2 
 3 import java.util.ArrayList;
 4 import java.util.Iterator;
 5 
 6 public class FileVisitor extends Visitor {
 7 
 8     String currentDir = "";
 9     String suffix;
10     ArrayList files=new ArrayList();
11     
12     public FileVisitor(String suffix){
13          this.suffix = suffix;
14     }
15     
16     public void visit(File file) {
17         if(file.getName().endsWith(suffix)){
18          // System.out.println(currentDir+"/"+file);
19             files.add(currentDir+"/"+file);
20         }
21     }
22 
23     public void visit(Directory directory) {
24         String saveDir=currentDir;
25         currentDir+=("/"+directory.getName());
26         Iterator it=directory.iterator();
27         while(it.hasNext()){
28             Entry entry=(Entry)it.next();
29             entry.accept(this);
30         }
31         currentDir=saveDir;
32     }
33     Iterator getFiles(){
34         return files.iterator();
35     }
36 
37 }

Main類:

 1 package zyr.dp.visitor;
 2 
 3 import java.util.Iterator;
 4 
 5 
 6 public class Main {
 7 
 8     public static void main(String[] args) {
 9 
10         Directory root=new Directory("根目錄");
11         
12         Directory life=new Directory("我的生活");
13         File eat=new File("吃火鍋.txt",100);
14         File sleep=new File("睡覺.html",100);
15         File study=new File("學習.txt",100);
16         life.add(eat);
17         life.add(sleep);
18         life.add(study);
19         
20         Directory work=new Directory("我的工作");
21         File write=new File("寫博客.doc",200);
22         File paper=new File("寫論文.html",200);
23         File homework=new File("寫家庭作業.docx",200);
24         work.add(write);
25         work.add(paper);
26         work.add(homework);
27         
28         Directory relax=new Directory("我的休閑");
29         File music=new File("聽聽音樂.js",200);
30         File walk=new File("出去轉轉.psd",200);
31         relax.add(music);
32         relax.add(walk);
33         
34         Directory read=new Directory("我的閱讀");
35         File book=new File("學習書籍.psd",200);
36         File novel=new File("娛樂小說.txt",200);
37         read.add(book);
38         read.add(novel);
39         
40         root.add(life);
41         root.add(work);
42         root.add(relax);
43         root.add(read);
44         
45         root.accept(new ListVisitor());
46         System.out.println("========================");
47         FileVisitor visitor=new FileVisitor(".psd");
48         root.accept(visitor);
49         Iterator it = visitor.getFiles();
50         while(it.hasNext()){
51             System.out.println(it.next());
52         }
53         
54     }
55 
56 }

運行結果:

技術分享圖片

可以看到我們的運行結果第一個和使用Composite模式的結果一樣,第二個是實現另一種方式的訪問,只訪問文件後綴為某一特定的內容的文件,結果也是正確的,並且為了說明我們的訪問還可以保存下來訪問的結果,我們使用了ArrayList自帶的叠代器將保存到ArrayList中的結果輸出出來,我們當然也可以直接在遍歷的時候就輸出出來,這個看我們的使用要求了。由此可以看到在保證數據結構(File和Directory)不發生變化的情況下(沒有新增或者刪除),可以非常方便增加新的一種訪問方法,只需要新增加一個訪問類即可,但是如果我們數據結構發生變化之後,就需要修改繼承自Visitor類的所有類了,這也違背了開閉原則,因此我們應該認真考慮,到底我們的數據結構是定死的還是經常變化的。沒有任何一種設計模式是十全十美的,總是有所取舍,有所利弊,根據實際情況來選擇才是最好的設計方法。

這裏要說明一下雙重分發機制,我們來看一下最核心的遍歷邏輯,結合組合模式的時候我們已經分析過的遍歷方法,遞歸,大家覺得這次我們要怎麽在數據結構外面進行遍歷,肯定還是要使用遞歸了,可是數據結構中的數據在類的內部,怎麽遞歸到內部呢,我們想到了間接遞歸,也就是雙重分發。

1     public void printList(String prefix) {
2         System.out.println(prefix+"/"+this);
3         Iterator it=entrys.iterator();
4         Entry entry;
5         while(it.hasNext()){
6             entry=(Entry)it.next();
7             entry.printList(prefix+"/"+name);
8         }
9     }

上面的代碼是在組合模式類的內部遍歷的過程,可以明確的看到遞歸(直接遞歸)的使用。我們看一下訪問者模式中的間接遞歸:

Directory類中:

    public void accept(Visitor visitor) {
        //System.out.println("開始訪問文件夾:"+this);
        visitor.visit(this);
        //System.out.println("結束訪問文件夾:"+this);
        //System.out.println();
    }

File類:

1     public void accept(Visitor visitor) {
2         //System.out.println("開始訪問文件:"+this);
3         visitor.visit(this);
4         //System.out.println("結束訪問文件:"+this);
5         //System.out.println();
6     }

在ListVisitor中:

 1     public void visit(File file) {
 2         System.out.println(currentDir+"/"+file);
 3     }
 4 
 5     public void visit(Directory directory) {
 6         System.out.println(currentDir+"/"+directory);
 7         String saveDir=currentDir;
 8         currentDir+=("/"+directory.getName());
 9         Iterator it=directory.iterator();
10         while(it.hasNext()){
11             Entry entry=(Entry)it.next();
12             entry.accept(this);
13         }
14         currentDir=saveDir;
15     }

我們看到了entry.accept(this)這句話,這句話是非常重要的,我們在Main中是這樣用的:

1     root.accept(new ListVisitor());

那麽串連起來,在Main中我們通過Directory或者File類型的對象調用accept(訪問者)方法,接受訪問者的訪問,這是訪問者和被訪問者的第一次親密接觸,親近對方就是為了獲得對方的數據,然後才能對對方的數據進行使用,那麽怎麽拿到的呢?!我們看到了這句visitor.visit(this);這句話無疑是重要的,被調用者告訴訪問者,我將我的內容this,全部給你了,以後訪問者就可以對this所指代的被訪問者的內容進行操作了,分為兩類,如果被訪問者是File文件類型的,就會直接輸出內容,到達葉子結點,訪問結束;如果是文件夾,那就非常有意思了,首先我們仍舊是讓被訪問者將自己的內容交給訪問者visitor.visit(this);,之後public void visit(Directory directory)被調用,通過遍歷的方式將屬於這個文件夾下面的數據全部拿到Iterator it=directory.iterator();,然後開始一個個的處理,怎麽處理呢,繼續訪問屬於這個文件夾下面對象的accept()方法使用entry.accept(this);,來將訪問者交過去,交給誰?!肯定是給entry所指的對象,也就是文件夾裏面的子文件夾或者文件,如果是文件的話,繼續在自己的方法中調用visitor.visit(this);,最終落實到調用 public void visit(File file)通過System.out.println(currentDir+"/"+file);訪問結束,如果不是文件呢?若為文件夾,則繼續調用屬於文件夾的方法,就這樣不斷地往下面查找,一直到遍歷完文件夾下面的所有的元素,因此也是深度優先遍歷。就這樣通過壓棧和出棧,我們完成了最終的遍歷,最終的出口有兩個,一個是訪問文件,輸出之後結束,另一個是遍歷完文件夾,即使文件夾下面沒有文件依舊結束。

1 root.accept(new ListVisitor());
1     public void accept(Visitor visitor) {
2         visitor.visit(this);
3     }
 1     public void visit(File file) {
 2         System.out.println(currentDir+"/"+file);
 3     }
 4 
 5     public void visit(Directory directory) {
 6         System.out.println(currentDir+"/"+directory);
 7         String saveDir=currentDir;
 8         currentDir+=("/"+directory.getName());
 9         Iterator it=directory.iterator();
10         while(it.hasNext()){
11             Entry entry=(Entry)it.next();
12             entry.accept(this);
13         }
14         currentDir=saveDir;
15     }

在accept函數中調用visit,同樣在visit中調用accept,這就是間接遞歸,或者叫做雙重分發。產生的原因就是訪問者需要和被訪問者相互交流,才能一步步的得到想要的數據。我們可以考慮主持人采訪一個明星,那麽這個明星接受采訪,把自己基本信息(能問的問題以及某些答案)告訴主持人,問主持人有問題嗎?如果主持人有問題(還能向下問)要問那麽就再次拿著新的問題問這個明星,這個明星再次將自己關於這方面的信息告訴主持人;如果沒有問題(得到答案),主持人將信息總結之後說出來。就這樣一直持續下去,直到主持人沒問題問了,並且明星的信息也都被問到了,這樣采訪就結束了。由此可見,很多時候設計模式都是和生活密切相關的,生活中的常識有時候就是一些套路,而這種套路就是一種抽象的模式。

三、總結

訪問者模式是一個非常有意思的模式,因為自己需要得到數據就需要向被訪者索取,如果能夠一次索取成功,訪問就結束了,如果還需要其他信息,則再次向被訪問者索取,就這樣知道拿到自己需要的所有數據。在本例中借用了組合模式中的數據結構,那是因為這種樹形的結構很適合我們進行遞歸訪問。訪問者模式和叠代器模式都是在某種數據結構上進行處理,一種是對數據結構中的元素進行某種特定的處理,另一種是用某種方式遍歷所有元素。在實際應用中,我們根據實際需要來考慮是不是需要雙重分發機制。在本例中的訪問者模式中用到了組合模式、委托(組合)、雙重分發等原理,便於新增訪問方式,不便於對數據結構的修改。

程序代碼

設計模式:訪問者(Visitor)模式