《让僵冷的翅膀飞起来》系列之二——从实例谈Adapter模式

《让僵冷的翅膀飞起来》系列之一——从实例谈OOP、工厂模式和重构

《让僵冷的翅膀飞起来》系列之三——从Adapter模式到Decorator模式

在拙文《<让僵冷的翅膀飞起来>系列之一——从实例谈OOP、工厂模式和重构》中,冰汽水提出了一个问题,“如果我想让RM, MPEG类具有自己的一些特定属性的话怎么做呢?”原来的RM和MPEG类继承了VideoMedia抽象类,而VideoMedia类又实现了IMedia接口,该接口仅仅提供了Play()方法。冰汽水的意思是希望为RM,MPEG提供与AudioMedia不同的属性和方法。例如,对于视频媒体而言,应该有一个调整画面大小的方法,如Resize()。而这个方法是IMedia接口所不具备的。

那么怎样为RM,MPEG类提供IMedia接口所不具备的Resize()方法呢?非常自然地,通过这个问题我们就引出Adapter模式的命题了。首先,要假设一个情况,就是原文的所有代码,我们是无法改变的,这包括暴露的接口,类与接口的关系等等,都无法通过编码的方式实现新的目标。只有这样,引入Adapter模式才有意义。

熟悉Adapter模式的人都知道,Adapter模式分为两种:类的Adapter模式、对象的Adapter模式。下面我试图根据本例对两种方式进行说明及实现。在实现Adapter模式之前,有必要看看原来的类结构:


 

左边橙色的类为音频媒体类型,右边蓝色的类为视频媒体类型。所有的这些类型,包括类和接口都是无法改变的。现在我们的目的就是要让RM、MPEG具有Resize()方法。那么首先定义一个接口IVideoMedia,该接口具有Resize()方法。 


下面我们就根据Adapter模式来实现需求。

一、 类的Adapter模式

既然要让RM、MPEG具有Resize()方法,最好的办法就是让它们直接实现IVedioScreen接口。然而受到条件的限制,这两个类类型是不可修改的。唯一可行的办法就是为相应的类新引入一个类类型,这就是Adapter模式中所谓的Adapter类了。它好比是一个转接头,通过它去实现IVedioScreen接口,同时又令其继承原有的RM或MPEG类,以保留原有的行为。类图如下:

图中的类RMAdapter和MPEGAdapter就是通过Adapter模式获得的对象,它在保留了原有行为的同时,又拥有了IVedioScreen的功能。
代码如下:
public interface IVedioScreen
{
 void Resize();
}

public class RMAdapter:RM,IVedioScreen
{
 public void Resize()
 {
  MessageBox.Show("Change the RM screen's size.");
 }
}

public class MPEGAdapter:MPEG,IVedioScreen
{
 public void Resize()
 {
  MessageBox.Show("Change the MPEG screen's size.");
 }
}
也许很多人已经注意到了,在使用这种方式建立Adapter时,存在一个局限,就是我们必须为每一个要包裹(Wrapping)的类,建立一个相应的Adapter类。如上所述的RM对应RMAdapter,MPEG对应MPEGAdapter。必须如此,为什么呢?虽然RM和MPEG继承了同一个抽象类VedioMedia,但其Play()方法,可能是不相同的。此时,相对应的Adpater类只有直接继承该具体类,方才可以保留其原来的Play()方法的行为本质。

OOP中很重要的思想是,尽量使用聚合而非继承。让我们换一种思路来考察Adapter模式。

二、 对象的Adapter模式

对象的Adapter模式,与类的Adapter模式,最大的区别就是不采用继承的方式,而是将要包裹的对象,以聚合的方式放到Adapter中,然后用委托的方式调用其对象的方法,实现类图如下:
 


比较两种实现方式的类图,可以得出两个结论:
1、 对象的Adapter模式,减少了对象的个数;
2、 耦合度更加松散;

代码如下:
public interface IVedioScreen
{
 void Resize();
}

public class VedioAdapter:IVedioScreen
{
 private vedioMedia _vedio;
 
 public VedioAdapter(vedioMedia vedio)
 {
  _vedio = vedio;
 }

 public void Play()
 {
  _vedio.Play();  
 }

 public void Resize()
{
  if (_vedio is RM)
   MessageBox.Show("Change the RM screen's size.");
  else
   MessageBox.Show("Change the MPEG screen's size."); 
}
}

以这种方式形成的VedioAdapter,由于没有和RM、MPEG直接发生关系,并通过在构造函数传递参数的方式,等待客户端使用Adapter时,才将具体的VedioMedia对象传递给Adapter,显得耦合度更加松散,更加灵活。

我们来看客户端调用时,两者的区别:
1、 类的Adapter模式
public class Client
{
 public static void Main()
 {
  RMAdapter rmAdapter = new RMAdapter();
  MPEGAdapter mpegAdapter = new MPEGAdapter();
  
  rmAdapter.Play();
  rmAdapter.Resize();
  mpegAdapter.Play();
  mpegAdapter.Resize();
 }
}
2、 对象的Adapter模式
public class Client
{
 public static void Main()
 {
  VedioAdapter rmAdapter = new VedioAdapter(new RM());
  VedioAdapter mpegAdapter = new VedioAdapter(new MPEG());
  
  rmAdapter.Play();
  rmAdapter.Resize();
  mpegAdapter.Play();
  mpegAdapter.Resize();
 }
}

其实,对于对象的Adapter模式,还可以做一些改进,就是用属性或方法来取代构造函数传递被包裹对象的方式。代码修改如下:
public class VedioAdapter:IVedioScreen
{
 private vedioMedia _vedio;
 
 public VedioMedia Vedio
 {
  set {_vedio = value;}
 }
 ……
}
这样,上面的客户端调用就更简单了:
public class Client
{
 public static void Main()
 {
  VedioAdapter adapter = new VedioAdapter();
  adapter.Vedio = new RM();
  adapter.Play();
  adapter.Resize();

  adapter.Vedio = new MPEG();
  adapter.Play();
  adapter.Resize();
 }
}

通过运用Adapter模式,扩展了新的接口,而原有的类型并不需要做任何改变,这就是Adapter模式的实质,也是为什么取名为Adapter的原因之所在了。同时,我们要注意的是,在运用Adapter模式时,必须审时度势,根据具体的情况,抉择最优的方式,或者采用类的Adapter模式,或者采用对象的Adapter模式。决定权在与你,菜单给你送上来了,看看自己的腰包,想想点什么样的菜吧。

posted on 2005-01-11 15:10 张逸 阅读(3984) 评论(48)  编辑 收藏 网摘 所属分类: Design & Pattern

评论

#1楼  2005-01-11 15:22 mikespook [未注册用户]

很有启发~~~   回复  引用    

#2楼  2005-01-11 15:28 冰汽水      

多谢wayfarer指点,等忙完了手头的活儿,仔细拜读一下   回复  引用  查看    

#3楼  2005-01-11 15:44 大漠孤烟      

好精彩!

不晓得哪一天我才能写出这样的文章出来。   回复  引用  查看    

#4楼  2005-01-11 15:56 寒星      

好文,有时间一定加入你的团队,多学习点东西。   回复  引用  查看    

#5楼  2005-01-11 17:09 小陆      

有种叫做visitor的模式,也应该可以实现这样的功能:在子类中扩展基类中没有的方法。实现方式有什么不同吗?   回复  引用  查看    

#6楼 [楼主] 2005-01-11 18:25 wayfarer      

@寒星:
欢迎之至,共同学习!   回复  引用  查看    

#7楼  2005-01-11 18:51 idior [未注册用户]

晕啊,wayfarer差点上了你的当,如果用对象的VedioAdapter不能利用IMedia paly了啊.   回复  引用    

#8楼  2005-01-11 18:52 minbear      

恩,有空看看~   回复  引用  查看    

#9楼  2005-01-11 19:06 idior      

@小陆
visitor虽然能够添加新的功能,但是它通常都是在使用原有功能的基础上加上一些功能,对于这题就不太合适.
而且往往它会去遍历一个objectstruct去完成对某个集合的操作.
  回复  引用  查看    

#10楼 [楼主] 2005-01-11 19:13 wayfarer      

@idior:
使用对象的Adapter模式,仍然可以使用IMedia类型的对象啊。只是本文的目的是为视频媒体添加另外的行为,而对于音频媒体则不变,所以我在传递对象的时候,是将视频媒体的抽象类VedioMedia作为Adapter的对象。   回复  引用  查看    

#11楼  2005-01-11 19:30 idior      

喔,看来我理解有点问题,我以为VedioAdapter不能cast成imedia,然后play,不过看来这个cast似乎没太大必要.   回复  引用  查看    

#12楼  2005-01-11 19:34 idior      

不过我还是有点不爽 :P   回复  引用  查看    

#13楼  2005-01-11 19:36 idior      

btw 那个项目啥时有动作? 看你真是忙不过来了.不过我支持TDD哦 呵呵   回复  引用  查看    

#14楼  2005-01-11 21:48 weahappy      

抛开.net,考虑到C++不支持多重继承的化,看来只能采用对象Adapter模式了。   回复  引用  查看    

#15楼  2005-01-11 23:05 idior      

你看让VedioAdapter也实现IMedia接口是不是可以   回复  引用  查看    

#16楼 [楼主] 2005-01-12 09:13 wayfarer      

对象的Adapter模式,已经通过对象提供了IMedia的方法了:
public void Play()
{
_vedio.Play();
}

这就根本没有必要实现IMedia了。除非,我们有需求将VedioAdapter显示转换为IMedia的必要。当然这样就有些不伦不类了。

不过模式是拿来运用的,没有必要那么死板。根据你的需求,需要实现就实现,无所谓了。但是从本文考虑的角度来看,没有必要实现IMedia。因为,前提就是,需要为视频媒体增加新的行为。如果你将VedioAdapter对象Cast为IMedia对象的话,就只能调用Play()方法,而Resize()方法就不可用了。   回复  引用  查看    

#17楼  2005-01-12 09:48 KingofSC [未注册用户]

楼主,不可否认
对象adapter模式丧失了类adapter模式的继承优势
好比要实现多一次IMedia的情况下还不如用类adapter模式来得快
和自然
  回复  引用    

#18楼 [楼主] 2005-01-12 11:25 wayfarer      

对啊,所以我才在文章末尾写到:

“在运用Adapter模式时,必须审时度势,根据具体的情况,抉择最优的方式,或者采用类的Adapter模式,或者采用对象的Adapter模式。决定权在与你。”   回复  引用  查看    

#19楼  2005-01-12 14:10 Samuel      

Good stuff.
不过这样的例子感觉是亡羊补牢的做法:)。   回复  引用  查看    

#20楼  2005-01-12 14:29 idior [未注册用户]

@KingofSC
相对于继承方式要实现两个adaptor(以后可能还是n个)
多加一个词 : IMedia 似乎没什么   回复  引用    

#21楼  2005-01-12 15:19 吕震宇      

同意idior的观点“相对于继承方式要实现两个adaptor(以后可能还是n个)”。

对象Adapter的Resize方法中,我闻到了腐化的味道。如果扩充VideoMedia的子类,就必须修改Resize方法,造成了过于紧密的耦合。

public void Resize()
{
if (_vedio is RM)
MessageBox.Show("Change the RM screen's size.");
else
MessageBox.Show("Change the MPEG screen's size.");
}

为什么不考虑使用Decorator模式呢?   回复  引用  查看    

#22楼 [楼主] 2005-01-12 16:30 wayfarer      

@震宇兄:
我思考了一下,按照本文的实现目标,Decorator模式似乎并非合适的选择,至少不能解决你提出的紧耦合的问题。原来的VedioMedia抽象类具有Play()方法,而现在的要求是为其添加Resize()方法,同时保持Play()方法不变。也就是说你要Decorator的对象,只具有Play()方法,而Resize()方法,是新添加的。你无法在Decorator中根据多态判定对象的类型。

如果要实现,可以结合模板方法模式就OK了。这样的话,使用Adapter模式就够了。方法:将VedioAdapter定义为抽象类。并将Resize()方法定义为抽象方法。再根据具体的视频类型,例如RM或MPEG,创建相应的对象,继承VedioAdapter类,并重写Resize方法即可。

不过这样一来,使用类的Adapter模式,反而更清晰些了。你认为呢?   回复  引用  查看    

#23楼  2005-01-12 16:42 idior [未注册用户]

哈哈,根据尽量使用聚合而非继承

再加个 resizer类吧 :p

用stratage模式吧,当然没必要考虑那么多,TDD的一个观点就是没有要求的就不要实现.

在if else还能忍受的情况下就不要变了.

吕兄认为可以用Decorator实现吗?我想了想没想通   回复  引用    

#24楼 [楼主] 2005-01-12 16:59 wayfarer      

我认为还是使用模板方法模式比较好。Decorator模式只能Decorate原对象的Play()方法,所以,我也认为不妥。   回复  引用  查看    

#25楼  2005-01-12 19:17 idior [未注册用户]

Decorator只能修改已有方法的实现,不能增加新的方法。
  回复  引用    

#26楼  2005-01-12 21:25 吕震宇      

其实我说的Decorator模式与“对象的Adapter模式”差不多,唯一的区别就是将VideoAdapter类的名字改成VideoDectorator并继承自VideoMedia,并且将Resize方法设置为Abstract方法,然后继承两个子类,分别是RMDectorator和MPEGDectorator。

似乎有些繁琐,今天没有时间仔细分析了,改天再写。   回复  引用  查看    

#27楼  2005-01-13 07:23 吕震宇      

续:

Adapter模式的用意应当是“把一个类的接口变换成客户端所期待的另一种接口”。那么对于对象Adapter模式例子中VideoAdapter提供给用户的接口是什么呢?只有IVideoScreen,至于Play方法是Adapter自身的方法,而不是IMedia接口指定的Play方法。对那些只针对IMedia接口调用Play方法的程序而言,并不能识别VideoAdapter中的Play方法。

那我们再看看客户期待的接口是什么?我想作为客户而言,最期待的应当是一个可调整大小的VideoMedia,可以说两种功能缺一不可。而且这两种功能被分别定义在IMedia接口与IVideoScreen接口中,所以Adapter应当同时实现这两种接口。但让Adapter实现IMedia接口,言外之意就是也支持AudioMedia了,这不是我们想要的。所以最好的办法是让Adapter继承自VideoMedia并实现IVideoScreen接口。

其实说到这里,Adapter模式已经被转变成Decorator模式了。不知道Wayfarer是怎么看待这个问题的?   回复  引用  查看    

#28楼 [楼主] 2005-01-13 10:39 wayfarer      

@震宇兄:
这样讨论很有意思,也颇有收获。应该承认,你说的颇有道理。

首先,根据本文的要求,类的Adapter模式显然是没有问题的,也是没有争议的。争议的焦点主要集中在对象的Adapter模式。

其实,我们要考察的应该就是客户端期待的行为,如你所说,应该是拥有Play()和Resize()方法的媒体。那么此时现有的MediaAdapter已经可以实现这个要求了。然而,理想的要求是,原有的客户端调用,对于MediaAdapter类型来说,也要可行。否则原来的程序变动会很大。因为原来的调用应该是IMedia类型,而MediaAdapter并没有实现IMedia接口或继承其类。

正如你所说,这时,我们要求MediaAdapter也必须继承VedioMedia抽象类。此时,确实Adapter已经转变为Decorator了。由于我们的目的是提供Resize()方法,所以原有的Play(),可以直接调用传递进来的VedioMedia子类对象的Play()方法,无须装饰。

所以你的说法是正确的。

但是,通过对象的Adapter模式进行一些改变,也是可行的。那就是让Adapter实现IMedia接口。虽然你说,这样实现,可能会支持AudioMedia类型,但别忘了,我们传递的对象,是VedioMedia类型的。这样也能解决你的问题。

其实Adapter模式与Decorator模式有很多相似之处,通过这样的讨论让我的思路也清晰了许多。我需要把本文再改一下。

  回复  引用  查看    

#29楼  2005-01-13 11:19 吕震宇      

完全同意!   回复  引用  查看    

#30楼  2005-01-13 12:52 idior      

这个题演化下去,可以说出很多思想。
wayfarer有兴趣完善它吗?我也可以写扁随笔了。不过要过段时间。
  回复  引用  查看    

#31楼  2005-01-13 12:54 idior      

其实用visit也是可以实现的,而且也不错   回复  引用  查看    

#32楼  2005-01-14 09:59 KingofSC [未注册用户]

奇怪的是,用class adapter已经非常完美的解决了要实现IMedia的问题。为什么要挖空心思的非要用object adapter,然后想出各种各样的补救来使object adapter来达到class adapter的效果?
虽然oop的思想是尽量使用聚合而非继承
但是也要具体问题具体分析啊,不能一条大路通罗马啊
而且oop都是说尽量而已,在目前这个需求下,是否用class adapter就是最好的解决方法呢?
  回复  引用    

#33楼 [楼主] 2005-01-14 10:16 wayfarer      

@KingofSC:

我看你是误解我的意思了。写这些文章,和应用到项目不一样。我也知道用类的Adapter模式,最适合本文的需求。你如果看了我的这个系列第三篇文章的评论就知道了。

但本文写到这里,已经不是限于应用了。而是想借助这个个案,来演化各种模式,研究各种模式,进而比较出各种模式的优劣,或适用的范围。

你看,通过这个例子,你不是已经得出现在的这个结果了吗?这就是本文希望达到的效果啊。

软件开发没有绝对的事情。不能说聚合一定比继承好,也不能说类的Adapter模式绝对比对象的Adapter模式好。必须要看你适应的要求。

GOF的设计模式一书中,对于同一个游戏房间的例子,他分别使用了工厂模式和Builder模式来说明。本来用工厂模式已经解决得很好了,为什么他又要引入Builder模式呢?道理就是这样的啊!   回复  引用  查看    

#34楼  2005-01-14 10:36 KingofSC [未注册用户]

哈哈,看来我真是误解了楼主的用心良苦啊,pf :)   回复  引用    

#35楼  2005-01-18 17:31 纯爷们      

最近忙的要死,一直也没时间细细品位你的文章,今天抽了个时间看了看,收获比较多!呵呵,接着看!   回复  引用  查看    

#36楼  2005-03-02 17:56 anxing_china_c@hotmai.com [未注册用户]

说下我的个人看法:
(1)似乎楼主在开头"冰汽水提出了一个问题,“如果我想让RM, MPEG类具有自己的一些特定属性的话怎么做呢?"".来引入Adapter模式有点牵强.
Adapter模式主要是为了解决两个已有接口不匹配的问题.<设计模式>上的一个例子是如果有一个类Graph实现绘图,现在我们要实现一个TextGraph实现一个文本绘制,我们可以继承Graph的基本操作,然后加入我们特殊的操作.但是如果我们刚好有一个现成的类:TextView用来显示文本.那么,我们遭遇了两个不相同的类,那么我们就可以使用Adapter模式.现在我们并没有这个TextView,所以去定义一个IVedioScreen 来凑出Adapter模式其实并没有必要,我们其实是定义了另外的接口并实现了而已.
(2)Adapter引入通常是因为我们的继承体系:MPEG继承自IMedia不能破坏.所以对象Adapter是否应该这样实现:public class VedioAdapter:vedioMedia
{
private IVedioScreen _Screen;
......
}
这个好象楼上们已经讨论了。
(3)Decorator模式:装饰。如果现在我们突然有一种新的需求,需要在播放每个MPEG前能够播放一段光告,想想现在的电视节目吧。所以我们可以使用Decorator模式.在我的项目中,我曾经在这里使用了Decorator模式.我经常写Console程序,有次我希望我在打印消息的时候打印出这样的效果:
CMD>A Regsiter Come From Boss Server
CMD>
这样我就可以象在DOS上一样操作我的程序.Decorator大多是为了增加额外的功能.Adapter是为了解决接口的不兼容.

纯粹个人观点,欢迎指正.   回复  引用    

#37楼  2005-03-02 18:00 有时狂欢 [未注册用户]

(2)Adapter引入通常是因为我们的继承体系:MPEG继承自IMedia不能破坏.所以对象Adapter是否应该这样实现:public class VedioAdapter:vedioMedia
{
private IVedioScreen _Screen;
......
}
这个好象楼上们已经讨论了。
(3)Decorator模式:装饰。如果现在我们突然有一种新的需求,需要在播放每个MPEG前能够播放一段光告,想想现在的电视节目吧。所以我们可以使用Decorator模式.在我的项目中,我曾经在这里使用了Decorator模式.我经常写Console程序,有次我希望我在打印消息的时候打印出这样的效果:
CMD>A Regsiter Come From Boss Server
CMD>
这样我就可以象在DOS上一样操作我的程序.Decorator大多是为了增加额外的功能.Adapter是为了解决接口的不兼容.

纯粹个人观点,欢迎指正.   回复  引用    

#38楼  2005-03-02 18:03 有时狂欢 [未注册用户]

博客园怎么回事,明明提示我"出现错误".我看也确实没有提交上.突然发现以前提交的都成功了.麻烦Wayfarer大人帮我删除了重复的帖子.   回复  引用    

#39楼  2005-04-11 18:07 idior      

@wayfarer
有空看看这个
http://www.alphatom.com/content/view/171/69/

http://www.cnblogs.com/idior/articles/113958.html   回复  引用  查看    

#40楼  2005-06-02 12:01 Articles about .NET      

严格说来,这应该是采用Decorator模式的功能,利用Adapter模式来解决问题,我觉得我们在学习中可以把各个模式分得很清楚,但我们在设计中可以灵活的运用,完全可以把多个模式结合起来运用;戏法人人会变,关键是谁变得好,只要能把这些模式运用得当,合理地解决我们的问题就是最大的收获。
 
当然,这样的讨论也不错,通过讨论,我们能更加深入地了解到Decorator模式和Adapre模式的异同。
  回复  引用  查看    

#41楼  2005-07-31 01:32 best-novice [未注册用户]

好章!
  回复  引用    

#42楼  2005-12-02 17:19 黑马      

好文章,看过《让僵冷的翅膀飞起来》系列文章之后,会更容易理解EnterpriseLibrary。   回复  引用  查看    

#43楼  2005-12-14 11:50 poo [未注册用户]

华山论剑,不,华山论剑是分胜负,不似此等境界
更像是紫禁之巅陆小凤同叶孤城那经典难忘的演绎,古大侠的胸襟和风范。
仿佛高山与流水的唱和,唱者坦坦荡荡,直抒胸臆,和者谦谦君子,虚怀若谷,美哉!   回复  引用    

#44楼  2007-01-31 17:19 蚊子飞飞 [未注册用户]

磋!   回复  引用    





标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2006-04-07 09:07 编辑过
Google站内搜索

相关文章:

相关链接:
 

导航

公告

logo.gif
我的著作与译作

《软件设计精要与模式》

《WCF服务编程》

MVP_Horizontal_BlueOnly.png

From 03-03-2006
Counter: site stats

与我联系

搜索

 

常用链接

我参加的小组

我参与的团队

随笔分类(245)

随笔档案(238)

最新随笔

积分与排名

最新评论

阅读排行榜

评论排行榜