(第Ⅲ部分 结构型模式篇) 第7章 适配器模式(Adapter Pattern)

http://blog.csdn.net/zhanglei5415/article/details/1675568

概述
在软件系统中,由于应用环境的变化,常常需要将“一些现存的对象”放在新的环境中应用,但是新环境要求的接口是这些现存对象所不满足的。那么如何应对这种“迁移的变化”?如何既能利用现有对象的良好实现,同时又能满足新的应用环境所要求的接口?这就是本文要说的 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 abstractclass LogAdaptee
{
    publicabstractvoid WriteLog();
}  
public class DatabaseLog:LogAdaptee
{
    publicoverridevoid WriteLog()
    {
        Console.WriteLine("Called WriteLog Method");
    }
}
public class FileLog:LogAdaptee
{
    publicoverridevoid WriteLog()
    {
        Console.WriteLine("Called WriteLog Method");
    }
}
在我们开发完成的应用程序中日志记录接口中(不妨称之为 ILogTarget 接口,在本例中为了更加清楚地说明,在命名上采用了 Adapter 模式中的相关角色名字),却用到了大量的 Write() 方法,程序已经全部通过了测试,我们不能去修改该接口。代码如下:
public interface ILogTarget
{
    void Write();
}
这时也许我们会想到修改现在的日志记录工具的 API 接口,但是由于版权等原因我们不能够修改它的源代码,此时 Adapter 模式便可以派上用场了。下面我们通过 Adapter 模式来使得该日志记录工具能够符合我们当前的需求。
前面说过, Adapter 模式有两种实现形式的实现结构,首先来看一下类适配器如何实现。现在唯一可行的办法就是在程序中引入新的类型,让它去继承 LogAdaptee 类,同时又实现已有的 ILogTarget 接口。由于 LogAdaptee 有两种类型的方式,自然我们要引入两个分别为 DatabaseLogAdapter FileLogAdapter 的类。
5 引入类适配器后的结构图
实现代码如下:
public class DatabaseLogAdapter:DatabaseLog,ILogTarget
{
    publicvoid Write()
    {
        WriteLog();
    }
}
 
public class FileLogAdapter:FileLog,ILogTarget
{
    publicvoid Write()
    {
        this.WriteLog();
    }
}
这里需要注意的一点是我们为每一种日志记录方式都编写了它的适配类,那为什么不能为抽象类 LogAdaptee 来编写一个适配类呢?因为 DatabaseLog FileLog 虽然同时继承于抽象类 LogAdaptee ,但是它们具体的 WriteLog() 方法的实现是不同的。只有继承于该具体类,才能保留其原有的行为。
我们看一下这时客户端的程序的调用方法:
public class App
{
    publicstaticvoid 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;   
    }
    publicvoid Write()
    {
        _adaptee.WriteLog();
    }
}
与类适配器相比较,可以看到最大的区别是适配器类的数量减少了,不再需要为每一种具体的日志记录方式来创建一个适配器类。同时可以看到,引入对象适配器后,适配器类不再依赖于具体的 DatabaseLog 类和 FileLog 类,更好的实现了松耦合。
再看一下客户端程序的调用方法:
public class App
{
    publicstaticvoid Main()
    {
       
        ILogTarget dbLog = new LogAdapter(new DatabaseLog());
        dbLog.Write("Logging Database...");
 
        ILogTarget fileLog = new LogAdapter(new FileLog());
        fileLog.Write("Logging Database...");
    }
}
通过 Adapter 模式,我们很好的实现了对现有组件的复用。对比以上两种适配方式,可以总结出,在类适配方式中,我们得到的适配器类 DatabaseLogAdapter FileLogAdapter 具有它所继承的父类的所有的行为,同时也具有接口 ILogTarget 的所有行为,这样其实是违背了面向对象设计原则中的类的单一职责原则,而对象适配器则更符合面向对象的精神,所以在实际应用中不太推荐类适配这种方式。再换个角度来看类适配方式,假设我们要适配出来的类在记录日志时同时写入文件和数据库,那么用对象适配器我们会这样去写:
public class LogAdapter:ILogTarget
{
    private LogAdaptee _adaptee1;
    private LogAdaptee _adaptee2;
 
    public LogAdapter(LogAdaptee adaptee1,LogAdaptee adaptee2)
    {
        this._adaptee1= adaptee1;
        this._adaptee2= adaptee2;
    }
    publicvoid Write()
    {
        _adaptee1.WriteLog();
        _adaptee2.WriteLog();
    }
}
如果改用类适配器,难道这样去写:
public class DatabaseLogAdapter:DatabaseLog,FileLog,ILogTarget
{
    publicvoid Write()
    {
        //WriteLog();
    }
}
显然是不对的,这样的解释虽说有些牵强,也足以说明一些问题,当然了并不是说类适配器在任何情况下都不使用,针对开发场景不同,某些时候还是可以用类适配器的方式。
.NET中的适配器模式
1 .Adapter模式在.NET Framework中的一个最大的应用就是 COM Interop 。COM 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模式的应用就是DataAdapter。ADO.NET为统一的数据访问提供了多个接口和基类,其中最重要的接口之一是IdataAdapter。与之相对应的DataAdpter是一个抽象类,它是ADO.NET与具体数据库操作之间的数据适配器的基类。DataAdpter起到了数据库到DataSet桥接器的作用,使应用程序的数据操作统一到DataSet上,而与具体的数据库类型无关。甚至可以针对特殊的数据源编制自己的DataAdpter,从而使我们的应用程序与这些特殊的数据源相兼容。注意这是一个适配器的变体。
实现要点
1 Adapter 模式主要应用于“希望复用一些现存的类,但是接口又与复用环境要求不一致的情况”,在遗留代码复用、类库迁移等方面非常有用。
2 Adapter 模式有对象适配器和类适配器两种形式的实现结构,但是类适配器采用“多继承”的实现方式,带来了不良的高耦合,所以一般不推荐使用。对象适配器采用“对象组合”的方式,更符合松耦合精神。
3 Adapter 模式的实现可以非常的灵活,不必拘泥于 GOF23 中定义的两种结构。例如,完全可以将 Adapter 模式中的“现存对象”作为新的接口方法参数,来达到适配的目的。
4 Adapter 模式本身要求我们尽可能地使用“面向接口的编程”风格,这样才能在后期很方便的适配。 [ 以上几点引用自 MSDN WebCast]
效果
对于类适配器:
1 .用一个具体的 Adapter 类对 Adaptee Taget 进行匹配。结果是当我们想要匹配一个类以及所有它的子类时,类 Adapter 将不能胜任工作。
2 .使得 Adapter 可以重定义 Adaptee 的部分行为,因为 Adapter Adaptee 的一个子类。
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 适配器模式 ( 结构型模式 )

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值