本文通过老王使用纸质书籍阅读小王使用电子书籍的故事,详细说明设计模式中的结构型设计模式之适配器模式,分别对对象适配器和类适配器代码实现,最后为了加深理解,会列举适配器设计模式在JDK和Spring源码中的应用。
读者可以拉取完整代码到本地进行学习,实现代码均测试通过后上传到码云。
一、引出问题
自从小王被老王赶出家门以后,老王过了几天舒心的日子,在家里的书架上买了许许多多的纸质书。
有一天,小王过够了野人生活回来了,小王也是一个喜欢读书的人,但是小王不喜欢纸质书,就要求老王将这些书换成电子版。
老王立马就不开心了,这是我不知道花费多少个日夜才设计好的书架,给你换成电子版的不仅要花费我大量的精力改变原有书架的结构,再想找我想看的书得有多难,而且老李来了想看纸质版怎么办,我还要再换回去吗?
小王随即想到了一种解决思路:这些书现在符合你的风格,应该设计一种模式,让这些书也能符合我的需求,让我们俩可以在一起读书,既不改变你的书架结构,又能扩展它的功能。
老王满意的点了点头,你说的不错,这实际上就是结构型设计模式中的适配器模式。
二、概念与使用
引用Gof中对适配器设计模式的概念:将一个类的接口转化成客户希望的另一个接口,由于接口不兼容而不能一起工作的类可以一起工作。
很显然,在适配器设计模式中应该有三个角色。
目标类:Target,该角色把其他类转换为我们期望的接口,可以是一个抽象类或接口,也可以是具体类。
被适配者类(源): Adaptee ,原有的接口,也是希望被适配的接口。
适配器: Adapter, 将被适配者和目标抽象类组合到一起的类。
在我们的实际案例中,老王的纸质书很明显应该是属于被适配者,小王的电子版就是目标类,适配器应该是能调用老王的纸质书,并使用一些相关的业务方法转化成电子版,比如调用老王书之前买一个扫描仪,在老王书调出来以后扫描书籍。
既然适配器中要调用老王的纸质书,调用它的方法应该是有两种实现方式。
一是直接继承老王,那样就可以直接调用老王的方法了。
二是在适配器中创建老王的对象,然后再调用老王的方法。
这其实对应了适配器的两种方式,根据适配器类与适配者类的关系不同,适配器模式可分为对象适配器和类适配器两种,在对象适配器模式中,适配器与适配者之间是关联关系;在类适配器模式中,适配器与适配者之间是继承(或实现)关系。
我们先看类适配器实现方式:
被适配者类:
/**
* 源对象
* @author tcy
* @Date 04-08-2022
*/
public class AdapteePaperReading {
public void readPaper(){
System.out.println("这是老王读的纸质书...(被适配者方法)");
}
}
目标对象:
/**
* 目标对象
*/
public interface TargetOnlineReading {
public void ReadOnline();
}
适配器:
/**
* @author tcy
* @Date 04-08-2022
*/
public class Adapter extends AdapteePaperReading implements TargetOnlineReading{
@Override
public void ReadOnline() {
System.out.println("买一个扫描仪...");
readPaper();
System.out.println("拿到纸质书扫描为电子书...");
}
}
客户端:
/**
* @author tcy
* @Date 04-08-2022
*/
public class Client {
public static void main(String[] args) {
Adapter adapter=new Adapter();
adapter.ReadOnline();
}
}
以上就实现类适配器,如果我们要实现对象适配器也很简单,目标对象和被适配者都不变,需要改变的是适配器代码
/**
* @author tcy
* @Date 04-08-2022
*/
public class Adapter implements TargetOnlineReading {
// 适配者是对象适配器的一个属性
private AdapteePaperReading adaptee = new AdapteePaperReading();
@Override
public void ReadOnline() {
System.out.println("买一个扫描仪...");
adaptee.readPaper();
System.out.println("拿到纸质书扫描为电子书...");
}
}
这样老王和小王就能在一起读书了。但这种方式只能作为系统的一种补救措施,而不是在系统设计之初就考虑这种方式,如果老王有十个八个儿子都要求按照他们的习惯来,那系统就会相当的复杂,无异于一场灾难。而是应该考虑重做书架,将各种情况都考虑进去。
需要说明的是,类适配器之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。
三、应用
案例有一些生硬,为了加深对适配器设计模式的把握,我们介绍该模式在Jdk源码和Spring中的应用。
1、JDK应用
JDK使用适配器的典型例子是Java线程池FutureTask类。我们知道通过实现接口实现多线程一共有两种方式,Runnable接口和Callable接口。
FutrueTask类中有两个构造方法:
构造方法一:传入参数为Callable接口
// 这是FutureTask的构造方法一
public FutureTask(Callable callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW;
}
构造方法二:传入的参数为Runnable接口
// 这是FutureTask的构造方法二
public FutureTask(Runnable runnable, V result) {
// 调用Executors类中的callable方法进行转化
this.callable = Executors.callable(runnable, result);
this.state = NEW;
}
在构造方法中实际上加传入的Runnable任务在内部统一被转换为Callable任务。
可以看到这里采用的是适配器模式,调用RunnableAdapter
方法来适配,实现如下:
static final class RunnableAdapter implements Callable {
final Runnable task;
final T result;
RunnableAdapter(Runnable task, T result) {
this.task = task;
this.result = result;
}
public T call() {
task.run();
return result;
}
}
这样无论是传入Runnalbe还是Callable都能适配任务,这个适配器很简单,就是简单的实现了Callable接口,在call()实现中调用Runnable.run()方法,然后把传入的result作为任务的结果返回。
通过这么一个简单案例可以加深对适配器模式的理解。
2、SpringAOP应用
我们知道在Spring的Aop中,使用的 Advice(通知) 来增强被代理类的功能。
其中Advice的类型有:BeforeAdvice(在执行切点前的通知)、AfterReturningAdvice(在运行完切点完未返回之前)、ThrowsAdvice(在运行完切点时抛出异常进行的通知),AfterAdvice(执行完该切点后,进行的通知)、Around advice(包裹一个方法的执行)
在每个类型 Advice 都有对应的拦截器,MethodBeforeAdviceInterceptor、AfterReturningAdviceInterceptor、 ThrowsAdviceInterceptor
Spring需要将每个 Advice 都封装成对应的拦截器类型,返回给容器,这时候采用的就是适配器类型。
Advice 就相当于适配者,对应的拦截器类型就是目标类。
限于篇幅,有兴趣的读者可以到Spring源码中了解具体过程。
3、SpringMVC应用
Spring MVC中的适配器模式主要用于执行目标 Controller 中的请求处理方法。
在Spring MVC中,DispatcherServlet 作为用户,HandlerAdapter 作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller 作为需要适配的类。
当Spring容器启动后,会将所有定义好的适配器对象存放在一个List集合中,当一个请求来临时,DispatcherServlet 会通过 handler 的类型找到对应适配器,并将该适配器对象返回给用户,然后就可以统一通过适配器的 hanle() 方法来调用 Controller 中的用于处理请求的方法。
通过适配器模式我们将所有的 controller 统一交给 HandlerAdapter 处理,免去了写大量的 if-else 语句对 Controller 进行判断,也更利于扩展新的 Controller 类型。
单纯的说苍白无力,我们手写实现SpringMVC的核心流程,完整代码已经上传到码云。
四、总结
既然适配器模式可以扩展原有类的功能,那它和代理模式在一定程度上不是重合了吗?貌似扩展老王的书架使用代理模式同样是可以实现。
其实我们看结构型设计模式的定义:结构型模式涉及到如何组合类和类以获得更大的结构,结构型类模式采用继承机制来组合接口或实现。
代理模式与适配器模式都分别有继承、接口方式实现的子分类模式。基于接口实现的代理模式称为静态代理模式、JDK(动态)代理模式,基于继承实现的代理模式称为Cglib(动态)代理模式。
基于接口(同时含类继承)实现的适配器模式称为类适配器模式,(只)基于继承(使用委托)实现的适配器模式称为类适配器模式。
代理模式是为其他类提供一种代理以控制对这个类的访问。我们不直接去接触目标类,而是直接操作代理类,代理类再去操作目标类。因为不直接接触目标类,因此我们可以在代理类的同名方法中添加或删除功能模块,而不用去修改目标类的原方法。
而适配器模式则主要是协调现实与需求的差异,减少对已有代码的改动,适配不同的接口、类类型。
项目实施中可能会出现这样的情况:当前已完成的项目的某一个包内的各个类实现了一些特定的接口,而客户提出了新的需求,要求实现他所指定的那些接口(抛弃原有的方法或接口),但其业务细节却是相同、完全一样的。此时,我们可能并不想复制粘贴原代码到新的方法中去,这就需要将一个类的接口转换成新需求的另一个接口。
实现方式有很多,没有必要咬文嚼字纠结使用哪种设计模式,设计模式本身就是很相似,只要能简洁开发流程,让我们的代码更好的工作就是完美的。具体使用哪一种就需要读者熟练掌握各种设计模式了,并认真体会他们各自的优势。
推荐读者,参考软件设计七大原则 认真阅读往期的文章,认真体会。
创建型设计模式:
结构型设计模式: