VISITOR模式 —— 齊天大圣鬧天宮 junguo Visitor模式的中文名稱是訪問者模式,該模式的目的是提供一個類來操作其它類型中的對象結(jié)構(gòu)中的元素(也就是專門幫助其它類來實現(xiàn)原本屬于它的函數(shù))。它使你可以在不改變各元素類的前提下定義作用于這些元素的新操作。是不是不明白這段話的意思?沒關(guān)系,還是通過例子來理解該模式。我們先來簡述一下例子。
呵呵,好不容易想到這么個土的掉渣的例子。別見怪,我實在想不出更好的例子。例子的背景大家應(yīng)該都非常熟悉,在這兒就不扯淡了。簡要描述一下我們要實現(xiàn)的功能。大家都知道在大鬧天宮中,有二郎神和孫悟空打斗的情節(jié)。他們兩個都有七十二變,七十二變在我們的例子里相當于七十二個方法。但他們的變化并不相同,如孫悟空變成廟的時候,尾巴變不掉,會變成一個旗桿;而二郎神沒有尾巴。所以這里把他們各自封裝。幫它們各自提供一個類。妖怪類Sprite和神仙類God看以下的類圖。
好了,有了類圖,開始開發(fā)。此次要實現(xiàn)的功能主要是幫助這兩個類來實現(xiàn)它的七十二個方法。我們知道程序肯定不是一次寫完的,每填加幾個函數(shù),我們就想進行一下單元測試,看看自己的代碼有沒有錯誤。這時候,我們就需要重新編譯程序。由于這兩個類的方法比較多,這樣每填加一個函數(shù),就可能需要把這整個類文件都重新編譯一次。這是件耗費時間的事,你不想看到。那么有沒有辦法幫我們解決該問題呢?有的,就是現(xiàn)在提到的Visitor模式。我們可以把Sprite和God的所有操作提煉成一個一個單獨的類,在這些類中完成原本屬于它們的方法。怎么做呢?先來看看類圖。
在類圖中,你可以看到我們提煉了一個新類Visitor,它有兩個子類Change1Vistor和Change2Vistor(如果有其它方法的話,我們可以添加新的類)。Visitor就是我們所說的訪問者類了,就是要通過它來幫助我們把所有的方法都提煉到單獨的類中。而Change1Vistor和Change2Vistor就是我們所要的具體類,用它來幫助我們實現(xiàn)神仙和妖怪的變化。我們說過了它們可能有七十二種變化,那么我們再填加新的變化的時候,就不需要去修改Sprite和God類的內(nèi)容了。(當然了,如果真填加七十二中變化的話,這代碼也夠受的,估摸也好不到哪兒去,真有這樣的系統(tǒng),你可能需要去尋找其它方法了。)
我們再看看我們的Sprite和God,它們擁有共同的基類SuperMan,它只有一個虛擬函數(shù) Accept(Visitor &) ,就是通過它來實現(xiàn)對Visitor類的調(diào)用了。Sprite和God類各自實現(xiàn)該方法。為了體現(xiàn)Visitor的真正意義,我們給Sprite和God各自添加了成員變量,其實Visitor的目的主要是幫助處理類中的數(shù)據(jù)成員了。在后面我們將講述這個問題。好了,還是先來看看具體的代碼,所先來看Visitor的代碼:
// Visitor基類class Visitor{public://抽象出來針對于Sprite對象的方法virtual void VisitorSprite(Sprite *p) = 0;//抽象出來針對God對象的方法virtual void VisitorGod(God *p) = 0;protected:Visitor(){}};//針對于SuperMan的第一個操作class Change1Vistor : public Visitor{public:void VisitorSprite(Sprite *p){cout << "這是妖怪 " << p->GetName() << " 的變化1" << endl;};void VisitorGod(God *p){cout << "這是神仙 " << p->GetType() <<" 的變化1" << endl;};};//針對于SuperMan的第一個操作class Change2Vistor : public Visitor{public:void VisitorSprite(Sprite *p){cout << "這是妖怪的變化2" << endl;};void VisitorGod(God *p){cout << "這是神仙的變化2" << endl;};};
這樣,當我們有新的操作需要的時候,我們就可以重新生成一個類Change3Visitor,Change4Visitor等等,只要它們都繼承于Visitor就可以了。這樣你可以產(chǎn)生新的文件,而無需重新編譯以前的類文件了。
我們再來看一下SuperMan的實現(xiàn):
//超人類class SuperMan{public:virtual ~SuperMan(){}//抽象出來的調(diào)用方法的接口virtual void Accept(Visitor &) = 0;protected:SuperMan(){}};//妖怪類class Sprite : public SuperMan{private:string m_strName;public:Sprite(string strName):m_strName(strName){}string GetName(){return m_strName;}void Accept(Visitor &v);};//神仙類class God : public SuperMan{private:string m_strType;public:God(string strType):m_strType(strType){}string GetType(){return m_strType;}void Accept(Visitor &v);};//Sprite類的Accept實現(xiàn)void Sprite::Accept(Visitor &v){v.VisitorSprite(this);}//God類的Accept實現(xiàn)void God::Accept(Visitor &v){v.VisitorGod(this);}
此處,我們需要注意的問題就是Accept的具體實現(xiàn)了。Sprite和God類需要分別調(diào)用針對于自己的接口函數(shù)。這里我們還應(yīng)該考慮到一個問題,就是SuperMan的子類應(yīng)該相對固定,不應(yīng)該太多的變動。當它增加一個子類的時候,Visitor接口就需要變化,而相應(yīng)的Visitor的所有子類也需要進行相應(yīng)的變化。那么這樣與該模式的初衷節(jié)省編譯時間就背道而馳了,可能要花費更多的編譯時間。
再來看看它的使用方法:
int main(int argc, char* argv[]){Sprite sp("孫悟空");God g("天神,不是地府之神");//變化1Change1Vistor c1;sp.Accept(c1);g.Accept(c1);//變化2Change2Vistor c2;sp.Accept(c2);g.Accept(c2);return 0;}
我們可以看到Sprite和God對象調(diào)用相應(yīng)函數(shù)的方法都可以通過Accept來實現(xiàn)。我們這里的實現(xiàn)比較簡單,只是生成了一個Sprite和一個God類,而實際應(yīng)用中它可能是一個列表,數(shù)組或者是一個組合(Composite),不過原理一致。其它方式大不過就是需要遍歷所有的元素,并調(diào)用該方法。
再簡單介紹一下《設(shè)計模式》中對該模式提供的例子。在編譯器的實現(xiàn)過程中,會將所有源程序組合成一個語法樹。該語法樹中包括變量,賦值語句,判斷語句等內(nèi)容,這些內(nèi)容都是一個單獨的類,而該些類有一個統(tǒng)一的基類。這些類通過Composite模式組織成一個結(jié)構(gòu),也就是語法樹。
在這樣的語法樹上,可能有這樣一些操作:類型檢查,代碼優(yōu)化等等,可能還有牽涉打印等,也就是操作是在不斷變化的。而樹的內(nèi)容相對來說是比較固定的。這樣的話,使用Visitor就可以把這些操作獨立出來。使新的操作不至于影響原有的類。也不會使單個的類變的臃腫。
好了,這就是這次所要說的Visitor模式了。我們可以看到Visitor的初衷是為了節(jié)省編譯時間也產(chǎn)生的。所以也可以這么說:設(shè)計模式是開發(fā)經(jīng)驗的總結(jié),學習它的目的也就是能幫入門者更快的達到真正理解面向?qū)ο蟮乃健?
參考書目:1, 設(shè)計模式——可復用面向?qū)ο筌浖幕A(chǔ)(Design Patterns ——Elements of Reusable Object-Oriented Software) Erich Gamma 等著 李英軍等譯 機械工業(yè)出版社2, Head First Design Patterns(影印版)Freeman等著 東南大學出版社3, 道法自然——面向?qū)ο髮嵺`指南 王詠武 王詠剛著 電子工業(yè)出版