1, 功能:
将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
2, 基本思想
通过一个适配器(Adapter),在已有的类(Adaptee)和客户(Client)之间进行协调,从而达到兼容的目标。
在Adapter模式中,有两种适配方法:类适配器和对象适配器。类适配器通过多重继承实现,对象适配器通过委托来实现。
OOP中的一个重要思想是:尽量使用聚合而不是继承。
一、类适配合对象适配的概念:
1)类适配
类适配器用一个具体的Adapter类对Adaptee和Target进行匹配。结果导致当我们想要匹配一个类以及所有它的子类时,类Adapter将不能胜任工作。
使得Adapter可以重定义Adaptee的部分行为,因为Adapter是Adaptee的一个子类。
仅仅引入了一个对象,并不需要额外的指针以间接得到adaptee。
2)对象适配器
允许一个Adapter与多个Adaptee(即Adaptee本身以及它的所有子类)同时工作。Adapter也可以一次给所有的Adaptee添加功能。
使得重定义Adaptee的行为比较困难。这需要生成Adaptee的子类并且使得Adapter引用这个子类而不是引用Adaptee本身。
二、类适配和对象适配的比较 (参考:http://blog.csdn.net/qinysong/category/225891.aspx)
1) 在Adapter对Adatpee的特殊性要求方面:
-
类适配:Adapter是Adaptee的子类,所以Adapter可以根据需要重新定义Adaptee的个别方法。
-
对象适配:Adapter不是Adaptee的子类,所以如果Adapter对Adaptee中的个别方法有特殊的需要,就要新建Adaptee的子类,而让Adapter使用这个子类(即Adapter组合这个子类的对象)。
2) 在Adaptee的类层次方面:
-
类适配:Adapter是Adaptee的子类,编译后就不能再更换所实现的父类Adaptee,因此如果有一个Adaptee的类层次结构,就要相应的有一个Adapter的类层次结构(Adapter是Target和Adaptee的子类,所以多一个Adapter,就需要有一个Target和Adaptee做它的父类),且新扩展Adaptee时很不方便。
-
对象适配:Adapter不是Adaptee的子类,而是通过使用的方式,所以只要Adaptee是相同类型的(拥有相同的父类),就可以在运行时更换Adapter所使用的Adaptee。这样就可以容易扩展新的Adaptee。
3, 适用情况
想使用一个已经存在的类,而它的接口不符合实际需求。
想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类(即那些接口可能不一定兼容的类)协同工作。
(仅适用于对象Adapter)想使用一些已经存在的子类,但是不可能对每一个都进行子类化以匹配它们的接口。对象适配器可以适配其父类接口。
4, 结构
图 类适配器的结构
图 对象匹配器的结构
5, 举例
借用网上一位牛人的C#的变压器例子,我用C++翻译了一下。
变压器是一种Adapter,它把一种电压转换成另外一种电压。美国的生活用电电压是110V,中国是220V。如果在老美那里买了电器,回国后就要加个变压器才可以用了。
1) 类适配器
class ElectricUtils
{
public :
virtual void TwoPhaseFaucet() = 0 ;
virtual void Power_110V() = 0 ;
};
// Adaptee Class
class ChinesePower
{
public :
ChinesePower():volt(220)
{cout<<"The Voltage in China is " << "volt" << endl;}
int getVolt() const ;
void setVolt(int v) {volt = v;}
protected :
int volt;
};
int ChinesePower::getVolt() const
{
return volt;
}
class Transformer : public ElectricUtils, // C++中应该用public继承Target
private ChinesePower // 用private继承Adaptee
{
public :
void TwoPhaseFaucet(){cout<<"The Two-Phase-Facucet provides transformation from 3 to 2 phase"<< endl;}
void Power_110V(){volt=getVolt()/2;cout<<"Power transformer. The output is "<<volt<< endl;}
};
int main()
{
ElectricUtils * dev = new Transformer();
dev-> Power_110V();
dev-> TwoPhaseFaucet();
return 0 ;
}
2) 对象适配器
对象适配器中的ElectricUtils(Target)类和ChinaPower(Adaptee)类都不需要变换,变化的是Adapter“组合”它们的方式。所以这里只需要修改一下Adapter类。
{
public:
Transformer(){power = new ChinesePower();}
~Transformer(){if (power != NULL) delete power;}
void TwoPhaseFaucet(){cout<<"The Two-Phase-Facucet provide transformation from 3 phase to 2 phase"<<endl;}
void Power_110V(){power->setVolt(power->getVolt()/2);cout<<"Power transformer. The output is "<<power->getVolt()<<endl;}
private:
ChinesePower * power;
};
6, 可插入的Adapter:
用书上的例子来说,有一个图形化显示树状结构的TreeDisplay窗口组件,如果它仅在一个应用中使用,我们可能要求它所显示的对象有特殊的接口,也就是说这些被显示的对象应该是Tree的子类。但是不同的应用不可能都要使用我们的抽象类Tree,它们可能会自己定义自己的树结构,而不同的树结构会有不同的接口。
例如在一个目录层次结构中,通过调用GetSubdirectories操作来访问子目录,然而在继承式层次结构中,相应的操作是GetSubclasses。这两种应用结构中使用的接口不同,所以TreeDisplay组件需要有接口适配的功能,使得它能够显示这两种结构。
关于可插入的适配器,首先要为Adaptee找到一个“窄”接口,即可用于适配的最小操作集。因为包含较少操作的窄接口相对包含较多操作的宽接口比较容易进行匹配。对TreeDisplay而言,最小接口集合仅包含两个操作:一个操作用于在层次结构中表示一个节点,另一个操作返回该节点的子节点。接下来,有三种方式可实现这个窄接口。
一、抽象接口操作:
在TreeDisplay中定义窄Adaptee接口相应的抽象操作。由子类来实现这些抽象操作并匹配具体的树结构的对象。例如,DirectoryTreeDisplay通过访问目录结构实现这些窄接口操作。下图中,TreeDisplay是Target,直接被客户使用。FileSystemEntity和ClassHiracheyEntity是Adaptee,是我们希望“借用”它们某些实现的类。DirectoryTreeDisplay和ClassTreeDisplay继承TreeDisplay,它们是Adapter,实现了在Target中定义的Adaptee的最窄接口。
二、代理对象:
TreeDisplay将访问树结构的请求转发到代理对象,TreeDisplay的客户进行一些选择,并将这些选择提供给代理对象,这样客户可以对适配加以控制。
比较上面两图,可以看出,代理对象方法其实就是将TreeDisplay(Target)中,将“窄”接口剥离出去,形成一个代理,并且在TreeDisplay中定义一个获取代理接口的方法。其他方面都和抽象接口方法一样。
C++中,需要代理的显示接口定义,上图中的TreeAccessorDelegate就是一个纯虚类作为一个接口。然后使用继承机制,真正的代理(DirectoryBrowser)实现接口。
三、参数化的适配器
在Smalltalk中支持可插入适配器的方法是,用一个或多个模块对适配器进行参数化。由于鄙人对Smalltalk一点儿都不了解,所以这里略过J
7, 总结
(1) Adapter对Adaptee的接口与Target的接口进行匹配的工作量取决于Target接口与Adaptee接口的相似程度。
(2) 使用C++实现类适配器时,Adapter类应该采用public方式继承Target类,用private方式继承Adaptee类。
(3) 当其他的类使用一个类时,如果所需的假定条件越少,这个类就更具可复用性,即Adapter是可插入的。为使一个Adapter可插入,就必须要为Adaptee找到一个窄接口,即可用于适配的最小操作集。