Visitor模式的可行与不可爱 作者/来源: Bruce Zhang
可否使用Visitor模式实现系列二的例子呢?经过我的悉心研究,结论是Visitor模式对于本例而言,可行但不可爱! 我们先看看本例最初的类图: 我们可以将这个类图看作是一个树形结构。那么各个类即可以看作是树的枝叶节点了。这样一说,似乎和Composite模式有些关系了。其实不然,因为我所谓的树枝节点,如AudioMedia类,与MP3及WAV类并非是聚合的关系。认识到这一点很重要,为避免混淆视听,我将这些类只称作节点好了,就别提树枝树叶,避免产生误会。 而Play()以及Resize()方法,就可以认为是这些节点共同的行为,然而其行为的内涵则是不相同的。也就是说,RM和MPEG虽然都有Play()方法,但Play的操作不同。也就是说,现在,我们可能会为这些不同的媒体类型不断提供更多的行为。而Visitor模式呢?其优势就在于: 为各节点增加新的行为,变得非常的容易。 也就是说,Visitor模式对于本例而言,是可行的。但这破坏了一个前提,就是如果适用Visitor模式必须要更改原来的设计架构,除非你愿意再为这些媒体类分别进行Wrap!好吧,既然是出于研究目的,我们就来看看Visitor模式的应用。修改前提为:重构原来的设计,使旧有的媒体播放器,能够非常容易地扩充行为。 使用Visitor模式,最大的特点是将被访问对象(通常称为节点或元素)的行为,与其对象分离。并用专门的Visitor对象,管理节点的行为。在本例中,抽象节点包括:AudioMedia和VedioMedia。其下的具体节点分别为:MP3、WAV和RM、MPEG。那么在Vistor对象,就应该包括四个访问行为。 是这样吗?看来还有些问题。从实际需求来分析,本例的节点应分为两大类型:视频媒体和音频媒体。而这两类媒体的行为方法,可能是不相同的。例如,视频媒体除了要求Play()方法外,可能还需要Resize()方法。而音频媒体就没有必要使用Resize()方法了,反而会需要视频媒体不需要的方法ShowScript()。因此结论是:我们应该分别为AudioMedia和VedioMedia建立不同的抽象Visitor类。最后获得应用Visitor模式的类图如下: 其实此时IMedia接口已经可以去掉,我之所以保留出于两个目的: 1、为两种类型媒体保持类型的抽象; 2、可以提供两种类型媒体同时具有,且不需要Visitor的行为; (注:此时的Play方法其实可以放到IMedia接口中。我仍然将其放到Visitor的目的是,便于说明Visitor模式,同时通过这种方式,使对Play方法的修改较为容易) 请大家注意上图的类图左侧,即我定义的媒体类型对象区,在这种Visitor模式设计的前提下,对于类型的行为来说,是非常稳定的。即:无论你要为这四种媒体类型增加什么行为,都不需要修改该区域的任何类或接口;而你只需要在Visitor下增加行为的具体Visitor类即可。这就符合了OO思想中非常著名的开—闭原则。左区即为相对的闭区间(我不能说是绝对的对修改关闭,因此,我才用虚线来作界定),而右区则为相对的开区间。 还是来看看源代码: 首先是被访问的类型: 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(); } } 请注意,在我的Visitor方法中,并没有将节点或元素对象传递给Visitor。这一点,与通常的Visitor模式实现小有区别。 下面是Visitor的实现: public
abstract
class AudioVisitor {
public
abstract
void VisitMP3();
public
abstract
void VisitWAV(); } public
class PlayAudioVisitor:AudioVisitor {
public
override
void VisitMP3()
{ //实现MP3的Play方法; }
public
override
void VisitWAV()
{ //实现WAV的Play方法; } } public
class ShowScriptAudioVisitor:AudioVisitor {
public
override
void VisitMP3()
{ //实现MP3的ShowScript方法; }
public
override
void VisitWAV()
{ //实现WAV的ShowScript方法; } } public
abstract
class VedioVisitor {
public
abstract
void VisitRM();
public
abstract
void VisitMPEG(); } public
class PlayVedioVisitor:VedioVisitor {
public
override
void VisitRM()
{ //实现RM的Play方法; }
public
override
void VisitMPEG()
{ //实现MPEG的Play方法; } } public
class ResizeVedioVisitor:VedioVisitor {
public
override
void VisitRM()
{ //实现RM的ShowScript方法; }
public
override
void VisitMPEG()
{ //实现MPEG的ShowScript方法; } } 现在我解释一下为什么不在Visitor方法中传递节点对象。从上面的Visitor实现,可以看到。每个Visitor的Visit方法,实际上代表的就是各自的行为,或者是Play,或者是Resize,等等。而这些行为均是在Visitor中实现的,而非它访问的节点对象。也就是说,通过Visitor模式,我将各个媒体对象的行为都交给Visitor了。既然干活的对象发生了转移,那么发生了什么责任,也就去找Visitor吧,这个责任可与媒体对象本身没有关系了啊。 根据Visitor模式,一般还应该提供结构对象(ObjectStructure)角色。然而我在开篇名义之处,就提到本例中,媒体类型对象是不存在聚合关系的,因此不需要劳烦ObjectStructure来枚举每个节点了。也许,这个Visitor模式有些不伦不类吧,没关系,我们只需要了解这个模式的思想。 现在看看客户端的调用: public
class Client {
public
static
void Main()
{ //调用视频媒体的Play方法; VedioMedia rm =
new RM(); rm.Accept(new PlayVedioVisitor()); //调用音频媒体的ShowScript方法; AudioMedia mp3 =
new MP3(); mp3.Accept(new ShowScriptAudioVisitor()); } } 从上面的Visitor实现可以看到,每个Visitor的Visit方法,实际上代表的就是各自的行为,或者是Play,或者是Resize,等等。而这些行为均是在Visitor中实现的,而非它访问的节点对象。也就是说,通过Visitor模式,我将各个媒体对象的行为都交给Visitor了。既然干活的对象发生了转移,那么发生了什么责任,也就去找Visitor吧,这个责任可与媒体对象本身没有关系了哦。 如果要添加行为,那么同样把责任交给Visitor吧。为该行为定义一个Visitor类,继承抽象Visitor类即可。看看被访问对象,因为Visit方法接受的是抽象Visitor类对象,Accept()方法对于各种行为是完全一致的,你自然不需要修改媒体对象区间的这些所有对象了。这也是Visitor模式最有价值的体现。例如,我想为视频媒体增加一个行为Brighten(),该行为能够让画面更亮。 首先定义一个Brighten的Visitor类: public
class BrightenVedioVisitor:VedioVistor {
public
void VisitRM(RM rm)
{ //增加亮度的方法实现; }
public
void VisitMPEG(MPEG mpeg)
{ //增加亮度的方法实现; } } 我们来看看,不改变视频媒体类的任何代码,能否调用Brighten方法?对了,很简单: rm.Accept(new BrightenVedioVisitor()); 看了上面的描述,也许你会认为Visitor模式不仅是可行的,而且真的很可爱呢。但我却要说它是不可爱的,至少针对本例是如此。请设想这样几种情形: 1、 假设你需要增加新的一种媒体文件,如WMV文件,在既有Visitor模式的架构下,应该怎样?你头疼了,因为实在是太麻烦。你需要定义一个WAV类,继承VedioMedia。最麻烦的是你必须修改VedioVisitor及其所有子类,因为Visit方法中没有包含访问WAV对象的行为。 2、 当各种的对象的行为越来越多时,怎么办?你需要为每种行为都创建一个Visitor。例如我希望提供加亮的同时,还能提供变暗的功能,是不是又要为其建立Visitor对象呢?假如这些行为所属的Visitor对象之间,又没有什么聚合的关系,即无法引入ObjectStructure来管理,你一定会烦不胜烦的。 3、 当各种对象行为的外部接口非常复杂时,例如传递复杂的对象,甚至可能会有out或ref参数,同时还有返回对象,又该怎么办?你一定注意到了,所有的Visit方法所代表的行为都没有返回值,也没有传递参数。如果真要解决,你恐怕只有为被访问对象引入属性了。这不又要修改被访问对象吗? 明白它的不可爱之处了吧。那么Vistor模式的优势又在哪里呢?上面其实已经不厌其烦地说过多次,那就是在保证被访问对象相对稳定的情况下,为现有系统添加行为带来了便宜。 尤其我要说,对于媒体播放器,Vistor模式非但不可爱,而且丑陋。从现实角度考虑,我们要做一个媒体播放器,在设计之初,对于各种媒体应具备的行为,通常能够充分考虑到。且各种媒体的行为是大致相同的。然而对于媒体文件类型,反而是无法估量的。说不定,什么时候又会出现新成果来。从mp3到mp4,再到mpn,你能预知吗?所以,根据本例而言,你是在利用Visitor的劣势了。 为什么还要写本文?是因为我想告诉你两点: 1、 Visitor模式的优势与劣势; 2、 通过一个反面教材,有时候比正面教材给人印象更深; 让Visitor模式用到它最擅长的地方吧。让它不仅可行,而且还要可爱! |
|