|
Visitor模式的可行與不可愛(ài) 作者/來(lái)源: Bruce Zhang
可否使用Visitor模式實(shí)現(xiàn)系列二的例子呢?經(jīng)過(guò)我的悉心研究,結(jié)論是Visitor模式對(duì)于本例而言,可行但不可愛(ài)! 我們先看看本例最初的類(lèi)圖:
我們可以將這個(gè)類(lèi)圖看作是一個(gè)樹(shù)形結(jié)構(gòu)。那么各個(gè)類(lèi)即可以看作是樹(shù)的枝葉節(jié)點(diǎn)了。這樣一說(shuō),似乎和Composite模式有些關(guān)系了。其實(shí)不然,因?yàn)槲宜^的樹(shù)枝節(jié)點(diǎn),如AudioMedia類(lèi),與MP3及WAV類(lèi)并非是聚合的關(guān)系。認(rèn)識(shí)到這一點(diǎn)很重要,為避免混淆視聽(tīng),我將這些類(lèi)只稱(chēng)作節(jié)點(diǎn)好了,就別提樹(shù)枝樹(shù)葉,避免產(chǎn)生誤會(huì)。 而Play()以及Resize()方法,就可以認(rèn)為是這些節(jié)點(diǎn)共同的行為,然而其行為的內(nèi)涵則是不相同的。也就是說(shuō),RM和MPEG雖然都有Play()方法,但Play的操作不同。也就是說(shuō),現(xiàn)在,我們可能會(huì)為這些不同的媒體類(lèi)型不斷提供更多的行為。而Visitor模式呢?其優(yōu)勢(shì)就在于: 為各節(jié)點(diǎn)增加新的行為,變得非常的容易。 也就是說(shuō),Visitor模式對(duì)于本例而言,是可行的。但這破壞了一個(gè)前提,就是如果適用Visitor模式必須要更改原來(lái)的設(shè)計(jì)架構(gòu),除非你愿意再為這些媒體類(lèi)分別進(jìn)行Wrap!好吧,既然是出于研究目的,我們就來(lái)看看Visitor模式的應(yīng)用。修改前提為:重構(gòu)原來(lái)的設(shè)計(jì),使舊有的媒體播放器,能夠非常容易地?cái)U(kuò)充行為。 使用Visitor模式,最大的特點(diǎn)是將被訪(fǎng)問(wèn)對(duì)象(通常稱(chēng)為節(jié)點(diǎn)或元素)的行為,與其對(duì)象分離。并用專(zhuān)門(mén)的Visitor對(duì)象,管理節(jié)點(diǎn)的行為。在本例中,抽象節(jié)點(diǎn)包括:AudioMedia和VedioMedia。其下的具體節(jié)點(diǎn)分別為:MP3、WAV和RM、MPEG。那么在Vistor對(duì)象,就應(yīng)該包括四個(gè)訪(fǎng)問(wèn)行為。 是這樣嗎?看來(lái)還有些問(wèn)題。從實(shí)際需求來(lái)分析,本例的節(jié)點(diǎn)應(yīng)分為兩大類(lèi)型:視頻媒體和音頻媒體。而這兩類(lèi)媒體的行為方法,可能是不相同的。例如,視頻媒體除了要求Play()方法外,可能還需要Resize()方法。而音頻媒體就沒(méi)有必要使用Resize()方法了,反而會(huì)需要視頻媒體不需要的方法ShowScript()。因此結(jié)論是:我們應(yīng)該分別為AudioMedia和VedioMedia建立不同的抽象Visitor類(lèi)。最后獲得應(yīng)用Visitor模式的類(lèi)圖如下:
其實(shí)此時(shí)IMedia接口已經(jīng)可以去掉,我之所以保留出于兩個(gè)目的: 1、為兩種類(lèi)型媒體保持類(lèi)型的抽象; 2、可以提供兩種類(lèi)型媒體同時(shí)具有,且不需要Visitor的行為; (注:此時(shí)的Play方法其實(shí)可以放到IMedia接口中。我仍然將其放到Visitor的目的是,便于說(shuō)明Visitor模式,同時(shí)通過(guò)這種方式,使對(duì)Play方法的修改較為容易) 請(qǐng)大家注意上圖的類(lèi)圖左側(cè),即我定義的媒體類(lèi)型對(duì)象區(qū),在這種Visitor模式設(shè)計(jì)的前提下,對(duì)于類(lèi)型的行為來(lái)說(shuō),是非常穩(wěn)定的。即:無(wú)論你要為這四種媒體類(lèi)型增加什么行為,都不需要修改該區(qū)域的任何類(lèi)或接口;而你只需要在Visitor下增加行為的具體Visitor類(lèi)即可。這就符合了OO思想中非常著名的開(kāi)—閉原則。左區(qū)即為相對(duì)的閉區(qū)間(我不能說(shuō)是絕對(duì)的對(duì)修改關(guān)閉,因此,我才用虛線(xiàn)來(lái)作界定),而右區(qū)則為相對(duì)的開(kāi)區(qū)間。 還是來(lái)看看源代碼: 首先是被訪(fǎng)問(wèn)的類(lèi)型:
public
interface IMedia
{}
public
abstract
class AudioMedia,IMedia
{
public
abstract
void Accept(AudioVisitor visitor);
}
public
class MP3:AudioMedia
{
public
override
void Accept(AudioVisitor visitor)
{
visitor.VisitMP3();
}
}
public
class WAV:AudioMedia
{
public
override
void Accept(AudioVisitor visitor)
{
visitor.VisitWAV();
}
}
public
abstract
class VedioMedia,IMedia
{
public
abstract
void Accept(VedioVisitor visitor);
}
public
class RM:VedioMedia
{
public
override
void Accept(VedioVisitor visitor)
{
visitor.VisitRM();
}
}
public
class MPEG:VedioMedia
{
public
override
void Accept(VedioVisitor visitor)
{
visitor.VisitMPEG();
}
}
請(qǐng)注意,在我的Visitor方法中,并沒(méi)有將節(jié)點(diǎn)或元素對(duì)象傳遞給Visitor。這一點(diǎn),與通常的Visitor模式實(shí)現(xiàn)小有區(qū)別。 下面是Visitor的實(shí)現(xiàn):
public
abstract
class AudioVisitor
{
public
abstract
void VisitMP3();
public
abstract
void VisitWAV();
}
public
class PlayAudioVisitor:AudioVisitor
{
public
override
void VisitMP3()
{
//實(shí)現(xiàn)MP3的Play方法;
}
public
override
void VisitWAV()
{
//實(shí)現(xiàn)WAV的Play方法;
}
}
public
class ShowScriptAudioVisitor:AudioVisitor
{
public
override
void VisitMP3()
{
//實(shí)現(xiàn)MP3的ShowScript方法;
}
public
override
void VisitWAV()
{
//實(shí)現(xiàn)WAV的ShowScript方法;
}
}
public
abstract
class VedioVisitor
{
public
abstract
void VisitRM();
public
abstract
void VisitMPEG();
}
public
class PlayVedioVisitor:VedioVisitor
{
public
override
void VisitRM()
{
//實(shí)現(xiàn)RM的Play方法;
}
public
override
void VisitMPEG()
{
//實(shí)現(xiàn)MPEG的Play方法;
}
}
public
class ResizeVedioVisitor:VedioVisitor
{
public
override
void VisitRM()
{
//實(shí)現(xiàn)RM的ShowScript方法;
}
public
override
void VisitMPEG()
{
//實(shí)現(xiàn)MPEG的ShowScript方法;
}
}
現(xiàn)在我解釋一下為什么不在Visitor方法中傳遞節(jié)點(diǎn)對(duì)象。從上面的Visitor實(shí)現(xiàn),可以看到。每個(gè)Visitor的Visit方法,實(shí)際上代表的就是各自的行為,或者是Play,或者是Resize,等等。而這些行為均是在Visitor中實(shí)現(xiàn)的,而非它訪(fǎng)問(wèn)的節(jié)點(diǎn)對(duì)象。也就是說(shuō),通過(guò)Visitor模式,我將各個(gè)媒體對(duì)象的行為都交給Visitor了。既然干活的對(duì)象發(fā)生了轉(zhuǎn)移,那么發(fā)生了什么責(zé)任,也就去找Visitor吧,這個(gè)責(zé)任可與媒體對(duì)象本身沒(méi)有關(guān)系了啊。 根據(jù)Visitor模式,一般還應(yīng)該提供結(jié)構(gòu)對(duì)象(ObjectStructure)角色。然而我在開(kāi)篇名義之處,就提到本例中,媒體類(lèi)型對(duì)象是不存在聚合關(guān)系的,因此不需要?jiǎng)跓㎡bjectStructure來(lái)枚舉每個(gè)節(jié)點(diǎn)了。也許,這個(gè)Visitor模式有些不倫不類(lèi)吧,沒(méi)關(guān)系,我們只需要了解這個(gè)模式的思想。 現(xiàn)在看看客戶(hù)端的調(diào)用:
public
class Client
{
public
static
void Main()
{
//調(diào)用視頻媒體的Play方法;
VedioMedia rm =
new RM();
rm.Accept(new PlayVedioVisitor());
//調(diào)用音頻媒體的ShowScript方法;
AudioMedia mp3 =
new MP3();
mp3.Accept(new ShowScriptAudioVisitor());
}
}
從上面的Visitor實(shí)現(xiàn)可以看到,每個(gè)Visitor的Visit方法,實(shí)際上代表的就是各自的行為,或者是Play,或者是Resize,等等。而這些行為均是在Visitor中實(shí)現(xiàn)的,而非它訪(fǎng)問(wèn)的節(jié)點(diǎn)對(duì)象。也就是說(shuō),通過(guò)Visitor模式,我將各個(gè)媒體對(duì)象的行為都交給Visitor了。既然干活的對(duì)象發(fā)生了轉(zhuǎn)移,那么發(fā)生了什么責(zé)任,也就去找Visitor吧,這個(gè)責(zé)任可與媒體對(duì)象本身沒(méi)有關(guān)系了哦。 如果要添加行為,那么同樣把責(zé)任交給Visitor吧。為該行為定義一個(gè)Visitor類(lèi),繼承抽象Visitor類(lèi)即可??纯幢辉L(fǎng)問(wèn)對(duì)象,因?yàn)閂isit方法接受的是抽象Visitor類(lèi)對(duì)象,Accept()方法對(duì)于各種行為是完全一致的,你自然不需要修改媒體對(duì)象區(qū)間的這些所有對(duì)象了。這也是Visitor模式最有價(jià)值的體現(xiàn)。例如,我想為視頻媒體增加一個(gè)行為Brighten(),該行為能夠讓畫(huà)面更亮。 首先定義一個(gè)Brighten的Visitor類(lèi):
public
class BrightenVedioVisitor:VedioVistor
{
public
void VisitRM(RM rm)
{
//增加亮度的方法實(shí)現(xiàn);
}
public
void VisitMPEG(MPEG mpeg)
{
//增加亮度的方法實(shí)現(xiàn);
}
}
我們來(lái)看看,不改變視頻媒體類(lèi)的任何代碼,能否調(diào)用Brighten方法?對(duì)了,很簡(jiǎn)單: rm.Accept(new BrightenVedioVisitor()); 看了上面的描述,也許你會(huì)認(rèn)為Visitor模式不僅是可行的,而且真的很可愛(ài)呢。但我卻要說(shuō)它是不可愛(ài)的,至少針對(duì)本例是如此。請(qǐng)?jiān)O(shè)想這樣幾種情形: 1、 假設(shè)你需要增加新的一種媒體文件,如WMV文件,在既有Visitor模式的架構(gòu)下,應(yīng)該怎樣?你頭疼了,因?yàn)閷?shí)在是太麻煩。你需要定義一個(gè)WAV類(lèi),繼承VedioMedia。最麻煩的是你必須修改VedioVisitor及其所有子類(lèi),因?yàn)閂isit方法中沒(méi)有包含訪(fǎng)問(wèn)WAV對(duì)象的行為。 2、 當(dāng)各種的對(duì)象的行為越來(lái)越多時(shí),怎么辦?你需要為每種行為都創(chuàng)建一個(gè)Visitor。例如我希望提供加亮的同時(shí),還能提供變暗的功能,是不是又要為其建立Visitor對(duì)象呢?假如這些行為所屬的Visitor對(duì)象之間,又沒(méi)有什么聚合的關(guān)系,即無(wú)法引入ObjectStructure來(lái)管理,你一定會(huì)煩不勝煩的。 3、 當(dāng)各種對(duì)象行為的外部接口非常復(fù)雜時(shí),例如傳遞復(fù)雜的對(duì)象,甚至可能會(huì)有out或ref參數(shù),同時(shí)還有返回對(duì)象,又該怎么辦?你一定注意到了,所有的Visit方法所代表的行為都沒(méi)有返回值,也沒(méi)有傳遞參數(shù)。如果真要解決,你恐怕只有為被訪(fǎng)問(wèn)對(duì)象引入屬性了。這不又要修改被訪(fǎng)問(wèn)對(duì)象嗎? 明白它的不可愛(ài)之處了吧。那么Vistor模式的優(yōu)勢(shì)又在哪里呢?上面其實(shí)已經(jīng)不厭其煩地說(shuō)過(guò)多次,那就是在保證被訪(fǎng)問(wèn)對(duì)象相對(duì)穩(wěn)定的情況下,為現(xiàn)有系統(tǒng)添加行為帶來(lái)了便宜。 尤其我要說(shuō),對(duì)于媒體播放器,Vistor模式非但不可愛(ài),而且丑陋。從現(xiàn)實(shí)角度考慮,我們要做一個(gè)媒體播放器,在設(shè)計(jì)之初,對(duì)于各種媒體應(yīng)具備的行為,通常能夠充分考慮到。且各種媒體的行為是大致相同的。然而對(duì)于媒體文件類(lèi)型,反而是無(wú)法估量的。說(shuō)不定,什么時(shí)候又會(huì)出現(xiàn)新成果來(lái)。從mp3到mp4,再到mpn,你能預(yù)知嗎?所以,根據(jù)本例而言,你是在利用Visitor的劣勢(shì)了。 為什么還要寫(xiě)本文?是因?yàn)槲蚁敫嬖V你兩點(diǎn): 1、 Visitor模式的優(yōu)勢(shì)與劣勢(shì); 2、 通過(guò)一個(gè)反面教材,有時(shí)候比正面教材給人印象更深; 讓Visitor模式用到它最擅長(zhǎng)的地方吧。讓它不僅可行,而且還要可愛(ài)! |
|
|
來(lái)自: ljjzlm > 《設(shè)計(jì)模式》