《让僵冷的翅膀飞起来》系列之五——从容自若的CTO

让我们假设这样一个场景:一年以前,Media公司开发出一套通过电脑接收广播的Radio仿真软件产品。(有这样的产品吗,能真正接收广播的软件?我表示怀疑)这个产品早已投入市场,客户已经在使用了。后来,Media公司将开发重心转移到数字媒体上。于是他们投入了大量的人力物力,最后开发出了完美的媒体播放器软件。这个播放器支持大多数媒体文件,包括音频媒体和视频媒体。该产品取得了成功,也得到了用户的好评。

不过,现实生活中总有些***钻的客户,比如说wayfarer,就是鄙人了,素爱怀旧。在使用媒体播放器的时候,想起了在初中的时候就使用的收录机。磁带、广播,一机两用,真是令人怀念。于是我向Media公司提出了建议,希望能在媒体播放器中增加收音的功能。Media的CEO对这个似乎有些嗤之以鼻。可是像wayfarer这样的用户越来越多,呼声也越来越高。为了产品的市场,为了公司的前景,这位CEO不得不慎重考虑这个需求了。当首席执行官就是好,赶紧把这个烫手山芋抛给了CTO。

却看这位CTO仍然是从容不迫,脸上挂满自信的微笑。CEO不解,问他何故如此从容?CTO淡然一笑,吐出一字真言:“Adapter”。

呵呵,笑话了。设计模式可不是什么Bible,也非神奇的魔咒。不过对于以上场景,使用Adapter却是最佳的应用!且请听我慢慢道来。

已有产品:MediaPlayer、RadioPlayer;
分析:MediaPlayer是面向客户的外观,即表示层,它调用了对应的业务层,该层实现了IMedia接口。同理RadioPlayer也是面向客户的外观,它调用的业务层,是收听广播的业务,并实现了IRadio接口。
目的:将RadioPlayer的业务添加到MediaPlayer的外观中。原有的RadioPlayer不再使用。

既然与MediaPlayer、RadioPlayer的业务有关,所以我们有必要分析其各自的业务结构。MediaPlayer业务层结构:


 

为了简化,我这里将所有的方法都放在一个接口IMedia里(这个设计还有很多重构的空间,我会在后续文章中继续关注)。在本文的结构中,视频媒体和音频媒体的方法是相同的,本来我可以令各媒体文件继承同一个抽象类Media。但现实情况显然不是这样,所以我仍然保留这个系列文章中原有的结构。以下是每个方法的说明:
Play():播放媒体文件;
Stop():停止播放;
Pause():暂停播放;
OpenFile():打开媒体文件;
CloseFile():关闭媒体文件;
Forward():前进播放文件;
Back():后退播放文件;

OK,我们再来看看RadioPlayer的业务层结构:


 
RadioPlayer的业务均抽象为IRadio接口。并由抽象类Radio实现该接口。FM为调频收音,SW为短波收音。另外还有其他的,例如中波等,就不在详细列出。各方法的功能说明如下:
Receive():接收广播;
Stop():停止接收广播;
TurnOn():打开收音;
TurnOff():关闭收音;
ChangeChannel(bool direction):切换频率。参数direction为true时,则往上;否则往下。当然也可以使用枚举类型。

媒体播放器的业务由一个统一的Client类进行处理,它包括一系列的静态方法以实现对原有媒体类型的调用:
public class Client
{
 public static void Play(IMedia media)
 {
  media.Play();
 }

 public static void OpenFile(IMedia media)
 {
  media.OpenFile();
 }
 
 //……其他方法略;
}
MediaPlayer播放器本身,其外观则是一个WinForm应用程序,该应用程序将调用Client的相关静态方法。如:
Client.Play(new MP3());

现在看看我们需要实现的。我需要将RadioPlayer的业务,即抽象为IRadio接口的对象,放到MediaPlayer中。糟糕的是,Client的各个方法传递的参数类型,为IMedia接口。怎么才能将实现IRadio接口的对象传递到Client的方法中呢?对了,这就是适配,就是为IRadio对象适配成符合IMedia接口行为的过程。打一个不好听的比方,就好比一只狼,要让自己钻进羊群里,而不被发现,就需要找一张羊皮来披上。俗语云:“披着羊皮的狼”是也。不过,我们要注意的是,狼虽然不是羊,但有着和羊相似的属性。它和羊体形相似,照样能跑,能吃,只是吃的不是草,而是肉而已。你总不能为一张桌子披上羊皮,去装羊吧。而文中的IMedia类型和IRadio类型,还是有很多相似之处的。

现在,我们就为IRadio接口进行适当的包装。由于这是两个接口进行匹配的过程,所以我们通常名之为“适配”,而非“包裹”。那么它们之间有相似性吗?有!
IMedia    IRadio
Play()    Receive()
Stop()    Stop()
OpenFile()   TurnOn()
CloseFile()   TurnOff()
Forward()   ChangeChannel(true)
Back()    ChangeChannel(false)

当然现实情况并非总是那么完美。可能IMedia的方法中,IRadio可能并不需要。没关系,我们只提供该方法就可以了,方法的实现可以为空,如Pause()方法。也有可能IRadio的一些方法,IMedia并没有,此时的Adaptor模式,就将被适配对象的接口变宽了,也就是说引入了新的行为,这就类似于我系列文章之二所描述的。

不管现实的某些情况是多么的不如意,但至少通过引入Adapter模式,我们就不需要改变原有的IMedia和IRadio的相关对象与业务了。要修改的,仅仅是客户端,以及增加一个新的Adapter结构而已。

分析结束,开始动手术吧。先看类的Adapter模式:


 
类图好像很复杂,不过请大家主要关注橙色的两个类FMAdapter和SWAdapter。FMAdapter类是FM类型的Adapter,它继承了FM类,并实现了IMedia接口。通过这种方式,原有的FM类型的行为,就被适配为符合IMedia类型的新类型。代码如下:
public class FMAdapter:FM,IMedia
{
 public void Play()
 {
  this.Receive();
 }
 public void Forward()
 {
  this.ChangeChannel(true);
 }
 public void Pause(){}//Radio类型没有该行为,令其为空,或引入异常机制;
 //其他方法略
 …… 
}
SWAdapter的实现方式完全相同,就不赘述。

由于新的Adapter类均实现了IMedia接口,因此,该类型的对象可以安全正确地作为Client静态方法的参数对象传入。从外部行为的表现来看,没有区别。如:
Client.Play(new FMAdapter());
它调用了FMAdapter的Play方法,而其内部,实质上调用的是FM的Receiver()方法。

再看对象的Adapter模式,就更简单了:


 

只需要一个Adapter类RadioAdapter,然后实现IMedia接口。没有继承关系了,而是聚合了Radio对象。注意,这里聚合的是抽象类对象Radio,而不是具体的FM或SW。
public class RadioAdapter:IMedia
{
 private Radio _radio; 
 public RadioAdapter(Radio radio)
 {
  this._radio = radio;
 }
 public void Play()
 {
  _radio.Receive();
 }
 public void Forward()
 {
  _radio.ChangeChannel(true);
 }
 public void Pause(){}//Radio类型没有该行为,另其为空,或引入异常机制;
 //其他方法略
 …… 
}
调用Client的静态方法:
Client.Play(new RadioAdapter(new FM()));

通过引入Adapter模式,我们在不改变原有IMedia和IRadio的情况下,顺利地将IRadio类型适配成了IMedia类型。此时,我们只需要在MediaPlayer的客户端调用中加入原来RadioPlayer的业务即可,基本保证了原有系统的稳定性。

上述实例,才真正体现了Adapter的价值(请大家一定注意区分本文实例需求,与系列之二实例需求的区别)。因此,我们可以得到两个结论:
1、 通过Adapter模式,为适配对象引入以前不具备的行为;此时建议使用类的Adapter模式。理由请参考:《让僵冷的翅膀飞起来》系列之二——从实例谈Adapter模式《让僵冷的翅膀飞起来》系列之三——从Adapter模式到Decorator模式
2、 将一个固有对象适配为另一种接口对象;这是Adapter模式最重要的功能。使用类的Adapter模式与对象的Adapter模式均可,但感觉使用对象的Adapter模式更简单。

怎么样,够简单吧?难怪我们的CTO如此从容,因为他已经找到了终南捷径!

posted @ 2005-01-15 14:42  张逸  阅读(3003)  评论(9编辑  收藏  举报