4

设计模式之适配器模式_程序员田同学的技术博客_51CTO博客

 1 year ago
source link: https://blog.51cto.com/u_15476035/5544004
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文通过老王使用纸质书籍阅读小王使用电子书籍的故事,详细说明设计模式中的结构型设计模式之适配器模式,分别对对象适配器和类适配器代码实现,最后为了加深理解,会列举适配器设计模式在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<V> 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<T>(task, result)方法来适配,实现如下:

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(动态)代理模式。

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

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

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

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

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

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

创建型设计模式

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

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

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

结构型设计模式

 四、设计模式之代理模式


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK