《让僵冷的翅膀飞起来》系列之四——Visitor模式之可行与不可爱
Idior一再说,对于系列二的例子,使用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的行为;例如下面我就为媒体类型提供了都具备的方法:OpenFile()和CloseFile();
(注:此时的Play方法其实可以放到IMedia接口中。我仍然将其放到Visitor的目的是,便于说明Visitor模式,同时通过这种方式,使对Play方法的修改较为容易)
请大家注意上图的类图左侧,即我定义的媒体类型对象区,在这种Visitor模式设计的前提下,对于类型的行为来说,是非常稳定的。即:无论你要为这四种媒体类型增加什么行为,都不需要修改该区域的任何类或接口;而你只需要在Visitor下增加行为的具体Visitor类即可。这就符合了OO思想中非常著名的开—闭原则。左区即为相对的闭区间(我不能说是绝对的对修改关闭,因此,我才用虚线来作界定),而右区则为相对的开区间。
还是来看看源代码:
首先是被访问的类型:
public interface IMedia
{
void OpenFile();
void CloseFile();
}
public abstract class AudioMedia,IMedia
{
public void OpenFile()
{
//打开媒体文件;
}
public void CloseFile()
{
//关闭媒体文件;
}
public abstract void Accept(AudioVisitor visitor);
}
public class MP3:AudioMedia
{
public override void Accept(AudioVisitor visitor)
{
visitor.VisitMP3(this);
}
}
public class WAV:AudioMedia
{
public override void Accept(AudioVisitor visitor)
{
visitor.VisitWAV(this);
}
}
public abstract class VedioMedia,IMedia
{
public void OpenFile()
{
//打开媒体文件;
}
public void CloseFile()
{
//关闭媒体文件;
}
public abstract void Accept(VedioVisitor visitor);
}
public class RM:VedioMedia
{
public override void Accept(VedioVisitor visitor)
{
visitor.VisitRM(this);
}
}
public class MPEG:VedioMedia
{
public override void Accept(VedioVisitor visitor)
{
visitor.VisitMPEG(this);
}
}
下面是Visitor的实现:
public abstract class AudioVisitor
{
public abstract void VisitMP3(MP3 mp3);
public abstract void VisitWAV(WAV wav);
}
public class PlayAudioVisitor:AudioVisitor
{
public override void VisitMP3(MP3 mp3)
{
mp3.OpenFile();
//实现MP3的Play方法;
mp3.CloseFile();
}
public override void VisitWAV(WAV wav)
{
wav.OpenFile();
//实现WAV的Play方法;
wav.CloseFile();
}
}
public class ShowScriptAudioVisitor:AudioVisitor
{
public override void VisitMP3(MP3 mp3)
{
//实现MP3的ShowScript方法;
}
public override void VisitWAV(WAV wav)
{
//实现WAV的ShowScript方法;
}
}
public abstract class VedioVisitor
{
public abstract void VisitRM(RM rm);
public abstract void VisitMPEG(MPEG mpeg);
}
public class PlayVedioVisitor:VedioVisitor
{
public override void VisitRM(RM rm)
{
rm.OpenFile();
//实现RM的Play方法;
rm.CloseFile();
}
public override void VisitMPEG(MPEG mpeg)
{
mpeg.OpenFile();
//实现MPEG的Play方法;
mpeg.CloseFile();
}
}
public class ResizeVedioVisitor:VedioVisitor
{
public override void VisitRM(RM rm)
{
//实现RM的ShowScript方法;
}
public override void VisitMPEG(MPEG mpeg)
{
//实现MPEG的ShowScript方法;
}
}
请注意在Visit方法中传递节点对象的意图。目的就在于Visitor的行为中可能会调用被访问对象的固有行为,如Play行为中调用了OpenFile()和CloseFile()方法。但也可以不调用,如ShowScript和Resize行为。如果在你的设计中,不会出现调用被访问对象固有行为的情况,那么在Visit方法中删去该节点对象,也无不可。
根据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模式用到它最擅长的地方吧。让它不仅可行,而且还要可爱!