设计模式之适配器模式

2022-08-04 15:43:42 浏览数 (2)

本文通过老王使用纸质书籍阅读小王使用电子书籍的故事,详细说明设计模式中的结构型设计模式之适配器模式,分别对对象适配器和类适配器代码实现,最后为了加深理解,会列举适配器设计模式在JDK和Spring源码中的应用。

读者可以拉取完整代码到本地进行学习,实现代码均测试通过后上传到码云。

一、引出问题

自从小王被老王赶出家门以后,老王过了几天舒心的日子,在家里的书架上买了许许多多的纸质书。

有一天,小王过够了野人生活回来了,小王也是一个喜欢读书的人,但是小王不喜欢纸质书,就要求老王将这些书换成电子版。

老王立马就不开心了,这是我不知道花费多少个日夜才设计好的书架,给你换成电子版的不仅要花费我大量的精力改变原有书架的结构,再想找我想看的书得有多难,而且老李来了想看纸质版怎么办,我还要再换回去吗?

小王随即想到了一种解决思路:这些书现在符合你的风格,应该设计一种模式,让这些书也能符合我的需求,让我们俩可以在一起读书,既不改变你的书架结构,又能扩展它的功能。

老王满意的点了点头,你说的不错,这实际上就是结构型设计模式中的适配器模式。

二、概念与使用

引用Gof中对适配器设计模式的概念:将一个类的接口转化成客户希望的另一个接口,由于接口不兼容而不能一起工作的类可以一起工作。

很显然,在适配器设计模式中应该有三个角色。

目标类:Target,该角色把其他类转换为我们期望的接口,可以是一个抽象类或接口,也可以是具体类。

代码语言:txt复制
	被适配者类(源): Adaptee ,原有的接口,也是希望被适配的接口。
代码语言:txt复制
	适配器: Adapter, 将被适配者和目标抽象类组合到一起的类。

在我们的实际案例中,老王的纸质书很明显应该是属于被适配者,小王的电子版就是目标类,适配器应该是能调用老王的纸质书,并使用一些相关的业务方法转化成电子版,比如调用老王书之前买一个扫描仪,在老王书调出来以后扫描书籍。

既然适配器中要调用老王的纸质书,调用它的方法应该是有两种实现方式。

一是直接继承老王,那样就可以直接调用老王的方法了。

二是在适配器中创建老王的对象,然后再调用老王的方法。

这其实对应了适配器的两种方式,根据适配器类与适配者类的关系不同,适配器模式可分为对象适配器和类适配器两种,在对象适配器模式中,适配器与适配者之间是关联关系;在类适配器模式中,适配器与适配者之间是继承(或实现)关系。

我们先看类适配器实现方式:

被适配者类:

代码语言:txt复制
/**
 * 源对象
 * @author tcy
 * @Date 04-08-2022
 */
public class AdapteePaperReading {

    public void readPaper(){
        System.out.println("这是老王读的纸质书...(被适配者方法)");
    }
}

目标对象:

代码语言:txt复制
/**
 * 目标对象
 */
public interface TargetOnlineReading {
    public void ReadOnline();
}

适配器:

代码语言:txt复制
/**
 * @author tcy
 * @Date 04-08-2022
 */
public class Adapter extends AdapteePaperReading implements TargetOnlineReading{
    @Override
    public void ReadOnline() {

        System.out.println("买一个扫描仪...");
        readPaper();
        System.out.println("拿到纸质书扫描为电子书...");
    }


}

客户端:

代码语言:txt复制
/**
 * @author tcy
 * @Date 04-08-2022
 */
public class Client {

    public static void main(String[] args) {
        Adapter adapter=new Adapter();
        adapter.ReadOnline();

    }
}

以上就实现类适配器,如果我们要实现对象适配器也很简单,目标对象和被适配者都不变,需要改变的是适配器代码

代码语言:txt复制
/**
 * @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接口

代码语言:txt复制
// 这是FutureTask的构造方法一
代码语言:txt复制
public FutureTask(Callable<V> callable) {
代码语言:txt复制
    if (callable == null)
代码语言:txt复制
        throw new NullPointerException();
代码语言:txt复制
    this.callable = callable;
代码语言:txt复制
    this.state = NEW;     
代码语言:txt复制
}

构造方法二:传入的参数为Runnable接口

代码语言:txt复制
// 这是FutureTask的构造方法二
代码语言:txt复制
public FutureTask(Runnable runnable, V result) {
代码语言:txt复制
	// 调用Executors类中的callable方法进行转化
代码语言:txt复制
    this.callable = Executors.callable(runnable, result);
代码语言:txt复制
    this.state = NEW;   
代码语言:txt复制
}

在构造方法中实际上加传入的Runnable任务在内部统一被转换为Callable任务。

可以看到这里采用的是适配器模式,调用RunnableAdapter<T>(task, result)方法来适配,实现如下:

代码语言:txt复制
static final class RunnableAdapter<T> implements Callable<T> {
    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(动态)代理模式。

基于接口(同时含类继承)实现的适配器模式称为类适配器模式,(只)基于继承(使用委托)实现的适配器模式称为类适配器模式。

代理模式是为其他类提供一种代理以控制对这个类的访问。我们不直接去接触目标类,而是直接操作代理类,代理类再去操作目标类。因为不直接接触目标类,因此我们可以在代理类的同名方法中添加或删除功能模块,而不用去修改目标类的原方法。

而适配器模式则主要是协调现实与需求的差异,减少对已有代码的改动,适配不同的接口、类类型。

项目实施中可能会出现这样的情况:当前已完成的项目的某一个包内的各个类实现了一些特定的接口,而客户提出了新的需求,要求实现他所指定的那些接口(抛弃原有的方法或接口),但其业务细节却是相同、完全一样的。此时,我们可能并不想复制粘贴原代码到新的方法中去,这就需要将一个类的接口转换成新需求的另一个接口。

实现方式有很多,没有必要咬文嚼字纠结使用哪种设计模式,设计模式本身就是很相似,只要能简洁开发流程,让我们的代码更好的工作就是完美的。具体使用哪一种就需要读者熟练掌握各种设计模式了,并认真体会他们各自的优势。

推荐读者,参考软件设计七大原则 认真阅读往期的文章,认真体会。

创建型设计模式

一、设计模式之工厂方法和抽象工厂

二、设计模式之单例和原型

三、设计模式之建造者模式

结构型设计模式

四、设计模式之代理模式

0 人点赞