适配器模式
适配器模式(Adapter):将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
The Adapter Pattern converts the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.
适配器模式不再是同一个家族的相互组合 的结构了(代理者与具体主题是一个家族、装饰者与具体组件是一个家族,叶子节点与非叶子节点是一个家族),而是想将别人(被适配者)纳入自己家族(目标)。
还是看一下 UML 类图吧。
适配器模式角色如下:
⨳ Target(目标) : 定义客户端使用的特定领域的接口。
⨳ Adaptee(被适配者) : 定义了既有功能的接口,但是该接口与客户端所期望的接口不兼容。适配器通过将客户端的请求转发给被适配者来实现适配。
⨳ Adapter(适配器) : 通过实现目标接口(Target
)并维护对适配者对象(Adaptee
)的引用,使得适配器可以将客户端的请求转发给适配者。
简单来说,适配器模型就是使用适配器 (Adapter
) 将 被适配者 (Adapee
) 转换成客户端可以使用的 目标 (Target
)。
适配器模式有两种实现方式:类适配器 (使用继承关系来实现)和对象适配器 (使用组合关系来实现),上述为 UML
类图为对象适配器,也是推荐使用实现适配器的方式,因为组合结构相对于继承更加灵活。
目标 一般都是接口,接口就是规范,实现同一规范的类属于一个家族,用这个思路去想适配器模式,那适配器(Adapter
)就是为了能将被适配者(Adapee
)纳入目标(Target
)家族的"伪装"。
适配器和被适配者之间的关系,可以看作是手套与脚之间的关系(装饰者模式是手套与手的关系),我现在只想用手,不想用脚,这时就可以使用手套(适配器)将脚(被适配者)固定成手的形状(目标接口)。
举个例子,手机充电(客户端)只能使用 5V
电压(目标),但是插座的电压只有 220V
(被适配者),这时就需要 手机充电器(适配器)将 220V
的电压 转换成 5V
电压。
闲话少说,还是看代码吧。
基本实现
目标类 Target
目标类是客户所期待的接口,可以是具体的或抽象的类,也可以是接口。
java
public interface Target {
// 目标类处理请求的方式
void request();
}
被适配者 Adaptee
Adaptee
就像需要适配的类,和客户想要的 Target
没有一毛钱关系。
java
public class Adaptee {
void specificRequest(){
System.out.println("被适配者处理请求的方式");
}
}
适配器 Adapter
Adapter
通过在内部包装一个 Adaptee
对象,把源接口转换成目标接口 Target
。
java
public class Adapter implements Target{
private Adaptee adaptee = new Adaptee();
@Override
public void request() {
adaptee.specificRequest();
}
}
客户端 Client
java
Target target = new Adapter();
target.request();
输出如下:
js
被适配者处理请求的方式
对于客户端来说,并不需要知道被适配者 Adaptee
,通过调用 Target
的 request
方法,就实现了对 Adaptee
的使用。
源码鉴赏
JDK 之 Enumeration
JDK1.0 中包含一个遍历集合容器的类 Enumeration
。JDK2.0 对这个类进行了重构,将它改名为 Iterator
类,并且对它的代码实现做了优化。
但是考虑到如果将 Enumeration
直接从 JDK2.0 中删除,那使用 JDK1.0 的项目如果切换到 JDK2.0,代码就会编译不通过。为了避免这种情况的发生,我们必须把项目中所有使用到 Enumeration
的地方,都修改为使用 Iterator
才行。
为了做到兼容使用低版本 JDK 的老代码,我们可以暂时保留 Enumeration
类,并将其实现替换为直接调用 Itertor
。
java
public class Collections {
public static Emueration emumeration(final Collection c) {
return new Enumeration() {
Iterator i = c.iterator();
public boolean hasMoreElments() {
return i.hashNext();
}
public Object nextElement() {
return i.next():
}
}
}
}
这是 Emueration 适配 Iterator,老接口适配新接口,这种做法确保了新旧版本的兼容性,同时使得代码迁移过程更加平滑。
Mybatis 之 Log
Java 中有很多日志框架,比较常用的有 log4j
、logback
,以及 JDK 提供的JUL
(java.util.logging) 等。
Mybatis 为了适配这些日志,也用到了适配器模式:
很容易就看出了各种 XXXLoggerImpl
就是对各种 Logger
的适配器,用于将各种 Logger
拉入到 Mybatis
自己的 Log
体系中。
Mybatis
的 Log
适配的 Slf4j
日志框架其实也具备适配器模式的特征。它将不同的日志框架(如 Log4j、Logback 等)的实现适配到了统一的接口上,使得这些日志框架能够兼容 Slf4j 的接口定义,从而实现了日志记录的统一。
虽然Slf4j
的全称为 Simple Logging Facade for Java, 简单的日志门面,门面、门面、其实和适配器模式差不多,都是将别的系统的类拉入到自己系统,改头换面。
当适配器适配多个相同功能的不同实现时,优势就体现出来了,相同功能可以用同一个接口限制,不同实现可以用适配器适配这同一个接口,而且就算如 Log 一样每次只能用一个,也可以随意切换,无需修改使用代码。
Spring 之 HandlerAdapter
Spring 的 HandlerAdapter
也是适配器模式,它可以根据请求类型的不同,选择合适的处理器来处理请求。
如图:
HttpRequestHandlerAdapter
是对 HttpRequestHandler
的适配,主要用于处理 HTTP 请求;
SimpleControllerHandlerAdapter
是对 Controller
的适配,用于处理 Web 应用程序中的请求,包括但不限于 HTTP 请求;
SimpleServletHandlerAdapter
是对 Servlet
的适配,Servlet
是 Java Web 开发中的一种标准,更加底层,能够处理任何类型的请求。
...
这几种 Handler 处理请求的方法都不一样,通过 Adapter 就可以统一了。
总结
适配器模式最主要的功能就是接口转换。
当系统需要使用某个已存在的类,但其接口与系统要求的接口不匹配时,可以使用适配器模式进行接口转换,使得该类可以被系统正常使用。比如说在将一个已存在的类或第三方库集成到系统中时,可能存在接口不兼容的情况,此时可以使用适配器模式来使两者能够协同工作。
总的来说,适配器模式适用于需要解决接口不兼容或需要将不同接口统一的场景,帮助系统中不同部分之间实现无缝的协作和集成。
适配器模式优点如下:
⨳ 复用现有功能:通过适配器模式,可以重用现有的类,而不需要修改其原始代码,提高了代码的复用性。
⨳ 解耦性增强:适配器模式可以将目标类与适配器类解耦,使得二者可以独立变化,降低了类之间的耦合度。
⨳ 符合开闭原则:适配器模式可以在不修改现有代码的情况下添加新的适配器类,符合开闭原则,即对扩展开放,对修改关闭。
缺点如下:
⨳ 增加复杂性:引入适配器模式会增加额外的类和层次结构,从而增加了系统的复杂性,使得代码难以理解和维护。如果适配器模式使用不当,可能会造成系统中存在过多的适配器类,使得代码变得混乱,不利于维护和理解。
一般来说,适配器模式可以看作一种"补偿模式",用来补救设计上的缺陷。应用这种模式算是"无奈之举"。如果在设计初期,我们就能协调规避接口不兼容的问题,那这种模式就没有应用的机会了。