大衛的Design Patterns學習筆記23:Vistor

  大衛的Design Patterns學習筆記23:Vistor

 
一、概述
前面已經討論過的Adapter模式告訴我們如何應對接口不一致對我們的設計造成的影響,但是,這並不能在如下的Context下發揮多大的作用:
一個類系中的多個類要求支持相同的操作,但是這些類提供的接口並不一致。
看到這裏,你可能會說,我幹嘛要用什麼Adapter?我纔沒那麼笨呢,我直接修改整個類系的接口方法,添加新的統一的接口方法不就OK了?
確實如此,但是,如果你湊巧使用的是另一個不方便修改的模塊的代碼呢?
在這種情況下,試試Visitor(訪問者)模式吧。
Visitor模式用於表示一個作用於某對象結構中的各元素的操作,使你可以在不改變各元素的類的前提下定義作用於這些元素的新操作。
Visitor模式藉助所謂的“Double-Dispatch(雙分派)”來達到以上目的。在C++、Java這樣的典型的強類型、單分派語言中,我們通常使用的都是單分派,單分派的意思就是執行的方法是由調用者而不是調用參數決定的,譬如
a.add(b)
那麼,這時執行的方法就是由a來決定的。如果直接支持雙分派的語言,那麼執行這個方法就可以根據a和b兩個的類型來決定。
前面說過,C++、Java等並不能直接支持雙分派,因此,要在C++、Java中支持所謂的雙分派,必須通過增加額外的附加層和方法來實現。
有兩種主要的方式來實現Double-Dispatch,一種是type-switch(以下代碼取自JDK):
protected void processEvent(AWTEvent e) {
        if (e instanceof FocusEvent) {
            processFocusEvent((FocusEvent)e);
        } else if (e instanceof MouseEvent) {
            switch(e.getID()) {
              case MouseEvent.MOUSE_PRESSED:
              case MouseEvent.MOUSE_RELEASED:
              case MouseEvent.MOUSE_CLICKED:
              case MouseEvent.MOUSE_ENTERED:
              case MouseEvent.MOUSE_EXITED:
                processMouseEvent((MouseEvent)e);
                break;
              case MouseEvent.MOUSE_MOVED:
              case MouseEvent.MOUSE_DRAGGED:
                processMouseMotionEvent((MouseEvent)e);
                break;
              case MouseEvent.MOUSE_WHEEL:
                processMouseWheelEvent((MouseWheelEvent)e);
                break;
            }
        } else if (e instanceof KeyEvent) {
            processKeyEvent((KeyEvent)e);
        } else if (e instanceof ComponentEvent) {
            processComponentEvent((ComponentEvent)e);
        } else if (e instanceof InputMethodEvent) {
            processInputMethodEvent((InputMethodEvent)e);
        } else if (e instanceof HierarchyEvent) {
           switch (e.getID()) {
             case HierarchyEvent.HIERARCHY_CHANGED:
              processHierarchyEvent((HierarchyEvent)e);
              break;
             case HierarchyEvent.ANCESTOR_MOVED:
             case HierarchyEvent.ANCESTOR_RESIZED:
              processHierarchyBoundsEvent((HierarchyEvent)e);
              break;
           }
       }
}
這種方式通過檢查b的類型信息進行Re-Dispatch,雖然type-switch在設計上比較簡單,但type-switch是OOD中應當儘量避免使用的技術,因爲它可能給我們的代碼引入一些難以察覺的Bug,以下面的代碼爲例(Java Code):
class A {
}
 
class B extends A {
}
 
public class Test {
       static public void main(String[] args){
              B b = new B();
              if (b instanceof A) {
                     System.out.println("b is an instanceof A");
              } else if (b instanceof B) {
                     System.out.println("b is an instanceof B");
              }
       }
}
程序運行的結果是:
b is an instanceof A
雖然從邏輯上講,這個結論是正確的,但這顯然不是我們期望的答案。要讓上面的程序輸出“b is an instanceof B”,需要調整上面的if判斷的順序,使子類判斷出現在基類判斷之前,由特殊到普通,否則,父類判斷將屏蔽掉子類判斷,對於簡單的類型判斷,使用type-switch是個不錯的選擇,但是當繼承體系變得十分複雜時,判斷順序上的問題可能給你帶來意想不到的麻煩(當然,還有別的辦法,如通過getClass來檢查類名信息,但這種方式比上面的方式也好不到哪裏去)。
還有一種Double-Dispatch實現方式在使用上相對較爲安全,但實現較爲複雜,而且需要更多的設計技巧,Visitor模式採用的是這一種形式。以下面的函數調用爲例:
a.add(Number b)
我們可以在add函數體內採用type-switch方法對b進行判斷,完成add操作,也可以像下面這樣:
對於整數類型,定義:
public class Integer {
Number add(Number b) {
return b.add(a);
}
...
}
則不管b是什麼類型,只要它實現了add(Integer a)這個方法,就可以準確完成add操作。
對於浮點類型,定義:
public class Float {
Number add(Number b) {
return b.add(a);
}
...
}
則不管b是什麼類型,只要它實現了add(Float a)這個方法,就可以準確完成add操作。
可以看到,這時候把到底執行哪一個方法轉交給了b,而且,在b執行該方法時,已經有了a的類型信息,因此,無需再進行type-switch。
那麼這種複雜的Double-Dispatch技術有什麼好處呢?它的好處之一在於可以使我們在不改變a的同時,通過對b進行擴充,達到爲a提供新的功能的目的。以上面的add爲例,我們可以從Number派生出一種新的數值類型,在其中實現各種add操作,則可以在不改變已有數值類型的基礎上與之協同工作。當然,由於在使用上存在一些限制,限制了Double-Dispatch的應用,後面將對此進一步進行說明。
 
二、結構
Visitor模式的結構如下圖所示:
圖1、Visitor模式的示意類圖
其中包括以下組成部分:
Visitor(訪問者):爲該對象結構中ConcreteElement的每一個類聲明一個Visit操作。該操作的名字和特徵標識了發送Visit請求給該訪問者的那個類。這使得訪問者可以確定正被訪問元素
的具體的類。這樣訪問者就可以通過該元素的特定接口直接訪問它。
ConcreteVisitor(具體訪問者):實現每個由Visitor聲明的操作。每個操作實現本算法的一部分,而該算法片斷乃是對應於結構中對象的類。ConcreteVisitor爲該算法提供了上下文並存儲它的局部狀態,這一狀態常常在遍歷該結構的過程中累積結果。
Element(元素):定義一個Accept操作,它以一個訪問者爲參數。
ConcreteElement(具體元素):實現Accept操作,該操作以一個訪問者爲參數。
ObjectStructure(對象結構,如Program):能枚舉它的元素;可以提供一個高層的接口以允許該訪問者訪問它的元素;可以是一個複合或是一個集合,如一個列表或一個無序集合。
 
三、應用
在下列情況下可考慮使用Visitor模式:
1、一個對象結構包含很多類對象,它們有不同的接口,而你想對這些對象實施一些依賴於其具體類的操作。
2、需要對一個對象結構中的對象進行很多不同的並且不相關的操作,而你想避免讓這些操作“污染”這些對象的類。Visitor使得你可以將相關的操作集中起來定義在一個類中。當該對象結構被很多應用共享時,用Visitor模式讓每個應用僅包含需要用到的操作。
3、定義對象結構的類很少改變,但經常需要在此結構上定義新的操作。改變對象結構類需要重定義對所有訪問者的接口,這可能需要很大的代價。如果對象結構類經常改變,那麼可能還是在這些類中定義這些操作較好。
 
四、優缺點
Visitor模式通過擴充新的繼承體系來爲已有的繼承體系提供新的針對特殊類型的功能(達到與添加虛成員函數相同的效果),適用於十分穩定,並執行繁重處理的繼承體系。下面是訪問者模式的一些優缺點:
1、Visitor模式使得易於增加新的操作訪問者使得增加依賴於複雜對象結構的構件的操作變得容易了。僅需增加一個新的訪問者即可在一個對象結構上定義一個新的操作。相反,如果每個功能都分散在多個類之上的話,定義新的操作時必須修改每一類。
2、訪問者集中相關的操作而分離無關的操作相關的行爲不是分佈在定義該對象結構的各個類上,而是集中在一個訪問者中。無關行爲卻被分別放在它們各自的訪問者子類中。這就既簡化了這些元素的類,也簡化了在這些訪問者中定義的算法。所有與它的算法相關的數據結構都可以被隱藏在訪問者中。
3、增加新的ConcreteElement類很困難Visitor模式使得難以增加新的Element的子類。每添加一個新的ConcreteElement都要在Visitor中添加一個新的抽象操作,並在每一個ConcreteVisitor類中實現相應的操作。有時可以在Visitor中提供一個缺省的實現,這一實現可
以被大多數的ConcreteVisitor繼承,但這與其說是一個規律還不如說是一種例外。
所以在應用訪問者模式時考慮關鍵的問題是系統的哪個部分會經常變化,是作用於對象結構上的算法呢,還是構成該結構的各個對象的類。如果老是有新的ConcreteElement類加入進來的話,Visitor類層次將變得難以維護。在這種情況下,直接在構成該結構的類中定義這些操作可能更容易一些。如果Element類層次是穩定的,而你不斷地增加操作獲修改算法,Visitor模式可以幫助你管理這些改動。
4、通過類層次進行訪問一個迭代器可以通過調用節點對象的特定操作來遍歷整個對象結構,同時訪問這些對象。但是迭代器不能對具有不同元素類型的對象結構進行操作。
5、累積狀態當訪問者訪問對象結構中的每一個元素時,它可能會累積狀態。如果沒有訪問者,這一狀態將作爲額外的參數傳遞給進行遍歷的操作,或者定義爲全局變量。
6、破壞封裝訪問者方法假定ConcreteElement接口的功能足夠強,足以讓訪問者進行它們的工作。結果是,該模式常常迫使你提供訪問元素內部狀態的公共操作,這可能會破壞它的封裝性。
 
五、舉例
個人認爲,Visitor模式是GoF所列舉的23種模式中最複雜的,同時由於其使用上的約束較多,日常的應用並不太多,在此不準備對其進行進一步的討論。以下示例簡單演示了Visitor模式的實現方法(Java Code):
 
abstract class Parts {
       abstract void accept(Visitor visitor);
}
 
// component class: Wheel
class Wheel extends Parts{
    private String name;
    Wheel(String name){
        this.name = name;
    }
    String getName(){
        return this.name;
    }
    void accept(Visitor visitor){   // function to support double-dispatch
        visitor.visit(this);
    }
}
 
// component class: Engine
class Engine extends Parts{
    void accept(Visitor visitor){
        visitor.visit(this);
    }
}
 
// component class: Body
class Body extends Parts{
    void accept(Visitor visitor){
        visitor.visit(this);
    }
}
 
// class to demonstrate visitor pattern and double-dispatch. If we don't use double-dispatch, we will lost all class info when we put all components into an array.
class Car{
    private Parts[] parts
        = { new Engine(), new Body(), new Wheel("front left"), new Wheel("front right"),
            new Wheel("back left") , new Wheel("back right") };
    void accept(Visitor visitor){
        visitor.visit(this);
        for(int i=0; i<parts.length; ++i)
            parts[i].accept( visitor );
    }
}
 
// visitor interface, all concrete visitor class must implement it.
interface Visitor{    // need a access-function for each element class in the class-hierachy
    void visit(Wheel wheel);
    void visit(Engine engine);
    void visit(Body body);
    void visit(Car car);
}
 
// concrete visitor: PrintVisitor
class PrintVisitor implements Visitor{
    public void visit(Wheel wheel){
        System.out.println("Visiting "+wheel.getName()
                            +" wheel");
    }
    public void visit(Engine engine){
        System.out.println("Visiting engine");
    }
    public void visit(Body body){
        System.out.println("Visiting body");
    }
    public void visit(Car car){
        System.out.println("Visiting car");
    }
}
 
// more concrete visitor class, omitted...
 
// entry class
public class VisitorDemo{
    static public void main(String[] args){
        Car car = new Car();
        Visitor visitor = new PrintVisitor();
        car.accept(visitor);
    }
}
 
以下是上述示例對應的C++實現:
 
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
 
class Wheel;
class Engine;
class Body;
class Car;
// visitor interface, all concrete visitor class must implement it.
// need a access-function for each element class in the class-hierachy
struct Visitor
{
       virtual void visit(Wheel& wheel) = 0;
       virtual void visit(Engine& engine) = 0;
       virtual void visit(Body& body) = 0;
       virtual void visit(Car& car) = 0;
};
 
struct Parts
{
       virtual void accept(Visitor& visitor) = 0;
};
 
// component class: Wheel
class Wheel : public Parts{
private:
       string name;
public:
       Wheel(string name)
       {
              this->name = name;
       }
       string getName()
       {
              return this->name;
       }
       void accept(Visitor& visitor) // function to support double-dispatch
       {
              visitor.visit(*this);
       }
};
 
// component class: Engine
class Engine : public Parts
{
       void accept(Visitor& visitor)
       {
              visitor.visit(*this);
       }
};
 
// component class: Body
class Body : public Parts
{
       void accept(Visitor& visitor)
       {
              visitor.visit(*this);
       }
};
 
// class to demonstrate visitor pattern and double-dispatch.
// If we don't use double-dispatch, we will lost all class info
// when we put all components into an array.
class Car
{
       struct DeleteFunctor
       {
              void operator () (Parts* p)
              {
                     delete p;
              }
       };
 
       class AcceptFunctor
       {
              Visitor& visitor;
       public:
              AcceptFunctor(Visitor& visitor) : visitor(visitor) {}
              void operator () (Parts* p)
              {
                     p->accept(visitor);
              }
       };
private:
       vector<Parts*> vParts;
public:
       Car()
       {
              Parts* pParts[] =
              { new Engine(), new Body(), new Wheel("front left"), new Wheel("front right"),
              new Wheel("back left") , new Wheel("back right") };
              vector<Parts*> v(pParts, pParts + sizeof(pParts) / sizeof(Parts*));
              swap(vParts, v);
       }
 
       virtual ~Car()
       {
              //for_each(vParts.begin(), vParts.end(), DeleteFunctor());
              for(vector<Parts*>::iterator it = vParts.begin(); it != vParts.end(); ++it)
                     delete *it;
       }
 
       void accept(Visitor& visitor)
       {
              visitor.visit(*this);
 
              //for_each(vParts.begin(), vParts.end(), AcceptFunctor(visitor));       
              for(vector<Parts*>::iterator it = vParts.begin(); it != vParts.end(); ++it)
                     (*it)->accept( visitor );
       }
};
 
// concrete visitor: PrintVisitor
class PrintVisitor : public Visitor{
public:
       void visit(Wheel& wheel)
       {
              cout << "Visiting " + wheel.getName() + " wheel" << endl;
       }
 
       void visit(Engine& engine)
       {
              cout << "Visiting engine" << endl;
       }
 
       void visit(Body& body)
       {
              cout << "Visiting body" << endl;
       }
 
       void visit(Car& car)
       {
              cout << "Visiting car" << endl;
       }
};
 
// more concrete visitor class, omitted...
 
// entry function
 
int main()
{
       Car car;
       Visitor* pVisitor = new PrintVisitor;
       car.accept(*pVisitor);
 
       return 0;
}
 
在上述實現中,我們可以發現,Visitor模式雖然使得爲已有的類型添加新的虛函數的需求變得容易實現,但是,Element類型與Visitor類型之間的耦合十分嚴重,出現了循環依賴,Visitor需要有所有Element子類的聲明,而所有Element子類也需要包含Visitor類的頭文件,當需要增加新的Element類型時,由於Visitor類的改動,將造成Element繼承體系和Visitor繼承體系全部需要重新編譯。
那麼有什麼辦法來減輕耦合呢?Martin Robert在其http://www.objectmentor.com/resources/articles/acv.pdf
一文中提出了爲每一個Element類型實現一個ConcreteVisitor,並最終通過多繼承來實現IntegratedConcreteVisitor以解除這種耦合關係的實現方法,但這種實現方法使得繼承體系變得更加複雜,同時還存在一些其它的開銷,感興趣的朋友可以參考<Modern C++ Design>或Robert的文章。
 
附:
1、           關於如何在Java中用Rflection實現Visitor模式,見http://www.javaworld.com/javatips/jw-javatip98_p.html
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章