本文总结摘自刘伟老师的《设计模式》和程杰老师的《大话设计模式》
1.定义
适配器模式定义:将一个接口转化成客户希望的另一个接口,适配器模式使原本由于接口不兼容而不能一起工作的那些类可以一起工作。
2.为什么需要适配器模式
通常情况下,客户端可以通过目标类的接口访问它所提供的服务。有时,现有的类可以满足客户类的功能需要,但是它所提供的接口不一定是客户类所期望的,这可能是因为现有类中方法名与目标类中定义的方法名不一致等原因所导致的。如现在目标类中定义的方法名为method1(),客户端已经针对该方法进行编程,而现有类中方法method2()恰好满足客户端的要求,如何在不修改原有目标类和客户端代码的基础上确保能够使用到现有类中的method2()方法,就是适配器模式所要解决的问题。
在软件开发中,系统的数据和行为都正确,但接口不符时,我们应该考虑用适配器,目的是使控制范围之外的一个原有对象与某个接口匹配。适配器模式主要应用于希望复用一些现有的类,但是接口又与复用环境要求不一致的情况,比如在需要对早期代码复用一些功能等应用上很有价值。(看不懂没关系,继续看下去)。
3.适配器模式结构
适配器模式包括类适配器模式和对象适配器模式。
(类适配器用继承,对象适配器用组合/聚合)
3.2 类适配器模式
类适配器模式如图3-1所示

类适配器用继承。
根据类适配器模式结构图所示,在类适配器中,适配者类Adaptee 没有request() 方法,而客户端期待这个方法,但是在适配者类中实现了specificRequest() 方法,该方法所提供的实现正是客户所需要的。为了使客户能够使用适配者类,我们提供了一个中间类,即适配器类Adapter,适配器类实现了抽象目标类接口,并继承了适配者类,在适配器类的request() 方法中调用所继承的适配者类的specificRequest() 方法,实现了适配的目的。因为适配器类与适配者类是继承关系,所以这种适配器模式称为类适配器模式。典型的类适配器代码如下:
public class Adapter extends Adaptee implements Target
{
public void request()
{
specificRequest();
}
}
3.3 对象适配器模式
对象适配器模式如图3-2所示:

对象适配器用组合/聚合。
根据对象适配器结构图,在对象适配器中,客户端需要调用request() 方法,而适配器者类Adaptee没有该方法,但是它所提供的specificRequest() 方法却是客户端所需要的。为了使客户端能够使用适配者类,需要提供一个包装类Adapter,即适配器类。这个包装类包装了一个适配者的实例,从而将客户端与适配者衔接起来,在适配器的request() 方法中调用适配者的specificRequest() 方法。因为适配器类与适配者类是关联关系,所以这种适配器模式称为对象适配器模式。典型的对象适配器模式代码如下:
public class Adapter extends Target
{
private Adaptee adaptee;
public Adapter(Adaptee adaptee)
{
this.adaptee=adaptee;
}
public void request()
{
adaptee.specificRequest();
}
}
适配器模式更多的是强调对代码的组织,而不是功能的实现。
3.3.1 对象适配器模式例子--加密适配器
某系统需要提供一个加密模块,将用户信息(如密码等机密信息)加密之后再存储在数据库中,系统已经定义好了数据库操作类。为了提高开发效率,现需要重用已有的加密算法,这些算法封装在一些由第三方提供的类中,有些甚至没有源代码。使用适配器模式设计该加密模块,实现在不修改现有类的基础上重用第三方加密方法。
该例题结构图如图3-3所示:
其中的Caesar和NewCipher类是第三方加密类。

实例主要代码解释
(1)目标抽象类DataOperation(数据操作类)
public abstract class DataOperation
{
private String password;
public void setPassword(String password)
{
this.password=password;
}
public String getPassword()
{
return this.password;
}
public abstract String doEncrypt(int key,String ps);
}
DataOperation类中包含了抽象方法doEncrypt(),客户端针对抽象类DataOperation进行编程,在客户端代码中调用DataOperation的doEncrypt()实现数据加密。
(2)适配者类Caesar(数据加密类)
public final class Caesar
{
public String doEncrypt(int key,String ps)
{
String es="";
for(int i=0;i<ps.length();i++)
{
char c=ps.charAt(i);
if(c>='a'&&c<='z')
{
c+=key%26;
if(c>'z') c-=26;
if(c<'a') c+=26;
}
if(c>='A'&&c<='Z')
{
c+=key%26;
if(c>'Z') c-=26;
if(c<'A') c+=26;
}
es+=c;
}
return es;
}
}
Caesar类是一个由第三方提供的数据加密类,该类定义为final类,无法继承。因此本实例无法通过类适配器来实现,只能使用对象适配器实现。客户端在使用时无须关心Caesar类的源代码,甚至无法获得该类的源代码,只有编译后的class文件。
Caesar加密算法比较简单,通过26个字母移位来实现加密运算。
(3)适配器类CipherAdapter(加密适配器类)
public class CipherAdapter extends DataOperation
{
private Caesar cipher;
public CipherAdapter()
{
cipher=new Caesar();
}
public String doEncrypt(int key,String ps)
{
return cipher.doEncrypt(key,ps);
}
}
CipherAdapter类充当适配器角色,由于Caesar类无法继承,本实例采用对象适配器模式,在CipherAdapter类中定义一个Caesar类型的成员对象,在CipherAdapter类的构造函数中实例化Caesar对象,CipherAdapter与Caesar类之间是组合关联关系。
(4)客户端代码
public class Client
{
public static void main(String args[])
{
DataOperation dao= new CipherAdapter();
dao.setPassword("sunnyLiu");
String ps=dao.getPassword();
String es=dao.doEncrypt(6,ps);
System.out.println("明文为:" + ps);
System.out.println("密文为:" + es);
}
}
4.何时使用适配器模式
在想使用一个已经存在的类,但如果它的接口,也就是它的方法和你的要求不相同时,就应该考虑使用适配器模式。
注意:其实用适配器模式是一个无奈之举,有点“亡羊补牢”的感觉,是软件就有维护的一天,维护就有可能会因为不同的开发人员、不同的产品、不同的厂家而造成功能类似而接口不同的情况,此时就是适配器模式大展拳脚的时候了。也就是说适配器模式通常是在软件开发后期或维护期考虑使用的,如果是在设计阶段,完全没必要把类似的功能类的接口设计得不同。因此在一个项目前期最好就设计好公司内部类和方法命名的规范,然后如果真的出现了接口不相同的问题时,首先不应该考虑用适配器模式,而是应该考虑通过重构统一接口。
当然也有设计之初就使用适配器模式的时候,就是公司设计系统时考虑使用第三方开发组件,而这个组件的接口与我们自己的系统接口是不相同的,而我们也完全没必要为了迎合它而改动自己的接口,此时尽管是在开发的设计阶段,也是可以考虑用适配器模式来解决不同的问题。
5.适配器模式的应用
(1) Sun公司在1996年公开了Java语言的数据库连接工具JDBC,JDBC使得Java语言程序能够与数据库连接,并使用SQL语言来查询和操作数据。JDBC给出一个客户端通用的抽象接口,每一个具体数据库引擎(如SQL Server、Oracle、MySQL等)的JDBC驱动软件都是一个介于JDBC接口和数据库引擎接口之间的适配器软件。抽象的JDBC接口和各个数据库引擎API之间都需要相应的适配器软件,这就是为各个不同数据库引擎准备的驱动程序。
(2)在Spring AOP框架中,对BeforeAdvice、AfterAdvice、ThrowsAdvice三种通知类型借助适配器模式来实现。
(3)在JDK类库中也定义了一系列适配器类,如在com.sun.imageio.plugins.common包中定义的InputStreamAdapter类,用于包装ImageInputStream接口及其子类对象。
6.扁鹊的医术
大话设计模式一个很好的故事。
当年,魏文王问名医扁鹊说:“你们家兄弟三人,都精于医术,到底哪一位最好呢?”扁鹊答:“长兄最好,中兄次之,我最差。”文王再问:“那为什么你最出名呢?”扁鹊答:“长兄治病,是治病于病情发作之前。一般人不知道他事先能铲除病因,所以他的名气无法传出去;中兄治病,是治病于病情起初时。一般人以为他只能治轻微的小病,所以他的名气只及本乡里。而我是治病于病情严重之时。一般人都看到我在筋脉上穿针管放血、在皮肤上敷药等大手术,所以大家都以为我的医术高明,名气因此响遍全国。”这个故事说明什么?
“啊,你的意思是如果能事先预防接口不同的问题,不匹配问题就不会发生;在有小的接口不统一问题发生时,及时重构,问题不至于扩大;只有碰到无法改变原有设计和代码的情况时,才考虑适配。事后控制不如事中控制,事中控制不如事前控制。 ”小菜总结说。
“对呀,如果能事前控制,又何必要事后再去弥补呢?”大鸟肯定说:“适配器模式当然是好模式,但是如果无视它的应用场景而盲目使用,其实是本末倒置了。”