訪問差異類型的集合類--visitor模式入門

一,問題提出
訪問同一類型的集合類是我們最常見的事情了,我們工作中這樣的代碼太常見了。

1  Iterator ie  =  list.iterator();
2  while (ie.hasNext()) {
3     Person p  =  (Person)ie.next();
4     p.doWork();
5 }


這種訪問的特點是集合類中的對象是同一類對象Person,他們擁有功能的方法run,我們調用的恰好是這個共同的方法。
在大部份的情況下,這個是可以的,但在一些複雜的情況,如被訪問者的繼承結構複雜,被訪問者的並不是同一類對象,
也就是說不是繼承自同一個根類。方法名也並不相同。例如Java GUI中的事件就是一個例子。
例如這樣的問題,有如下類和方法:
類:PA ,方法:runPA();
類:PB ,方法:runPB();
類:PC ,方法:runPC();
類:PD ,方法:runPD();
類:PE ,方法:runPE();
有一個集合類List
List list = new ArrayList();
list.add(new PA());
list.add(new PB());
list.add(new PC());
list.add(new PD());
list.add(new PE());
....
二:解決
要求能訪問到每個類的對應的方法。我們第一反應應該是這樣的。

 1  Iterator ie  =  list.iterator();
 2  while (ie.hasNext()) {
 3     Object obj  =  ie.next();
 4       if  (obj  instanceof  PA) {
 5         ((PA)obj).runPA();
 6      } 
else   if (obj  instanceof  PB) {
 7         ((PB)obj).runPB();
 8      } 
else   if (obj  instanceof  PC) {
 9         ((PC)obj).runPC();
10      } 
else   if (obj  instanceof  PD) {
11         ((PD)obj).runPD();
12      } 
else   if (obj  instanceof  PE) {
13         ((PE)obj).runPE();
14     } 

15 }


三:新問題及分析解決
當數目變多的時候,維護if else是個費力氣的事情:
仔細分析if,else做的工作,首先判斷類型,然後根據類型執行相應的函數
如何才能解決這兩個問題呢?首先想到的是java的多態,多態就是根據參數執行相應的內容,
能很容易的解決第二個問題,我們可以寫這樣一個類:

 1  public   class  visitor {
 2       public   void  run(PA pa) {
 3         pa.runPA();
 4     } 

 5       public   void  run(PB pb) {
 6         pb.runPB();
 7     } 

 8       public   void  run(PC pc) {
 9         pc.runPC();
10     } 

11       public   void  run(PD pd) {
12         pd.runPD();
13     } 

14       public   void  run(PE pe) {
15         pe.runPE();
16     } 

17 }


這樣只要調用run方法,傳入對應的參數就能執行了。
還有一個問題就是判斷類型。由於重載(overloading)是靜態多分配(java語言本身是支持"靜態多分配"的。
關於這個概念請看這裏)所以造成重載只根據傳入對象的定義類型,而不是實際的類型,所以必須在傳入前就確定類型,
這可是個難的問題,因爲在容器中對象全是Object,出來後要是判斷是什麼類型必須用
if (xx instanceof xxx)這種方法,如果用這種方法啓不是又回到了原點,有沒有什麼更好的辦法呢?

我們知到Java還有另外一個特點,覆寫(overriding),而覆寫是"動態單分配"的(關於這個概念見這裏),
那如何利用這個來實現呢?看下邊這個方法:
 我們讓上邊的一些類PA PB PC PD PE都實現一個接口P,加入一個方法,accept();

 1  public   void  accept(visitor v) {
 2      // 把自己傳入1 
 3      v.run( this );
 4 

 5 然後在visitor中加入一個方法
 6  public   void  run(P p) {
 7      // 把自己傳入2 
 8      p.accept( this );
 9 

10  // 這樣你在遍歷中可以這樣寫 
11  Visitor v  =   new  Visitor();
12 Iterator ie  =  list.iterator();
13  while (ie.hasNext()) {
14     P p  =  (P)ie.next();
15         p.accept(v);
16     } 

17 }


首先執行的是"把自己傳入2",在這裏由於Java的特性,實際執行的是子類的accept(),也就是實際類的accept
然後是"把自己傳入1",在這裏再次把this傳入,就明確類型,ok我們巧妙的利用overriding解決了這個問題
其實歸納一下第二部分,一個關鍵點是"自己認識自己",是不是很可笑。
其實在計算計技術領域的很多技術上看起來很高深的東西,其實就是現有社會中人的生活方式的一種映射
而且這種方式是簡單的不能再簡單的方式。上邊的全部過程基本上是一個簡單的visitor模式實現,visitor模式
已經是設計模式中比較複雜的模式了,但其實原理簡單到你想笑。看看下邊這個比喻也許你的理解會更深刻。

四:一個幫助理解的比喻:
題目:指揮工人工作
條件:你有10個全能工人,10樣相同工作。
需求:做完工作
實現:大喊一聲所有人去工作

條件變了,工人不是全能,但是工作相同,ok問題不大
條件再變,工作不是相同,但工人是全能,ok問題不大

以上三種情況在現實生活中是很少發生得,最多的情況是這樣:
10個工人,每人會做一種工作,10樣工作。你又一份名單Collection)寫着誰做什麼。但你不認識任何人
這個時候你怎麼指揮呢,方案一:
你可以一個個的叫工人,然後問他們名字,認識他們,查名單,告訴他們做什麼工作。
你可以直接叫出他們名字,告訴他們幹什麼,不需要知到他是誰。
看起來很簡單。但如果你要指揮10萬人呢 ?而且人員是流動的,每天的人不同,你每天拿到一張文檔。
其實很簡單,最常用的做法是,你把這份名單貼在牆上,然後大喊一聲,所有人按照去看,按照自己的分配情況去做。
這裏利用的關鍵點是"所有工人自己認識自己",你不能苛求每個工人會做所有工作,不能苛求所有工作相同,但你
能要求所有工人都認識自己。

再想想我們開始的程序,每個工人對應着PA PB PC PD PE....
所有的工人都使工人P
每個工人會做的東西不一樣runPA runPB runPC
你有一份名單Visitor(重載)記錄着誰做什麼工作。

看完上邊這些,你是不是會產生如下的問題:
問題:爲什麼不把這些方法的方法名做成一樣的,那就可以解決了。
例如,我們每個PA ,PB ,PC都加入一個run 方法,然後run內部再調用自己對應的runPx()方法。
答案:有些時候從不同的角度考慮,或者因爲實現的複雜度早成很難統一方法名(被訪問者的功能接口具有封閉性不可改變)。
例如上邊指揮人工作的例子的例子,其實run方法就是大叫一聲去工作,因爲每個工人只會做一種工作,所以能行
但我們不能要求所有人只能會做一種事情,這個要求很愚蠢。所以如果每個工人會幹兩種或者多種工作呢,
也就是我PA 有runPA() walkPA()等等方法, PB有runPB() climbPB()等等。。。
這個時候按照名單做事纔是最好的辦法。

五:作者的話
所以說模式中很多複雜的東西,在現實中其實是很基本的東西,多多代入代出能幫助理解模式。

看完本文,如果你對visitor模式有更多的興趣,想了解更多請看如下幾篇文章。
1,靜態分派,動態分派,多分派,單分派 --------------   visitor模式準備
2,訪問差異類型的集合類 ------------------------   visitor模式入門(本文)
3,visitor模式理論及學術概念-------------------   visitor模式深入
4,重載overloading和覆寫overriding哪個更早執行--   visitor幫助篇 
雖然排列順序是1,2,3,4 但是我個人建議的學習方式是2,1,3,4因爲這個順序更方便一般人理解

 

 

 

 

 

 

 

 

 

 


 

posted on 2006-12-18 20:18 dreamstone 閱讀(3838) 評論(14)  編輯  收藏 所屬分類: 設計模式

評論

# re: 訪問差異類型的集合類--visitor模式入門 2006-12-19 09:37 hannyu

我有一個疑問,下面這段代碼是不是少寫了什麼東西?我怎麼覺得p.accept與v.run之間是死循環呢?請解釋一下 
1 public void accept(visitor v) { 
2 // 把自己傳入1 
3 v.run( this ); 
4 } 
5 然後在visitor中加入一個方法 
6 public void run(P p) { 
7 // 把自己傳入2 
8 p.accept( this ); 
9 } 
10 // 這樣你在遍歷中可以這樣寫 
11 Visitor v = new Visitor(); 
12 Iterator ie = list.iterator(); 
13 while (ie.hasNext()) { 
14 P p = (P)ie.next(); 
15 p.accept(v); 
16 } 
17 }   回覆  更多評論   

# re: 訪問差異類型的集合類--visitor模式入門 2006-12-19 09:46 themax

首先執行的是"把自己傳入2",??? 
應該是"把自己傳入1"吧. 

  回覆  更多評論   

# re: 訪問差異類型的集合類--visitor模式入門 2006-12-19 21:53 dreamstone

to hannyu : 
其實這就是visitor模式關鍵的地方,p.accept(v)的時候會發生向下轉型,所以執行的是子類的accept()方法,而子類的accept()方法中,傳入this就是子類而不是父類了。也就是說PA中調用v.run(this)其實傳入的this是PA類型,而不是P類型了,這樣visitor重載的時候就會執行run(Pa pa)而不是run(P p),所以不會死循環的。代碼在公司,明天把代碼上傳,你執行一下就知道了。 
(上邊就是說的自己認識自己了)  回覆  更多評論   

# re: 訪問差異類型的集合類--visitor模式入門 2006-12-19 21:56 dreamstone

to themax : 
可能我寫的有點歧意,我上邊的“把自己傳入2";是個label,相當於姓名一,姓名二,下邊解釋的是拿它來當label用,而不是把自己傳入給第二個,sorry。  回覆  更多評論   

# re: 訪問差異類型的集合類--visitor模式入門 2006-12-19 22:31 dreamstone

另外,加入了source的下載,見文章開始,雖然簡單,但是可以看看。 
個人感覺visitor模式是模式中比較複雜的。實現起來也是比較巧的.兩次this的利用。好像有一種學術的叫法"返還球"  回覆  更多評論   

# re: 訪問差異類型的集合類--visitor模式入門 2006-12-20 09:22 hannyu

多謝,看了源代碼終於明白了,visitor模式繞來繞去的真難理解。接口P應該只起來一個輔助作用,如果有個特殊點的命名就容易理解一些了。比如visitee?  回覆  更多評論   

# re: 訪問差異類型的集合類--visitor模式入門 2006-12-27 13:07 jounyc

我覺得把Visitor中 

public void run(Person p) 

p.accept(this); 


去掉也可以運行, 
因爲在 p.accept(v); 時父類向下轉型,執行子類的accept(Visitor v), 
然後就執行子類在Visitor中對應的run()了,和上面那段沒有關係阿。 

樓主出來解釋下。  回覆  更多評論   

# re: 訪問差異類型的集合類--visitor模式入門 2006-12-27 19:52 dreamstone[匿名]

呵呵,其實我當時寫這個是爲了這樣的情況,可能這種情況用的更多 
Visitor v = new Visitor(); 
Iterator ie = list.iterator(); 
while (ie.hasNext()) { 
P p = (P)ie.next(); 
v.run(p); 

這個時候那個函數就必須有了。因爲run(p)的時候必須在編譯器有一個對應的函數,這兩個的區別是看想問題的角度了。 
是讓visitor 訪問每一個p 
還是讓每一個P運行。 
不過更多情況應該是v.run(p);更復合思考的習慣。後來發文的時候簡化了一下。 


  回覆  更多評論   

# re: 訪問差異類型的集合類--visitor模式入門[未登錄] 2007-02-09 11:52 xmlspy

代碼中 

for(int i=0;i<list.size();i++){ 
P p = (P)list.get(i); 
p.accept(v); 


這種方式嚴重影響性能!! 

改成: 

P p =null; 
for(int i=0;i<list.size();i++){ 
p = (P)list.get(i); 
p.accept(v); 

  回覆  更多評論   

# re: 訪問差異類型的集合類--visitor模式入門 2007-02-28 12:17 dreamstone

to xmlspy: 
建議迴文的時候調查一下,你的說法是很錯誤的:具體說明見這裏 
http://www.blogjava.net/dreamstone/archive/2007/02/11/99207.aspx  回覆  更多評論   

# re: 訪問差異類型的集合類--visitor模式入門 2007-07-05 10:32 xnabx

通過抽象類可以非常方便的實現,也就是“自己找自己” 

//抽象類 P 
public abstract class P{ 
public abstract String run(); 


//類PA繼承抽象類P 
public class PA entends P{ 
public String run(){ 
return "PB"; 


//類PB繼承抽象類P 
public class PB entends P{ 
public String run(){ 
return "PB"; 


//類PC繼承抽象類P 
public class PC entends P{ 
public String run(){ 
return "PC"; 


//類PD繼承抽象類P 
public class PD entends P{ 
public String run(){ 
return "PD"; 


//類PE繼承抽象類P 
public class PE entends P{ 
public String run(){ 
return "PE"; 




List list = new ArrayList(); 
list.add(new PA()); 
list.add(new PB()); 
list.add(new PC()); 
list.add(new PD()); 
list.add(new PE()); 


Iterator ie = list.iterator(); 
while (ie.hasNext()) { 
Run p = (Run)ie.next(); 
p.run(); 
}   回覆  更多評論   

# re: 訪問差異類型的集合類--visitor模式入門 2007-07-05 11:21 xnabx

Iterator ie = list.iterator(); 
while (ie.hasNext()) { 
Run p = (Run)ie.next(); 
p.run(); 
} 應該是 

Iterator ie = list.iterator(); 
while (ie.hasNext()) { 
P p = (P)ie.next(); 
p.run(); 
}   回覆  更多評論   

# re: 訪問差異類型的集合類--visitor模式入門 2007-07-06 12:58 dreamstone

@xnabx 
你根本不理解什麼是visitor啊。 
你的那種實現方式是: 
調用一個集合類中,不同的對像的"同一個"方法。 
我的實現是: 
調用一個集合類中,不同的對象的"不同的"方法 

另外你曲解了demo的含義,我並不是未了打印出pa,pb,pc,pd只是爲了直觀那麼些。這有點像我問到美國怎麼走,你告訴我做300能上三環 

至於爲什麼需要這種需求,最開始寫了個簡單的引入,如果你開發過很複雜的系統你就會明白這種需求是經常存在的,特別是你在改造一個有很多已有模塊的工程。

 

摘自http://www.blogjava.net/dreamstone/archive/2006/12/18/88623.html

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