.NET设计模式(8):适配器模式(Adapter Pattern)

 

适配器模式(Adapter Pattern

——.NET设计模式系列之八

Terrylee20062

概述

在软件系统中,由于应用环境的变化,常常需要将“一些现存的对象”放在新的环境中应用,但是新环境要求的接口是这些现存对象所不满足的。那么如何应对这种“迁移的变化”?如何既能利用现有对象的良好实现,同时又能满足新的应用环境所要求的接口?这就是本文要说的Adapter 模式。

意图

将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

结构图

1 类的Adapter模式结构图

2 对象的Adapter模式结构图

生活中的例子

适配器模式允许将一个类的接口转换成客户期望的另一个接口,使得原本由于接口不兼容而不能一起工作的类可以一起工作。扳手提供了一个适配器的例子。一个孔套在棘齿上,棘齿的每个边的尺寸是相同的。在美国典型的边长为1/2''1/4''。显然,如果不使用一个适配器的话,1/2''的棘齿不能适合1/4''的孔。一个1/2''1/4''的适配器具有一个1/2''的阴槽来套上一个1/2''的齿,同时有一个1/4的阳槽来卡入1/4''的扳手。

3使用扳手适配器例子的适配器对象图

适配器模式解说

我们还是以日志记录程序为例子说明Adapter模式。现在有这样一个场景:假设我们在软件开发中要使用一个第三方的日志记录工具,该日志记录工具支持数据库日志记录DatabaseLog和文本文件记录FileLog两种方式,它提供给我们的API接口是Write()方法,使用方法如下:

Log.Write("Logging Message!");

当软件系统开发进行到一半时,处于某种原因不能继续使用该日志记录工具了,需要采用另外一个日志记录工具,它同样也支持数据库日志记录DatabaseLog和文本文件记录FileLog两种方式,只不过它提供给我们的API接口是WriteLog()方法,使用方法如下:

Log.WriteLog("Logging Message!");

该日志记录工具的类结构图如下:

4日志记录工具类结构图

它的实现代码如下:

public abstract class LogAdaptee

{

    public abstract void WriteLog();

}  

public class DatabaseLog:LogAdaptee

{

    public override void WriteLog()

    {

        Console.WriteLine("Called WriteLog Method");

    }

}

public class FileLog:LogAdaptee

{

    public override void WriteLog()

    {

        Console.WriteLine("Called WriteLog Method");

    }

}

在我们开发完成的应用程序中日志记录接口中(不妨称之为ILogTarget接口,在本例中为了更加清楚地说明,在命名上采用了Adapter模式中的相关角色名字),却用到了大量的Write()方法,程序已经全部通过了测试,我们不能去修改该接口。代码如下:

public interface ILogTarget

{

    void Write();

}

这时也许我们会想到修改现在的日志记录工具的API接口,但是由于版权等原因我们不能够修改它的源代码,此时Adapter模式便可以派上用场了。下面我们通过Adapter模式来使得该日志记录工具能够符合我们当前的需求。

前面说过,Adapter模式有两种实现形式的实现结构,首先来看一下类适配器如何实现。现在唯一可行的办法就是在程序中引入新的类型,让它去继承LogAdaptee类,同时又实现已有的ILogTarget接口。由于LogAdaptee有两种类型的方式,自然我们要引入两个分别为DatabaseLogAdapterFileLogAdapter的类。

5 引入类适配器后的结构图

实现代码如下:

public class DatabaseLogAdapter:DatabaseLog,ILogTarget

{

    public void Write()

    {

        WriteLog();

    }

}

 

public class FileLogAdapter:FileLog,ILogTarget

{

    public void Write()

    {

        this.WriteLog();

    }

}

这里需要注意的一点是我们为每一种日志记录方式都编写了它的适配类,那为什么不能为抽象类LogAdaptee来编写一个适配类呢?因为DatabaseLogFileLog虽然同时继承于抽象类LogAdaptee,但是它们具体的WriteLog()方法的实现是不同的。只有继承于该具体类,才能保留其原有的行为。

我们看一下这时客户端的程序的调用方法:

public class App

{

    public static void Main()

    {

        ILogTarget dbLog = new DatabaseLogAdapter();

        dbLog.Write("Logging Database...");

 

        ILogTarget fileLog = new FileLogAdapter();

        fileLog.Write("Logging File...");

    }

}

下面看一下如何通过对象适配器的方式来达到我们适配的目的。对象适配器是采用对象组合而不是使用继承,类结构图如下:

6引入对象适配器后的结构图

实现代码如下:

public class LogAdapter:ILogTarget

{

    private LogAdaptee _adaptee;

 

    public LogAdapter(LogAdaptee adaptee)

    {

        this._adaptee = adaptee;   

    }

    public void Write()

    {

        _adaptee.WriteLog();

    }

}

与类适配器相比较,可以看到最大的区别是适配器类的数量减少了,不再需要为每一种具体的日志记录方式来创建一个适配器类。同时可以看到,引入对象适配器后,适配器类不再依赖于具体的DatabaseLog类和FileLog类,更好的实现了松耦合。

再看一下客户端程序的调用方法:

public class App

{

    public static void Main()

    {

       

        ILogTarget dbLog = new LogAdapter(new DatabaseLog());

        dbLog.Write("Logging Database...");

 

        ILogTarget fileLog = new LogAdapter(new FileLog());

        fileLog.Write("Logging Database...");

    }

}

通过Adapter模式,我们很好的实现了对现有组件的复用。对比以上两种适配方式,可以总结出,在类适配方式中,我们得到的适配器类DatabaseLogAdapterFileLogAdapter具有它所继承的父类的所有的行为,同时也具有接口ILogTarget的所有行为,这样其实是违背了面向对象设计原则中的类的单一职责原则,而对象适配器则更符合面向对象的精神,所以在实际应用中不太推荐类适配这种方式。再换个角度来看类适配方式,假设我们要适配出来的类在记录日志时同时写入文件和数据库,那么用对象适配器我们会这样去写:

public class LogAdapter:ILogTarget

{

    private LogAdaptee _adaptee1;

    private LogAdaptee _adaptee2;

 

    public LogAdapter(LogAdaptee adaptee1,LogAdaptee adaptee2)

    {

        this._adaptee1 = adaptee1;

        this._adaptee2 = adaptee2;

    }

    public void Write()

    {

        _adaptee1.WriteLog();

        _adaptee2.WriteLog();

    }

}

如果改用类适配器,难道这样去写:

public class DatabaseLogAdapter:DatabaseLog,FileLog,ILogTarget

{

    public void Write()

    {

        //WriteLog();

    }

}

显然是不对的,这样的解释虽说有些牵强,也足以说明一些问题,当然了并不是说类适配器在任何情况下都不使用,针对开发场景不同,某些时候还是可以用类适配器的方式。

.NET中的适配器模式

1Adapter模式在.NET Framework中的一个最大的应用就是COM InteropCOM Interop就好像是COM.NET之间的一条纽带,一座桥梁。我们知道,COM组件对象与.NET类对象是完全不同的,但为了使COM客户程序象调用COM组件一样调用.NET对象,使.NET程序

象使用.NET对象一样使用COM组件,微软在处理方式上采用了Adapter模式,对COM对象进行包装,这个包装类就是RCW(Runtime Callable Wrapper)RCW实际上是runtime生成的一个.NET类,它包装了COM组件的方法,并内部实现对COM组件的调用。如下图所示:

7 .NET程序与COM互相调用示意图

2.NET中的另一个Adapter模式的应用就是DataAdapterADO.NET为统一的数据访问提供了多个接口和基类,其中最重要的接口之一是IdataAdapter。与之相对应的DataAdpter是一个抽象类,它是ADO.NET与具体数据库操作之间的数据适配器的基类。DataAdpter起到了数据库到DataSet桥接器的作用,使应用程序的数据操作统一到DataSet上,而与具体的数据库类型无关。甚至可以针对特殊的数据源编制自己的DataAdpter,从而使我们的应用程序与这些特殊的数据源相兼容。注意这是一个适配器的变体。

实现要点

1Adapter模式主要应用于“希望复用一些现存的类,但是接口又与复用环境要求不一致的情况”,在遗留代码复用、类库迁移等方面非常有用。

2Adapter模式有对象适配器和类适配器两种形式的实现结构,但是类适配器采用“多继承”的实现方式,带来了不良的高耦合,所以一般不推荐使用。对象适配器采用“对象组合”的方式,更符合松耦合精神。

3Adapter模式的实现可以非常的灵活,不必拘泥于GOF23中定义的两种结构。例如,完全可以将Adapter模式中的“现存对象”作为新的接口方法参数,来达到适配的目的。

4Adapter模式本身要求我们尽可能地使用“面向接口的编程”风格,这样才能在后期很方便的适配。[以上几点引用自MSDN WebCast]

效果

对于类适配器:

1.用一个具体的Adapter类对AdapteeTaget进行匹配。结果是当我们想要匹配一个类以及所有它的子类时,类Adapter将不能胜任工作。

2.使得Adapter可以重定义Adaptee的部分行为,因为AdapterAdaptee的一个子类。

3.仅仅引入了一个对象,并不需要额外的指针一间接得到Adaptee.

对于对象适配器:

1.允许一个Adapter与多个Adaptee,即Adaptee本身以及它的所有子类(如果有子类的话)同时工作。Adapter也可以一次给所有的Adaptee添加功能。

2.使得重定义Adaptee的行为比较困难。这就需要生成Adaptee的子类并且使得Adapter引用这个子类而不是引用Adaptee本身。

适用性

在以下各种情况下使用适配器模式:

1.系统需要使用现有的类,而此类的接口不符合系统的需要。

2.想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。这些源类不一定有很复杂的接口。

3.(对对象适配器而言)在设计里,需要改变多个已有子类的接口,如果使用类的适配器模式,就要针对每一个子类做一个适配器,而这不太实际。

总结

总之,通过运用Adapter模式,就可以充分享受进行类库迁移、类库重用所带来的乐趣。

参考资料

阎宏,《Java与模式》,电子工业出版社

James W. Cooper,《C#设计模式》,电子工业出版社

Alan Shalloway James R. Trott,《Design Patterns Explained》,中国电力出版社

MSDN WebCast C#面向对象设计模式纵横谈(7)Adapter 适配器模式(结构型模式)

作者:TerryLee
出处:http://terrylee.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
posted @ 2006-02-18 11:57 TerryLee 阅读(38180) 评论(59) 编辑 收藏

 回复 引用 查看   
#1楼 2006-02-18 13:17 高海东      
支持 不错
 回复 引用   
#2楼 2006-02-18 13:35 路过[未注册用户]
非常不错!
强烈支持把本系列文章写完,期待中……

 回复 引用   
#3楼 2006-02-18 13:42 fgfgfg[未注册用户]
把Adapter模式讲的很透彻!

设计模式的又一经典系列……,希望能够坚持发完!

 回复 引用   
#4楼 2006-02-18 14:19 Cooll[未注册用户]
经典!!!
 回复 引用 查看   
#5楼[楼主] 2006-02-18 14:52 Terrylee      
该系列文章我一定会坚持写完的:)
 回复 引用   
#6楼 2006-02-18 15:56 学习中[未注册用户]
学习……
 回复 引用 查看   
#7楼 2006-02-18 20:01 雁儿飞飞      
学习ing
 回复 引用   
#8楼 2006-02-19 09:35 AUTO123[未注册用户]
很好懂的文章,感谢Terrylee的无私贡献!!!
 回复 引用   
#9楼 2006-02-27 13:54 mrhgw[未注册用户]
强烈支持!
 回复 引用 查看   
#10楼 2006-02-28 01:05 Allen      
今天一口气看了好几个你写的好几个模式。
这个以前就知道了。所以看的倒也挺快。
有个建议:毕竟有些模式比较容易理解,有些掌握起来不容易。比如这个就瞒简单的。
可不可以每个模式加个难度系数,给初学者一些参考。

如果这个建议没有必要,请见笑^_^

 回复 引用 查看   
#11楼[楼主] 2006-02-28 08:29 Terrylee      
@Allen
多谢你的建议!

我会考虑加上的:-)

 回复 引用   
#12楼 2006-06-28 17:27 飞鸟扬[未注册用户]
上面两个结构图显示不出来了,兄弟能不能修复一下:D
 回复 引用 查看   
#13楼[楼主] 2006-06-28 17:45 TerryLee      
@飞鸟扬

我看了一下,图片路径没问题:-)

 回复 引用   
#14楼 2006-06-28 21:14 飞鸟扬[未注册用户]
@TerryLee
奇怪,就是有一半显示不出来,嘿嘿,可能rpwt吧。

 回复 引用 查看   
#15楼 2006-10-10 22:47 小芒果先生      
看了你的文章,然后再对照看了一下"新版设计模式手册[C#].pdf"中的适配器模式及其它网上别人发布的一些关于适配器方面的代码,发现“"新版设计模式手册[C#].pdf”中的适配器模式是属于类适配器模式而非对象适配器模式的。
看了你的这一片文章,给我很享受的感觉,尤其是其中的对象适配器模式,在程序架构中很多地方都可以用的到。非常谢谢。

 回复 引用 查看   
#16楼[楼主] 2006-10-12 15:58 TerryLee      
@小芒果先生
不用客气:-)

 回复 引用   
#17楼 2006-11-01 16:54 小兵[匿名][未注册用户]
对象适配器讲的很好,我得到了启发.
不错啊..

 回复 引用 查看   
#18楼[楼主] 2006-11-01 20:20 TerryLee      
@小兵[匿名]
:)

 回复 引用 查看   
#19楼 2006-11-28 16:10 刘寅      
@TerryLee

前面的那个类适配器的例子有这样的句子,不是道是不是我理解错了..>.<

"在我们开发完成的应用程序中日志记录接口中(不妨称之为ILogTarget接口,在本例中为了更加清楚地说明,在命名上采用了Adapter模式中的相关角色名字),却用到了大量的Write()方法,程序已经全部通过了测试,我们不能去修改该接口。代码如下
public interface ILogTarget

{

void Write();

}"

这里应该是"public abstract class LogAdaptee{...}"这个抽象类的WriteLog()方法使用广泛吧..然后通过适配器,使现有的系统"class app{...}"来使用新环境的 "Wirte()"方法吧(由"public interface ILogTarget
{..}"来抽象)
还有一个,就是新环境的接口的"Wirte()"的参数问题..编译过不了

一篇篇看过来,真的写得不错,会全部看完的..terry加油

 回复 引用   
#20楼 2007-04-09 14:21 KidYang
我刚刚接触设计模式,有一个疑问,适配器模式可以重复使用现有的类,但是这里的前提是,原有的程序中的类设计也是遵循了依赖倒置原则的吧,否则还是无法应用适配器模式的,还是需要大量修改原有程序的,是这样的么,多谢指教。
 回复 引用   
#21楼 2007-05-25 15:01 jess.lv[未注册用户]
看了你的设计模式,收获不小,希望能写完
 回复 引用 查看   
#22楼 2007-07-02 22:58 枫崖      
一口气从创建型模式看过来,看得过程中感觉很爽,各个例子也举的形象生动,可是看完了就脑子里空空一片,到底应该怎么运用和学习呢,模式是一个个掌握吗,还是。。。
 回复 引用 查看   
#23楼 2007-08-20 09:41 sekihin      
@枫崖
个人愚见,设计模式了解其是怎么演变而来的就行,最重要的还是设计原则。遵循设计原则重构,解决实际中软件实体重用的问题,比模式本身更重要。

 回复 引用 查看   
#24楼 2008-07-31 13:57 姜敏      
快三年了的文章,本人现在才开始细读,真是,感觉你的文章非常容易懂,举的例子非常到位,这个head fisrt的风格差不多.
 回复 引用 查看   
#25楼[楼主] 2008-08-01 00:58 TerryLee      
@姜敏
谢谢:)

 回复 引用 查看   
#26楼 2008-08-05 17:16 王国金      
非常不错。支持!
 回复 引用 查看   
#27楼[楼主] 2008-08-06 13:20 TerryLee      
@王国金
谢谢支持:)

 回复 引用 查看   
#28楼 2008-11-21 10:27 自强不息      
如果楼主能够用具体的小项目把这几种模式实现了那就太好了
要求有点过分了,呵呵

 回复 引用 查看   
#29楼[楼主] 2008-11-21 10:55 TerryLee      
@自强不息
这就有点滥用模式的味道,呵呵,模式是解决某一类通用问题的手法,通常是通过重构才达到某一个模式,而不是非要在一个项目中使用多少种模式才算好的设计:)

 回复 引用 查看   
#30楼 2008-11-22 10:24 neil-zhao      
LZ:
最近我一直在看您写的文章,文章写得很好,有一个问题就是我看到关于模式的文章前两篇还有源码下载地址,可后边的怎么没有了呢?假如能有的话那就更好了。
期待中........

 回复 引用 查看   
#31楼[楼主] 2008-11-22 17:07 TerryLee      
@neil-zhao
其实拷贝文章中的代码就可以使用,不提供下载是希望大家能亲自动手写一下代码:)

 回复 引用 查看   
#32楼 2009-01-06 15:11 Fengdesudu      
不错啊,学习
 回复 引用 查看   
#33楼[楼主] 2009-01-06 18:36 TerryLee      
@Fengdesudu
:)

 回复 引用 查看   
#34楼 2009-01-07 13:33 pillow      
用现有对象实现匹配要求接口。
 回复 引用 查看   
#35楼 2009-01-07 14:03 pillow      
适配器模式允许将一个类的接口转换成客户期望的另一个接口。
 回复 引用 查看   
#36楼 2009-03-05 09:49 李松-2008      
写的很清楚,让人一看就明白。3Q
 回复 引用 查看   
#37楼 2009-03-31 13:33 徐培华      
--引用--------------------------------------------------
李松-2008: 写的很清楚,让人一看就明白。3Q
--------------------------------------------------------
真的很不错哈,这个完全完全弄明白了。
呵呵,什么时候更新啊,真是期待啊,

 回复 引用 查看   
#38楼 2009-03-31 19:48 GXW      
写的很不错,谢谢!
 回复 引用 查看   
#39楼 2009-08-04 10:38 月云      
“这里需要注意的一点是我们为每一种日志记录方式都编写了它的适配类,那为什么不能为抽象类LogAdaptee来编写一个适配类呢?因为DatabaseLog和FileLog虽然同时继承于抽象类LogAdaptee,但是它们具体的WriteLog()方法的实现是不同的。只有继承于该具体类,才能保留其原有的行为。”
李大哥的这句话我看了半天才懂。是说LogAdapter不能声明为抽象类或是工厂,而必须在其内部声明抽象类LogAdaptee的对象。对吧?

 回复 引用   
#40楼 2009-08-12 22:25 bananaxzw[未注册用户]
感觉对象适配器 有点build模式的味道 呵呵
 回复 引用 查看   
#41楼 2009-08-30 09:37 鸽子飞扬      
通俗易懂,非常感谢楼主分享!
 回复 引用 查看   
#42楼 2009-09-10 11:10 我是一条鱼      
ILogTarget dbLog = new LogAdapter(new DatabaseLog());
dbLog.Write("Logging Database...");

ILogTarget fileLog = new LogAdapter(new FileLog());
fileLog.Write("Logging Database...");


这个策略模式有啥区别?

 回复 引用 查看   
#43楼 2009-10-14 10:33 王金平      
今天再次温习此模式,理解的更透彻了些。感谢TerryLee的无私奉献
 回复 引用 查看   
#44楼 2009-11-26 10:12 jack.feng      
楼主,请问有qq群吗?想进入学习学习。
没有,建议你建一个。

 回复 引用 查看   
#45楼 2009-12-17 15:57 戴伟      
强烈支持!
 回复 引用 查看   
#46楼 2009-12-18 09:14 戴伟      
您好,
ILogTarget dbLog = new DatabaseLogAdapter();

dbLog.Write("Logging Database...");



ILogTarget fileLog = new FileLogAdapter();

fileLog.Write("Logging File...");

DatabaseLogAdapter()中的Write()方法时void类型的不带参数,这里的Write()方法怎么带参数了呢?

 回复 引用 查看   
#47楼 2010-01-16 23:21 李帅团      
写的很不错,学习学习。。。
 回复 引用 查看   
#48楼 2010-01-20 22:32 孤單一吻      
写的很不错,学习学习。。。
 回复 引用 查看   
#49楼 2010-09-18 15:16 阿张      
引用戴伟:
您好,
ILogTarget dbLog = new DatabaseLogAdapter();

dbLog.Write("Logging Database...");



ILogTarget fileLog = new FileLogAdapter();

fileLog.Write("Logging File...");

DatabaseLogAdapter()中的Write()方法时void类型的不带参数,这里的Write()方法怎么带参数了呢?



这个代码可能LZ没有实际运行下,调用的时候可以不带参数
或者修改基类的方法
PS:LZ加油哦 看好你

 回复 引用 查看   
#50楼 2010-10-13 10:13 YoungSin      
写的很好哈,适配器模式应该是用的比较多的模式,特使是我们在对老系统做改造,需要引入新的组件时
 回复 引用 查看   
#51楼 2011-08-23 15:28 nd      
楼主 ,
public static void Main()
{

ILogTarget dbLog = new LogAdapter(new DatabaseLog());
dbLog.Write("Logging Database...");

ILogTarget fileLog = new LogAdapter(new FileLog());
fileLog.Write("Logging Database...");
}
这两方法带参数, 前面没见写带参数的write方法啊 , 很是疑惑。

 回复 引用 查看   
#52楼 2011-08-23 16:50 nd      
感觉和策略模式很像!
 回复 引用 查看   
#53楼 2011-11-08 09:52 nighrmare23      
不顶一下不行,不过terry几年没有更新了,搬到新的地方去了是吧?但是那个.me的打不开,是不是被景德镇的局域网屏蔽了呀?