Visitor模式加深理解

benbenxiongyuan發表於2014-04-15

靜態分派,動態分派,多分派,單分派 -------------- visitor模式準備

一,靜態分派:
1,定義:發生在編譯時期,分派根據靜態型別資訊發生,過載就是靜態分派
2,什麼是靜態型別:變數被宣告時的型別是靜態型別
      什麼是動態型別:變數所引用的物件的真實型別
3,有兩個類,BlackCat ,WhiteCat都繼承自Cat
如下呼叫

class Cat{}
class WhiteCat extends Cat{}
class BlackCat extends Cat{}
public class Person {
    
public void feed(Cat cat){
        System.out.println(
"feed cat");
    }

    
public void feed(WhiteCat cat){
        System.out.println(
"feed WhiteCat");
    }

    
public void feed(BlackCat cat){
        System.out.println(
"feed BlackCat");
    }

    
public static void main(String[] args) {
        Cat wc 
= new WhiteCat();
        Cat bc 
= new BlackCat();
        Person p 
= new Person();
        p.feed(wc);
        p.feed(bc);
    }


}

執行結果是:
feed cat
feed cat
這樣的結果是因為過載是靜態分派,在編譯器執行的,取決於變數的宣告型別,因為wc ,bc都是Cat所以呼叫的都是feed(Cat cat)的函式.
二,動態分派
1,定義:發生在執行期,動態分派,動態的置換掉某個方法。
還是上邊類似的例子:
class Cat{
    
public void eat(){
        System.out.println(
"cat eat");
    }

}

public class BlackCat extends Cat{
    
public void eat(){
        System.out.println(
"black cat eat");
    }

    
public static void main(String[] args){
        Cat cat 
= new BlackCat();
        cat.eat();
    }

}

這個時候的結果是:
black cat eat
這樣的結果是因為在執行期發生了向下轉型,就是動態分派了。

三,單分派:
1,定義:根據一個宗量的型別進行方法的選擇
四,多分派:
1,定義:根據多於一個宗量的型別對方法的選擇
2,說明:多分派其實是一系列的單分派組成的,區別的地方就是這些但分派不能分割。
3,C++ ,Java都是動態單分派,靜態多分派語言
多分派的語言有:CLOS  Cecil

 

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

訪問差異型別的集合類--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模式概念------------------- visitor模式進一步

visitor模式理論及學術概念-------------------   visitor模式進一步
一,訪問者模式的角色:
抽象訪問者:宣告一個或者多個訪問操作,形成所有的具體元素都要實現的介面
具體訪問者:實現抽象訪問者所宣告的介面
抽象節點:宣告一個接受操作,接受一個訪問者物件作為參量
具體節點:實現了抽象元素所規定的接受操作
結構物件:遍歷結構中的所有元素,類似List Set等
二,在什麼情況下應當使用訪問者模式
訪問者模式應該用在被訪問類結構比較穩定的時候,換言之系統很少出現增加新節點的
情況。因為訪問者模式對開-閉原則的支援並不好,訪問者模式允許在節點中加入方法,
是傾斜的開閉原則,類似抽象工廠。
三,訪問者模式的缺點:
1,增加節點困難
2,破壞了封裝
因為訪問者模式的缺點和複雜性,很多設計師反對使用訪問者模式。個人感覺應該在瞭解的
情況下考慮衡量選擇。

過載overloading和覆寫overriding哪個更早起作用-- visitor幫助篇

接受建議,改一下標題.例子不太恰當,我刪除了。換成了迴文中的例子。
過載overloading和覆寫overriding哪個更早執行--   visitor幫助篇
一:問題提出
雖然我們經常寫程式用到過載和覆寫,但是很少會考慮他們的執行順序。下邊的內容就是關於,他們同時出現時
哪個先起作用:
二:問題分析
Java是"動態單分派靜態多分派語言",這個定義已經多次提起,如果你不瞭解這些概念,看這裡"visitor模式準備"
所以就註定了過載(靜態多分派)要早於覆寫(動態單分派),因為靜態分派是編繹期實現的,動態分派是執行期實現的。
三:驗證

見這裡

相關文章