一、概述
Adapter(适配器)模式又称Wrapper模式,主要用于将一个类的接口转换成客户希望的另外一个接口,解决两个已有接口之间不匹配的问题。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。因此,Adapter模式经常被描述成第三方函数库或旧的程序库与现有系统接口 /需求不一致时的救星,也正是由于Adapter模式的这种特性,有人也将Adapter模式比喻成变压器,变压器把一种电压变换成另一种电压。美国的生活用电电压是110V,而中国的电压是220V。如果要在中国使用美国电器,就必须有一个能把220V电压转换成110V电压的变压器,这个变压器就是一个Adapter。
二、结构
在GoF的DP一书中给出了两种Adapter模式的应用:Class Adapter和Object Adapter,两种应用模式的类图如下所示:
图 1:Class Adapter
其中涉及的角色包括:
目标(Target)角色:这是客户所期待的接口。
源(Adaptee)角色:需要适配的类。
适配器(Adapter)角色:把源接口转换成目标接口。这一角色必须是类。
图 2:Object Adapter
其中涉及的角色包括:
目标(Target)角色:这是客户所期待的接口。目标可以是具体的或抽象的类,也可以是接口。
源(Adaptee)角色:需要适配的类。
适配器(Adapter)角色:通过在内部包装(Wrap)一个Adaptee对象,把源接口转换成目标接口。
Class Adapter中adapter类通过继承adaptee类获得adaptee类的功能,而Object Adapter中adpter通过包容adaptee获得其功能。基于“当涉及到依存性时,应当始终优先选择组合 /成员关系而不是继承”的设计原则( <Exceptional C ++>),并且由于多继承在使用上的复杂性,及在部分情况下不可行等原因,Object Adapter的运用显得更加广泛。
此外,当需要改变多个已有子类的接口时,如果使用Class Adapter模式,就要针对每一个子类做一个适配器,而这不太实际,此时比较适合使用Object Adapter。
Object Adapter模式的基本思想在于:Delegate委托,即将需要完成的工作交给adaptee去完成,当然,为了兼容Client需求与adaptee接口之间的不一致,在委派的过程中,我们往往也需要作一些适当的调整。
三、应用
Adapter模式可应用于如下的情况:
1、系统需要使用现有的类,而此类的接口不符合系统的需要。(示例 1、 2、 3均属于这种情况,只是出发点稍有不同)
2、想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有与一致的接口。这种情况从不同的角度考虑,可能被划入Facade模式的范畴,但从与现有设计适配的角度考虑该问题,则将其划归Adapter模式也是可以理解的。
3、通过接口转换,将一个类插入另一个类系中。有人举过这样一个例子:虎与飞禽是没有直接关联的两类动物,但是现在出来了个“飞虎”,它同时具有虎肉食动物跟飞禽会飞的特质,要在飞禽这个类系中添加一个成员类“飞虎”,除了直接实现“飞虎”类,还有一种简单的办法是实现一个Adapter类,在其中包容一个虎的对象,同时实现飞禽的接口即可。当然,对于这个问题,多继承或者实现多接口可能是一个更直观的作法,在实际应用中,可视具体需要确定采用何种作法。
四、优缺点
五、举例
1、出发点:委托实现,提供不同接口
在STL的实现中,我们可以轻易地找到很多Object Adapter的例子。如各种iterator最终的实现均是Delegate给具体的容器类实现的,以insert_iterator为例 :
template < class Container >
class insert_iterator {
protected :
Container * container ; // adaptee is here
typename Container ::iterator iter ;
public :
typedef output_iterator_tag iterator_category ;
typedef void value_type ;
typedef void difference_type ;
typedef void pointer ;
typedef void reference ;
insert_iterator (Container & x , typename Container ::iterator i )
: container (&x ), iter (i ) {}
insert_iterator <Container >&
operator =( const typename Container ::value_type & value ) {
iter = container ->insert (iter , value ); // delegate operation to adaptee
++iter ;
return * this ;
}
insert_iterator <Container >& operator *() { return * this ; }
insert_iterator <Container >& operator ++() { return * this ; }
insert_iterator <Container >& operator ++( int ) { return * this ; }
};
此外,std ::stack,std ::queue等也都是将相应的操作委托给deque完成的,而map实际上是通过包容rb_tree对象实现的。关于STL中应用Adapter的详细分析可以参考jjhou的 <STL源码剖析 >一书,jjhou分container adapter、iterator adpater、functor adpater三个方面深刻地剖析了STL中Adapter的运用。
2、出发点:提供接口默认实现,简化客户代码
对于Java程序员而言,Adapter这个词可能再熟悉不过了,JDK中有很多Adapter类,如WindowAdapter、MouseAdapter等,分别实现了WindowListener、MouseListener等接口,这些都是Class Adapter的实例。由于这些接口非常常用,而我们往往只关心这些接口的某几个方法,但implements接口要求必须实现接口的所有方法,以至于我们经常不得已将接口方法实现为空。这实在是一件非常繁琐的事情,并且,代码中充斥着一些空方法,也会影响阅读,为此,JDK通过随接口一同发布的Adapter类为这些接口方法提供了缺省的空方法,我们只需要从Adapter类派生(实际上,由于Adapter类往往被用于编写匿名内部类,因此,连派生这一步都省了,编译器会自动为我们完成这一步),并为需要处理的接口方法提供实现即可。
这与上面讨论的Adapter的定义稍有不同,但本质上还是比较相似的:解决需求与实现间不一致的问题。
3、出发点:接口转换
有时候,现有的实现在设计之初没有或者不便于实现一些基本功能以外的接口,当遇到这种问题的时候,一种解决方案是在现有的实现中添加这些接口的实现,但往往更好的办法是提供一个Adapter类,在维持原有接口的同时,达到支持新接口的目的。此外,在JDK中还存在着一些为了兼容老的设计而运用Adapter模式的例子,以下代码取自java .util .Collections:
public static Enumeration enumeration (final Collection c ) {
return new Enumeration () {
Iterator i = c .iterator ();
public boolean hasMoreElements () {
return i .hasNext ();
}
public Object nextElement () {
return i .next ();
}
};
}
其中的匿名类就是一个Adapter,目的是为了兼容老的Enumeration(Java1 .2修改了集合框架的设计,并引入了Iterator)。
对于普通应用而言,这种应用Adapter模式的情况比较常见。如一个用javax .swing .JTable显示AddressBook的应用,根据设计,我们不可能直接为AddressBook类实现JTable所需的TableModel接口(这会让其它使用AddressBook类的人莫名其妙),要在JTable中显示AddressBook,一种可行的作法是引入Adapter类,实现TableModel接口,在保持JTable和AddressBook独立性的基础上,通过引入Adapter类达到适配的目的。相应的示例代码如下所示:
import java .util .*;
import javax .swing .*;
import javax .swing .table .*;
class AddressItem {
public String name ;
public String address ;
public String tel ;
// ...more attribute
public AddressItem (String name , String address , String tel ) {
this .name = name ;
this .address = address ;
this .tel = tel ;
}
public String getName () {
return name ;
}
public String getAddress () {
return address ;
}
public String getTel () {
return tel ;
}
}
class AddressBook {
List personList = new Vector ();
public int getSize () {
return personList .size ();
}
public boolean addItem (AddressItem item ) {
return personList .add (item );
}
public boolean addItem (AddressItem [] aItem ) {
return personList .addAll (Arrays .asList (aItem ));
}
public AddressItem getItem ( int index ) {
return (AddressItem )personList .get (index );
}
}
class AddressBookTableAdapter extends AbstractTableModel
{
AddressBook ab ;
String [] columnName = { "Name" , "Address" , "Tel." };
public AddressBookTableAdapter (AddressBook ab ) {
this .ab = ab ;
}
// TableModel impl
public int getRowCount () {
return ab .getSize ();
}
public int getColumnCount () {
return columnName .length ;
}
public Object getValueAt ( int row , int col ) {
AddressItem item = ab .getItem (row );
if (item == null )
return null ;
switch (col ) {
case 0 :
return item .getName ();
case 1 :
return item .getAddress ();
case 2 :
return item .getTel ();
default :
return null ;
}
}
public String getColumnName ( int c ) {
return columnName [c ];
}
}
class AddressBookTest extends JFrame {
AddressItem [] aItem = {
new AddressItem ( "Bill" , "Sky Road. No.1" , "123" ),
new AddressItem ( "David" , "Sky Road. No.2" , "456" ),
new AddressItem ( "God" , "Sky Road. No.3" , "789" )
};
AddressBook ab = new AddressBook ();
public AddressBookTest () {
ab .addItem (aItem );
TableModel model = new AddressBookTableAdapter ( ab );
getContentPane ().add ( new JScrollPane ( new JTable (model )));
}
public static void main ( String [] args ) {
JFrame frame = new AddressBookTest ();
frame .setTitle ( "Tables and Models" );
frame .setBounds ( 300 , 300 , 450 , 300 );
frame .setDefaultCloseOperation (JFrame .DISPOSE_ON_CLOSE );
frame .show ();
}
}
4、出发点:简化接口,分离实现
有些情况下,随着系统的升级和设计的修改,新的实现方式可能会对外提供新的接口,为了保持向下兼容,一种普遍的作法是在客户(使用我们接口的人,并非最终Customer)接口类中通过重载(或Factory Pattern等方式,参考笔记 2中Name Factory的例子)提供针对不同接口的实现。这种实现存在两个严重问题:
1)代码难以维护,随着系统的升级,接口类将变得越来越臃肿;
2)底层对客户过于透明,或者说,客户程序员要很好地使用接口类,必须对底层的实现策略有所了解。
一种改进的作法是:
针对不同接口提供不同的Adapter类,最终将接口统一起来。
这种实现方式不仅使得底层对客户程序员透明,也使得今后进行系统扩展所受到的束缚和需要进行的修改更小。
Adapter(适配器)模式又称Wrapper模式,主要用于将一个类的接口转换成客户希望的另外一个接口,解决两个已有接口之间不匹配的问题。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。因此,Adapter模式经常被描述成第三方函数库或旧的程序库与现有系统接口 /需求不一致时的救星,也正是由于Adapter模式的这种特性,有人也将Adapter模式比喻成变压器,变压器把一种电压变换成另一种电压。美国的生活用电电压是110V,而中国的电压是220V。如果要在中国使用美国电器,就必须有一个能把220V电压转换成110V电压的变压器,这个变压器就是一个Adapter。
二、结构
在GoF的DP一书中给出了两种Adapter模式的应用:Class Adapter和Object Adapter,两种应用模式的类图如下所示:
图 1:Class Adapter
其中涉及的角色包括:
目标(Target)角色:这是客户所期待的接口。
源(Adaptee)角色:需要适配的类。
适配器(Adapter)角色:把源接口转换成目标接口。这一角色必须是类。
图 2:Object Adapter
其中涉及的角色包括:
目标(Target)角色:这是客户所期待的接口。目标可以是具体的或抽象的类,也可以是接口。
源(Adaptee)角色:需要适配的类。
适配器(Adapter)角色:通过在内部包装(Wrap)一个Adaptee对象,把源接口转换成目标接口。
Class Adapter中adapter类通过继承adaptee类获得adaptee类的功能,而Object Adapter中adpter通过包容adaptee获得其功能。基于“当涉及到依存性时,应当始终优先选择组合 /成员关系而不是继承”的设计原则( <Exceptional C ++>),并且由于多继承在使用上的复杂性,及在部分情况下不可行等原因,Object Adapter的运用显得更加广泛。
此外,当需要改变多个已有子类的接口时,如果使用Class Adapter模式,就要针对每一个子类做一个适配器,而这不太实际,此时比较适合使用Object Adapter。
Object Adapter模式的基本思想在于:Delegate委托,即将需要完成的工作交给adaptee去完成,当然,为了兼容Client需求与adaptee接口之间的不一致,在委派的过程中,我们往往也需要作一些适当的调整。
三、应用
Adapter模式可应用于如下的情况:
1、系统需要使用现有的类,而此类的接口不符合系统的需要。(示例 1、 2、 3均属于这种情况,只是出发点稍有不同)
2、想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有与一致的接口。这种情况从不同的角度考虑,可能被划入Facade模式的范畴,但从与现有设计适配的角度考虑该问题,则将其划归Adapter模式也是可以理解的。
3、通过接口转换,将一个类插入另一个类系中。有人举过这样一个例子:虎与飞禽是没有直接关联的两类动物,但是现在出来了个“飞虎”,它同时具有虎肉食动物跟飞禽会飞的特质,要在飞禽这个类系中添加一个成员类“飞虎”,除了直接实现“飞虎”类,还有一种简单的办法是实现一个Adapter类,在其中包容一个虎的对象,同时实现飞禽的接口即可。当然,对于这个问题,多继承或者实现多接口可能是一个更直观的作法,在实际应用中,可视具体需要确定采用何种作法。
四、优缺点
五、举例
1、出发点:委托实现,提供不同接口
在STL的实现中,我们可以轻易地找到很多Object Adapter的例子。如各种iterator最终的实现均是Delegate给具体的容器类实现的,以insert_iterator为例 :
template < class Container >
class insert_iterator {
protected :
Container * container ; // adaptee is here
typename Container ::iterator iter ;
public :
typedef output_iterator_tag iterator_category ;
typedef void value_type ;
typedef void difference_type ;
typedef void pointer ;
typedef void reference ;
insert_iterator (Container & x , typename Container ::iterator i )
: container (&x ), iter (i ) {}
insert_iterator <Container >&
operator =( const typename Container ::value_type & value ) {
iter = container ->insert (iter , value ); // delegate operation to adaptee
++iter ;
return * this ;
}
insert_iterator <Container >& operator *() { return * this ; }
insert_iterator <Container >& operator ++() { return * this ; }
insert_iterator <Container >& operator ++( int ) { return * this ; }
};
此外,std ::stack,std ::queue等也都是将相应的操作委托给deque完成的,而map实际上是通过包容rb_tree对象实现的。关于STL中应用Adapter的详细分析可以参考jjhou的 <STL源码剖析 >一书,jjhou分container adapter、iterator adpater、functor adpater三个方面深刻地剖析了STL中Adapter的运用。
2、出发点:提供接口默认实现,简化客户代码
对于Java程序员而言,Adapter这个词可能再熟悉不过了,JDK中有很多Adapter类,如WindowAdapter、MouseAdapter等,分别实现了WindowListener、MouseListener等接口,这些都是Class Adapter的实例。由于这些接口非常常用,而我们往往只关心这些接口的某几个方法,但implements接口要求必须实现接口的所有方法,以至于我们经常不得已将接口方法实现为空。这实在是一件非常繁琐的事情,并且,代码中充斥着一些空方法,也会影响阅读,为此,JDK通过随接口一同发布的Adapter类为这些接口方法提供了缺省的空方法,我们只需要从Adapter类派生(实际上,由于Adapter类往往被用于编写匿名内部类,因此,连派生这一步都省了,编译器会自动为我们完成这一步),并为需要处理的接口方法提供实现即可。
这与上面讨论的Adapter的定义稍有不同,但本质上还是比较相似的:解决需求与实现间不一致的问题。
3、出发点:接口转换
有时候,现有的实现在设计之初没有或者不便于实现一些基本功能以外的接口,当遇到这种问题的时候,一种解决方案是在现有的实现中添加这些接口的实现,但往往更好的办法是提供一个Adapter类,在维持原有接口的同时,达到支持新接口的目的。此外,在JDK中还存在着一些为了兼容老的设计而运用Adapter模式的例子,以下代码取自java .util .Collections:
public static Enumeration enumeration (final Collection c ) {
return new Enumeration () {
Iterator i = c .iterator ();
public boolean hasMoreElements () {
return i .hasNext ();
}
public Object nextElement () {
return i .next ();
}
};
}
其中的匿名类就是一个Adapter,目的是为了兼容老的Enumeration(Java1 .2修改了集合框架的设计,并引入了Iterator)。
对于普通应用而言,这种应用Adapter模式的情况比较常见。如一个用javax .swing .JTable显示AddressBook的应用,根据设计,我们不可能直接为AddressBook类实现JTable所需的TableModel接口(这会让其它使用AddressBook类的人莫名其妙),要在JTable中显示AddressBook,一种可行的作法是引入Adapter类,实现TableModel接口,在保持JTable和AddressBook独立性的基础上,通过引入Adapter类达到适配的目的。相应的示例代码如下所示:
import java .util .*;
import javax .swing .*;
import javax .swing .table .*;
class AddressItem {
public String name ;
public String address ;
public String tel ;
// ...more attribute
public AddressItem (String name , String address , String tel ) {
this .name = name ;
this .address = address ;
this .tel = tel ;
}
public String getName () {
return name ;
}
public String getAddress () {
return address ;
}
public String getTel () {
return tel ;
}
}
class AddressBook {
List personList = new Vector ();
public int getSize () {
return personList .size ();
}
public boolean addItem (AddressItem item ) {
return personList .add (item );
}
public boolean addItem (AddressItem [] aItem ) {
return personList .addAll (Arrays .asList (aItem ));
}
public AddressItem getItem ( int index ) {
return (AddressItem )personList .get (index );
}
}
class AddressBookTableAdapter extends AbstractTableModel
{
AddressBook ab ;
String [] columnName = { "Name" , "Address" , "Tel." };
public AddressBookTableAdapter (AddressBook ab ) {
this .ab = ab ;
}
// TableModel impl
public int getRowCount () {
return ab .getSize ();
}
public int getColumnCount () {
return columnName .length ;
}
public Object getValueAt ( int row , int col ) {
AddressItem item = ab .getItem (row );
if (item == null )
return null ;
switch (col ) {
case 0 :
return item .getName ();
case 1 :
return item .getAddress ();
case 2 :
return item .getTel ();
default :
return null ;
}
}
public String getColumnName ( int c ) {
return columnName [c ];
}
}
class AddressBookTest extends JFrame {
AddressItem [] aItem = {
new AddressItem ( "Bill" , "Sky Road. No.1" , "123" ),
new AddressItem ( "David" , "Sky Road. No.2" , "456" ),
new AddressItem ( "God" , "Sky Road. No.3" , "789" )
};
AddressBook ab = new AddressBook ();
public AddressBookTest () {
ab .addItem (aItem );
TableModel model = new AddressBookTableAdapter ( ab );
getContentPane ().add ( new JScrollPane ( new JTable (model )));
}
public static void main ( String [] args ) {
JFrame frame = new AddressBookTest ();
frame .setTitle ( "Tables and Models" );
frame .setBounds ( 300 , 300 , 450 , 300 );
frame .setDefaultCloseOperation (JFrame .DISPOSE_ON_CLOSE );
frame .show ();
}
}
4、出发点:简化接口,分离实现
有些情况下,随着系统的升级和设计的修改,新的实现方式可能会对外提供新的接口,为了保持向下兼容,一种普遍的作法是在客户(使用我们接口的人,并非最终Customer)接口类中通过重载(或Factory Pattern等方式,参考笔记 2中Name Factory的例子)提供针对不同接口的实现。这种实现存在两个严重问题:
1)代码难以维护,随着系统的升级,接口类将变得越来越臃肿;
2)底层对客户过于透明,或者说,客户程序员要很好地使用接口类,必须对底层的实现策略有所了解。
一种改进的作法是:
针对不同接口提供不同的Adapter类,最终将接口统一起来。
这种实现方式不仅使得底层对客户程序员透明,也使得今后进行系统扩展所受到的束缚和需要进行的修改更小。