首先放一张图,让大家大致了解什么叫适配器。适配器属于接口隔离的一种,它能使接口不兼容的对象能够相互合作。
假如你正在开发一款股票市场监测程序,它会从不同来源下载 XML 格式的股票数据,然后向用户呈现出美观的图表。
在开发过程中,你决定在程序中整合一个第三方智能分析函数库。但是遇到了一个问题,那就是分析函数库只兼容 JSON 格式的数据。
你可以修改程序库来支持 XML。但是,这可能需要修改部分依赖该程序库的现有代码。甚至还有更糟糕的情况,你可能根本没有程序库的源代码,从而无法对其进行修改。
你可以创建一个适配器。这是一个特殊的对象,能够转换对象接口,使其能与其他对象进行交互。
适配器模式通过封装对象将复杂的转换过程隐藏于幕后。被封装的对象甚至察觉不到适配器的存在。例如,你可以使用一个将所有数据转换为英制单位 (如英尺和英里) 的适配器封装运行于米和千米单位制中的对象。
适配器不仅可转换不同格式的数据,其还助于采用不同接口的对象之间的合作。运作方式如下:
1 - 适配器实现与其中一个现有对象兼容的接口。
2 - 现有对象可以使用该接口安全地调用适配器方法。
3 - 适配器方法被调用后将以另一个对象兼容的格式和顺序将请求传递给该对象。
有时你甚至可以创建一个双向适配器来实现双向转换调用。
让我们回到股票市场程序。为了解决数据格式不兼容的问题,你可以为分析函数库中的每个类创建将 XML 转换为 JSON 格式的适配器,然后让客户端仅通过这些适配器来与函数库进行交流。当某个适配器被调用时,它会将传入的 XML 数据转换为 JSON 结构,并将其传递给被封装分析对象的相应方法。
适配器有两种,分别是对象适配器、类适配器,一般采用对象适配器。
对象适配器的类图
类适配器使用了继承机制:适配器同时继承两个对象的接口。请注意,这种方式仅能在支持多重继承的编程语言中实现,例如 C++。
类适配器类图
真实世界类比:
如果你是第一次从美国到欧洲旅行,那么在给笔记本充电时可能会大吃一惊。不同国家的电源插头和插座标准不同。美国插头和德国插座不匹配。同时提供美国标准插座和欧洲标准插头的电源适配器可以解决你的难题。
(1)模式动机
在软件系统中,由于应用环境的变化,常常需要将 “一些现存的对象” 放在新的环境中应用,但是新环境要求的接口是这些现存对象所不满足的。
如何应对这种 “迁移的变化” ?如何既能利用现有对象的良好实现,同时又能满足新的应用环境所要求的接口?
(2)模式定义
将一个类的接口转换成客户希望的另一个接口,Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
(3)要点总结
a). Adapter模式主要应用于 “希望复用一些现存的类,但是接口又与复用环境要求不一致的情况”,在遗留代码复用、类库迁移等方面非常有用。
b). GoF 23 定义了两种Adapter模式的实现结构:对象适配器和类适配器。但类适配器采用 “多继承” 的实现方式,一般不推荐使用。对象适配器采用 “对象组合” 的方式,更符合松耦合特点。
c). Adapter模式可以实现的非常灵活,不必拘泥于 GoF23 中定义的两种结构。例如,完全可以将Adapter模式中的 “现存对象” 作为新的接口方法参数,来达到适配的目的。
下面将使用代码讲解适配器,Target是未来的接口,Adaptee是被适配者(即遗留的接口)
//目标接口(新接口)
class ITarget{
public:
virtual void process() = 0;
};
//遗留接口(老接口)
class IAdaptee{
public:
virtual void foo(int data) = 0;
virtual void bar() = 0;
};
//遗留类型
class OldClass : public IAdaptee{
//...
};
class Adapter : public ITarget{ //继承
protected:
IAdaptee* pAdaptee; //组合对象
public:
Adapter(IAdaptee* pAdaptee){
this->pAdaptee = pAdaptee;
}
virtual void process(){
int data = pAdaptee->bar();
pAdaptee->foo(data);
}
};
int main(){
IAdaptee* pAdaptee = new OldClass();
ITarget* pTarget = new Adapter(pAdaptee );
pTarget->process();
}
实际运用中,像STL源码的 stack 和 queue的代码,内部都使用了deque,这个deque相当于内部的一个Adapter,虽然没有明确的感觉谁新谁旧,但是就是用deque转成需要的stack或queue。当然他们这部分代码没用指针,是直接将deque对象放上去(如下伪码)。这时候我们并没看到stack、queue的接口是什么,它们的继承是什么,因为它们本身既是接口也是实现,且符合适配器的原则即可将老的接口转为新的接口。
class stack{
deque container;
}
class queue{
deque container;
}