《程序猿之设计模式实战 · 适配器模式》-LMLPHP

写在前面的话

本篇文章继续介绍一下适配器模式,单词为Adapter。日常生活中适配器的场景也随处可见,例如USB、插座等转换头,或电压转换处理,总之,起中转适配作用的,都可以考虑用适配器模式。

工作中的场景就更不用说了,新老服务之间中转的桥梁、服务、工具,都可以称之为Adapter,那这个适配器模式到底是什么样的,且听我娓娓道来。

相关文章:
《程序猿之设计模式实战 · 策略模式》
《程序猿之设计模式实战 · 装饰者模式》
《程序猿之设计模式实战 · 池化思想》
《程序猿之设计模式实战 · 观察者模式》
《程序猿之设计模式实战 · 责任链模式》
《程序猿之设计模式实战 · 模板方法》


基础介绍

基础概念:

适配器模式(Adapter Pattern)是一种结构型设计模式,它允许将一个接口转换成客户端所期望的另一个接口。

适配器模式使得原本由于接口不兼容而无法一起工作的类可以一起工作。

主要组成:

适配器模式通常由以下几个角色组成:

  1. 目标接口(Target):客户端所期望的接口。
  2. 源类(Adaptee):需要被适配的类,通常是一个已有的类。
  3. 适配器(Adapter):实现目标接口,并持有源类的实例,负责将源类的接口转换为目标接口。

使用场景:

  • 当你希望使用一些现有的类,但它们的接口与您所需要的接口不兼容时。
  • 当你希望创建一个可重用的类,它可以与一些不相关的类(即接口不兼容的类)一起工作时。
  • 当你希望通过多个类的组合来实现某个功能,而这些类的接口不一致时。

总结一下:

适配器模式通过将不兼容的接口进行适配,使得不同的类可以协同工作。在 Spring 框架中,适配器模式的应用使得不同类型的处理器和视图能够以统一的方式进行处理,从而提高了系统的灵活性和可扩展性。


代码实现

挺简单的一段示例,也是对原有类的增强,使之可以间接成为目标接口。

// 目标接口
interface Target {
    void request();
}

// 源类
class Adaptee {
    public void specificRequest() {
        System.out.println("Called specificRequest()");
    }
}

// 适配器
class Adapter implements Target {
    private Adaptee adaptee;

    public Adapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }

    @Override
    public void request() {
        // 调用源类的方法
        adaptee.specificRequest();
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        Adaptee adaptee = new Adaptee();
        Target target = new Adapter(adaptee);
        target.request(); // 输出: Called specificRequest()
    }
}

Spring 中的适配器模式

在 Spring 框架中,适配器模式被广泛应用于多个地方,尤其是在 MVC 模块中。以下是一些主要的应用场景:

1、HandlerMapping:Spring MVC 中的 HandlerMapping 使用适配器模式来将请求映射到处理器(Controller)。不同类型的处理器(如注解驱动的控制器、传统的控制器等)可以通过适配器进行统一处理。

2、HandlerAdapter:Spring MVC 中的 HandlerAdapter 是适配器的具体实现,它允许不同类型的处理器(如 SimpleControllerHandlerAdapter、AnnotationMethodHandlerAdapter 等)以统一的方式处理请求。

3、ViewResolver:在视图解析过程中,Spring 使用适配器模式来支持不同类型的视图(如 JSP、Thymeleaf 等)。

以HandlerMapping为例,是如何利用到适配器模式?

在 Spring 框架中,HandlerMapping 是一个重要的组件,它负责将 HTTP 请求映射到相应的处理器(Handler)。为了实现这一点,Spring 使用了适配器模式来处理不同类型的请求处理器(如控制器)。

适配器模式在 HandlerMapping 中的应用,步骤如下:

1、接口与实现

在 Spring 中,HandlerMapping 会将请求映射到一个处理器(Handler),而这个处理器可能是不同类型的对象,比如:

  • 控制器类(例如,使用 @Controller 注解的类)
  • 函数式处理器(例如,使用 @RequestMapping 注解的方法)

为了能够处理这些不同类型的处理器,Spring 使用了适配器模式。

2、适配器接口

Spring 定义了一个适配器接口,例如 HandlerAdapter,它为不同类型的处理器提供了统一的调用方式。这个接口定义了一个方法,比如 handle(),用于处理请求。

public interface HandlerAdapter {
    boolean supports(Object handler);
    ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
}

3、具体适配器

Spring 提供了多个具体的适配器实现,例如:

RequestMappingHandlerAdapter:用于处理使用 @RequestMapping 注解的控制器方法。

SimpleControllerHandlerAdapter:用于处理实现了 Controller 接口的传统控制器。

每个适配器实现了 HandlerAdapter 接口,并提供了对特定类型处理器的支持。

public class RequestMappingHandlerAdapter implements HandlerAdapter {
    @Override
    public boolean supports(Object handler) {
        return (handler instanceof RequestMappingHandler);
    }

    @Override
    public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 处理请求
    }
}

4、HandlerMapping 的工作流程

请求到达:当一个 HTTP 请求到达时,HandlerMapping 会根据请求的 URL 找到对应的处理器。

适配器选择:HandlerMapping 会遍历所有注册的 HandlerAdapter,找到一个支持找到的处理器的适配器。

请求处理:一旦找到合适的适配器,HandlerMapping 会调用适配器的 handle() 方法,传递请求和响应对象,以及处理器。

返回结果:适配器处理请求并返回结果(如 ModelAndView),最终将响应返回给客户端。

总结一下,通过适配器模式,Spring 能够灵活地支持多种类型的请求处理器,而不需要在 HandlerMapping 中硬编码每种处理器的处理逻辑。这种设计使得框架具有良好的扩展性和可维护性,允许开发者轻松添加新的处理器类型和适配器。


适配器VS装饰者

观察了示例代码,可以发现,和装饰者模式有点像,都是扩展增强了原有对象,那两者有什么区别呢?

适配器模式和装饰者模式虽然在结构上有相似之处,但它们的目的和使用场景是不同的,下面是它们的主要区别:

1、先看适配器模式

1)目的:接口转换,适配器模式的主要目的是将一个类的接口转换为客户端所期望的另一个接口。它使得原本由于接口不兼容而无法一起工作的类可以一起工作。

2)使用场景:当你希望使用一些现有的类,但它们的接口与您所需要的接口不兼容时。例如,将一个旧的 API 适配到新的接口,以便可以在新的系统中使用。

3)结构:适配器通常包含一个源类的实例,并实现目标接口。适配器负责将目标接口的方法调用转发到源类的方法。

2、再看装饰者模式

1、目的:功能扩展,装饰者模式的主要目的是在不改变对象自身的情况下,动态地为对象添加额外的功能。它允许在运行时对对象进行扩展。

2、使用场景:当你希望在不修改现有类的情况下,给对象添加新的行为或功能时。例如,为一个图形对象添加边框、阴影等装饰效果。

3、结构装饰者通常包含一个被装饰对象的实例,并实现与被装饰对象相同的接口。装饰者可以在调用被装饰对象的方法之前或之后添加额外的行为。

3、总结一下

适配器模式:关注于接口的转换,使得不兼容的接口可以协同工作。

装饰者模式:关注于功能的扩展,通过组合的方式为对象添加新的行为。

【代理、桥接、装饰器、适配器的区别】

  • 代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。
  • 桥接模式:桥接模式的目的是将接口部分和实现部分分离,而让它们可以较为容易、独立地加以改变。
  • 装饰器模式:装饰器模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。
  • 适配器模式:适配器模式是一种时候的补救策略,适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原来类相同的接口。

适配器实操补充

经典案例一


// 类适配器: 基于继承
// 自己扩展的,客户端期望得到的接口,里面还可以包含被适配者没有的接口
public interface ITarget {
  void f1();
  void f2();
  void fc();
}

//被适配者(外部系统)
//理解为外国的电源插座,这些方法不会暴露给客户端直接调用,因此不支持或者说不好用
public class Adaptee {
  public void fa() { //... }
  public void fb() { //... }
  public void fc() { //... }
}

//适配器类,用于替换被适配者完成功能
//理解为转接头,负责两个事情,提供方法给客户端调用,方法内部会调用目标(国外电源插座)的方法
public class Adaptor extends Adaptee implements ITarget {
  public void f1() {
    super.fa();
  }
  
  public void f2() {
    //...重新实现f2()...
  }
  
  // 这里fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点
}

// 对象适配器:基于组合
public interface ITarget {
  void f1();
  void f2();
  void fc();
}

public class Adaptee {
  public void fa() { //... }
  public void fb() { //... }
  public void fc() { //... }
}

public class Adaptor implements ITarget {
  private Adaptee adaptee;
  
  public Adaptor(Adaptee adaptee) {
    this.adaptee = adaptee;
  }
  
  public void f1() {
    adaptee.fa(); //委托给Adaptee
  }
  
  public void f2() {
    //...重新实现f2()...
  }
  
  public void fc() {
    adaptee.fc();
  }
}

经典案例二(类适配器模式)

《程序猿之设计模式实战 · 适配器模式》-LMLPHP

在上图中可以看出,Adaptee类(被适配者、外国插座)并没有sampleOperation2()方法,而客户端则期待这个方法。为使客户端能够使用 Adaptee 类,提供一个中间环节,即类 Adapter(适配器、转换头),把 Adaptee 的 API与 Target 类的API衔接起来。Adapter 与 Adaptee 是继承关系,这决定了这个适配器模式是类的:

模式所涉及的角色有:

1、目标(Target)角色:这就是所期待得到的接口。

2、源(Adapee)角色:现在需要适配的接口。

3、适配器(Adaper)角色:适配器类是本模式的核心。适配器把源接口转换成目标接口。显然,这一角色不可以是接口,而必须是具体类。

public interface Target {
    /**
     * 这是源类Adaptee也有的方法
     */
    public void sampleOperation1(); 
    /**
     * 这是源类Adapteee没有的方法
     */
    public void sampleOperation2(); 
}

//上面给出的是目标角色的源代码,这个角色是以一个JAVA接口的形式实现的。
//可以看出,这个接口声明了两个方法:sampleOperation1()和sampleOperation2()。
//而源角色Adaptee是一个具体类,它有一个sampleOperation1()方法,但是没有sampleOperation2()方法。
public class Adaptee {
    public void sampleOperation1(){}
}

//适配器角色Adapter扩展了Adaptee,同时又实现了目标(Target)接口。由于Adaptee没有提供sampleOperation2()方法,而目标接口又要求这个方法
//因此适配器角色Adapter实现了这个方法。
public class Adapter extends Adaptee implements Target {
    /**
     * 由于源类Adaptee没有方法sampleOperation2()
     * 因此适配器补充上这个方法
     */
    @Override
    public void sampleOperation2() {
        //写相关的代码
    }

}

经典案例三(对象适配器模式)

与类的适配器模式一样,对象的适配器模式把被适配的类的API转换成为目标类的API,与类的适配器模式不同的是,对象的适配器模式不是使用继承关系连接到Adaptee类,而是使用委派关系连接到Adaptee类。

《程序猿之设计模式实战 · 适配器模式》-LMLPHP

从上图可以看出,Adaptee类并没有sampleOperation2()方法,而客户端则期待这个方法。为使客户端能够使用Adaptee类,需要提供一个包装(Wrapper)类Adapter。这个包装类包装了一个Adaptee的实例,从而此包装类能够把Adaptee的API与Target类的API衔接起来。Adapter与Adaptee是委派关系,这决定了适配器模式是对象的。

public interface Target {
    /**
     * 这是源类Adaptee也有的方法
     */
    public void sampleOperation1(); 
    /**
     * 这是源类Adapteee没有的方法
     */
    public void sampleOperation2(); 
}

public class Adaptee {
    public void sampleOperation1(){}   
}

public class Adapter {
    private Adaptee adaptee;
    
    public Adapter(Adaptee adaptee){
        this.adaptee = adaptee;
    }
    /**
     * 源类Adaptee有方法sampleOperation1
     * 因此适配器类直接委派即可
     */
    public void sampleOperation1(){
        this.adaptee.sampleOperation1();
    }
    /**
     * 源类Adaptee没有方法sampleOperation2
     * 因此由适配器类需要补充此方法
     */
    public void sampleOperation2(){
        //写相关的代码
    }
}

总结陈词

怎么说呢,适配器模式还是很强大的,它为我们带来:

1、更好的复用性:系统需要使用现有的类,而此类的接口不符合系统的需要。那么通过适配器模式就可以让这些功能得到更好的复用。

2、更好的扩展性:在实现适配器功能的时候,可以调用自己开发的功能,从而自然地扩展系统的功能。

但它也有一些缺点:过多的使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是A接口,其实内部被适配成了B接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。

最终是否采用,还是根据实际情况决定,纸上得来终觉浅,绝知此事要躬行

💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。

《程序猿之设计模式实战 · 适配器模式》-LMLPHP

09-25 00:30