文章目录
适配器(Adapter)模式
隶属类别——对象结构型模式 & 类对象结构模式(Java无法实现)
1. 意图
将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作
2. 别名
包装器Wrapper
3. 动机
有时,为复用而设计的工具箱类不能够被复用的原因仅仅是因为它的接口与专业应用领域所需要的接口不匹配。
例如,有一个绘图编辑器,这个编辑器允许用户绘制和排列基本图元(线、多边形和正文等)生成图片和图表。这个绘图编辑器的关键抽象是图形对象。图形对象有一个可编辑的形状,并可以绘制自身。图形对象的接口由一个Shape的抽象类定义。绘图编辑器为每一种图形对象定义了一个Shape的子类:LineShape类对应于直线,PolygonShape类对应对边型,等等
想LineShape和PolygonShape这样的基本几何图形的类比较容易实现,这是由于他们的绘制图和编辑功能本来就很有限。但是对于可以显示和编辑的正文的TextsHAPE子类来说,实现相当困难,因为即使是基本的正文编辑也要涉及到复杂的屏幕刷新和缓冲区管理。同时,成品的用户界面工具箱可能依旧提供了一个复杂的TextView类,但是工具箱的设计者但是并没有考虑Shape的存在,因此TextView对象不能互换
一个应用可能会有一些类具有不同的接口并且这些接口互不兼容,在这样的应用中像TextView这样已经存在并且互不相关的类如何协同工作呢?我们可以改变TextView类使它兼容Shape类的接口。但前提是必须有这个工具箱的源码。然而即使我们得到了这些源代码,修改TextView也是没有什么意义的;因为不应该仅仅为了实现一个应用,工具箱就不得不采用一些与特定领域相关的接口。
我们可以不用上面的方法,而定义一个TextShape(或者叫TextAdpater)类,由它来适配TextView的接口和Shape接口。我们可以用两种方法来做这件事
-
1.继承Shape类的接口和TextView的实现
-
2.或者将一个TextView实例作为TextShape的组成部分(组合的方式进行委托),并且使用TextView的接口实现TextShape。
这两种方法恰恰对应于Adapter模式的类和对象版本。我们将TextShape称之为适配器(adpater).
上线的类图说明了对象适配器实例。它说明了Shape类中声明的BoundingBox请求如何被转换成在TextView类中定义的operation1()请求.由于TextShape将TextView的接口(指代方法入口)与Shape的接口进行了匹配。因此绘图编辑器就可以复用原先不兼容的TextView类。
Adapter时常还要负责提供那些被匹配的类所没有提供的功能,上面的类图说明了适配器如何实现这些职责,由于绘图编辑器允许用户交互的将每一个Shape对象“拖动”到一个新的位置,而TextView设计中没有这种功能,我们可以实现TextShape的createManipulator操作,从而增加这个缺少的功能,这个操作返回一个Manipulator的一个实现类的实例。
Manipulator是一个接口(也可以是抽象类),它所描述的对象知道如何驱动Shape类响应相应的用户输入,例如将图形拖动到一个新的位置,对应于不同形状的图形,Manipulator有不同的子类,例如子类TextManipulator对应于TextShape。TextShape通过返回一个TextManipulator实例增加了TextView中缺少而Shape中需要的功能。
4. 适用性
以下情况使用Adapter模式:
- 你想使用一个已经存在的类,而它的接口不符合你的需求。
- 你想创建一个可以复用的类,该类可以与其它不相关的类或不可预见的类(即那些接口可能不一定兼容的类)协同工作。
- (仅适用于对象Adapter) 你想使用一些已经存在的子类,但是不可能对每一个都进行子类化以匹配它们的接口。对象适配器可以适配它们父类的接口。
5. 结构
由于Java不能多重继承,所以无法实现类的适配器,这里只给出了对象适配器。对象适配器依赖于对象组合,如下图所示。
6. 参与者
-
Target(Shape)
——定义Client使用的与特定领域相关的接口
-
Client(DrawingEditor)
——与符合Targer接口的对象协同合作
-
Adaptee(TextView)
——定义一个已经存在的接口(这里指方法入口),这个接口需要被适配。可以使类也可以使抽象类也可以使接口。其子类也可以进行适配。
-
Adapter(TextShape)
——对于Adaptee的接口与Target接口进行适配
7. 协作
- Client在Adapter实例上调用一些操作。接着适配器调用Adaptee的操作以实现这个请求。
8. 效果
类适配器和对象适配器有不同的权衡,类适配器:
- 用一个具体的Adapter类与Adaptee与Target进行匹配。结果是当我们想要匹配一个类以及所有它的子类是,类Adapter将不能胜任工作
- 使得Adapter可以重定义Adaptee的部分行为,因为Adapter是一个Adaptee的子类。
- 仅仅引入了一个对象,不需要额外的指针以简洁得到Adaptee。
对象适配器则
- 允许一个Adapter与多个Adaptee——即Adaptee本身以及它的所有子类(如果有子类的话)——同时工作,Adapter也可以一次给所有的Adaptee添加功能。
- 使得重定义Adaptee的行为比较困难。这就需要生成Adaptee的子类并且使得Adapter引用这个具体子类而不是引用Adaptee本身。
使用Adapter模式时需要考虑的其它一些因素有
-
1.Adapter的匹配程度 对Adaptee的接口与Target的接口进行匹配的工作量各个Adapter可能不一样。工作范围可能是,从简单的接口转换(例如改变操作名)到支持完全不同操作集合。Adapter的工作量取决于Target接口和Adaptee接口的相似程度。
-
2.可插拔的Adapter 当其他的类使用一个类是,如果所需的假定条件越少,这个类就更具有复用性。如果将接口匹配构建为一个类,就不需要假定对其他的类可见的是一个相同接口。也就是说,接口匹配使得我们可以将自己的类加入到一些现有的系统中去,而这些系统对这个类的接口可能会有所不同。Object-Work/Smalltalk[Par90]使用pluggable adapter一次描述那些具有内部接口适配的类。
考虑TreeDisplay窗口组件,它可以图形化显示树状结果。如果这是一个具有特殊用途的窗口组件,仅在一个应用中使用,我们可能要求它显示的对象有一个特殊的接口,即它都是抽象类的Tree的子类。如果我们希望使用TreeDisplay具有良好的复用性的话(比如说,我们希望将它作为可用窗口组件工具箱的一部分),那么这种要求将是不合理的,应用程序将自己定义树结构类,而不应一定要使用我们的抽象类Tree。不同的树结构会有不同的接口。
例如,一个目录层次结构中,可以通过GetSubdirectories操作进行访问子目录,然而在一个继承式层次结构中,相应的操作可能被称为GetSubclasses。尽管这种层次结果使用的接口不同,一个可复用的TreeDisplay窗口组件必须能显示所有这两种结果。也就是说,TreeDisplay应具有接口适配的功能。
-
3.使用双向适配器提供透明的操作 使用适配器的一个潜在问题是,它们不对所有的客户都透明,被适配的对象不再兼容Adaptee的接口(的确)。因此并不是所有Adaptee对象可以被使用的地方它都可以被使用。双向适配器提供了这样的透明性。在两个不同的客户需要用不同的方式查看同一个对象时(对两边都适配),双向适配器尤其有用。
考虑一个双向适配器,它将图形编辑框架Unidraw[VL90]与约束求解工具箱QOCA[HHMV92]集成起来。这两个系统都有一些类,这些类显式地表示变量:Unidraw含有类StateVariable,QQCA含有类ConstraintVariable,如下图所示,为了使Unidraw与QOCA协同工作,必须使类ContraintVariable与类StateVariable相匹配,而为了将QOCA的求解结果传递给Unidraw,必须使用StateVariable与ConstraintVariable向匹配。
原文的方案是采用多重继承的方法来实现,而Java并不支持多重继承。所以,我自己单独实现了我认为正确的双向适配器,如上图,我去stackoverflow问一下,看看对不对,对的话有没有什么更好的写法?
9. 实现
尽管Adapter模式的实现方式通常简单直接,但是仍需要注意以下一些问题:
-
1.使用Java实现适配器类 在使用Java实现适配器类是,Adapter类应该实现Target接口(不一定非要是接口,有抽象方法入口,严格来说,是具体类也行,但是如果是具体类的话,会带来很多问题,添加很多多余的特性),最好使用构造器去初始化去初始化Adaptee引用。
-
2.可插拔的适配器 有许多办法可以实现可插入的适配器。例如,在前面描述的TreeDisplay窗口组件可以自动的布置和显示层次式结果。对于它,有三种实现方法:
首先(这也是所有者三种实现都要做的)是为Adaptee找到一个"窄"接口。即即可用于适配的最小操作集。因为包含较小的窄接口相对包含较多操作的宽接口比较容易进行匹配。对于TreeDisplay而言,被匹配的对象可以是任何一个层次式结果。因此最小接口集合仅包含两个操作:一个操作定义如何在层次结果中表示一个节点,另一个操作返回该节点的子节点。
对于这个窄接口,有以下三个实现途径:
- a. 使用抽象操作 在Tree