跳至主要內容

适配器模式

AruNi_Lu设计模式设计模式与范式约 3850 字大约 13 分钟

本文内容

前言

适配器模式也是一种常用的结构型模式,它主要有两种实现方式,类适配器和对象适配器。下面就来介绍下适配器模式的原理和它的应用场景,以及它和前面的装饰器、代理模式又有何区别?

1. 什么是适配器模式

1.1 定义

适配器模式,顾名思义,就是用来做适配的,它能将不兼容的接口转化为可兼容的接口,让原本因接口不兼容而不能一起工作的类可以一起工作。

这个模式有一个非常形象的解释例子,就是 USB 转接头充当适配器,它将两种不兼容的接口,通过转接变得可以一起工作。

1.2 实现

适配器模式有两种实现方式:

  • 类适配器:使用 继承 实现;
  • 对象适配器:使用 组合 实现。

具体实现代码如下所示,其中,ITarget 接口类中是一组要转化成的方法;Adaptee 类中的方法不兼容 ITarget 中方法,所以 Adaptee 为被适配者;Adaptor 充当适配器的角色,将 Adaptee 中的方法转化成一组符合 ITarget 中方法定义的方法。

ps:上面的方法,其实就是接口,写成方法只是为了防止和 interface 接口混淆。

类适配器,基于继承:

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();
  }
}

对于这两种实现方式,我们可以根据两个标准来判断 — Adaptee 接口的个数,Adaptee 和 ITarget 的契合程度:

  • 如果 Adaptee 接口并不多,那么两种实现方式都可以;
  • 如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都相同,那推荐使用 类适配器。因为这样 Adaptor 可以复用父类 Adaptee 的大部分接口;
  • 如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都不相同,那么推荐使用 对象适配器,因为组合结构相比于继承更加灵活。

2. 应用场景

其实,适配器模式是一种 “补偿模式”,用于补救设计上的缺陷。这种模式可以说是一种 “无奈之举”。如果在设计初期,我们就能协调规避接口不兼容的问题,那么就不会有使用适配器模式的机会了

根据适配器模式的定义可知,它主要是应用在 “接口不兼容” 的类之间。所以我们只要搞清楚 什么情况会出现接口不兼容,也就知道了它的应用场景了。

下面就来看看有哪些情况会出现接口不兼容的情况。

2.1 封装有缺陷的接口设计

假设 我们依赖的外部系统在接口设计方面的缺陷,比如包含大量静态方法、不规范的命名、参数过多等等。引入后会影响代码的可读性和可测试性,这时就可以 使用适配器模式,将外部系统的接口进行二次封装,抽象出更好用的接口设计,从而隔离原本有缺陷的设计。

具体示例如下:

public class CD { // 这个类来自外部 sdk,我们无权修改它的代码
  //...
  public static void staticFunction1() { //... }
  
  public void uglyNamingFunction2() { //... }

  public void tooManyParamsFunction3(int paramA, int paramB, ...) { //... }
  
  public void lowPerformanceFunction4() { //... }
}

// 使用适配器模式进行重构
public interface ITarget {
  void function1();
  void function2();
  void fucntion3(ParamsWrapperDefinition paramsWrapper);
  void function4();
  //...
}
// 适配器,将上面 CD 中的接口转化为我们定义的 ITarget 中的接口
public class CDAdaptor extends CD implements ITarget {
  //...
  public void function1() {
     super.staticFunction1();
  }
  
  public void function2() {
    super.uglyNamingFucntion2();
  }
  
  public void function3(ParamsWrapperDefinition paramsWrapper) {
     super.tooManyParamsFunction3(paramsWrapper.getParamA(), ...);
  }
  
  public void function4() {
    //...reimplement it...
  }
}

2.2 统一多个类的接口设计

假设 某个功能的实现依赖多个外部系统(或者说类)。通过适配器模式,将它们的接口适配为统一的接口定义,然后就可以使用多态的特性来复用代码逻辑

例如,我们的系统需要对用户输入的文本进行敏感词过滤,为了提高过滤效果(使过滤更全面),我们引入了多款第三方敏感词过滤系统,依次对文本进行过滤。这些第三方的系统分别如下:

public class ASensitiveWordsFilter { // A敏感词过滤系统提供的接口
  // text是原始文本,函数输出用***替换敏感词之后的文本
  public String filterSexyWords(String text) {
    // ...
  }
  
  public String filterPoliticalWords(String text) {
    // ...
  } 
}

public class BSensitiveWordsFilter  { // B敏感词过滤系统提供的接口
  public String filter(String text) {
    //...
  }
}

public class CSensitiveWordsFilter { // C敏感词过滤系统提供的接口
  public String filter(String text, String mask) {
    //...
  }
}

可以发现,这些第三方系统提供的过滤接口都是不同的,所以我们没办法使用一套逻辑来调用各接口,需要依次调用它们。使用方式如下:

// 未使用适配器模式之前的代码:代码的可测试性、扩展性不好
public class RiskManagement {
  private ASensitiveWordsFilter aFilter = new ASensitiveWordsFilter();
  private BSensitiveWordsFilter bFilter = new BSensitiveWordsFilter();
  private CSensitiveWordsFilter cFilter = new CSensitiveWordsFilter();
  
  // 在进行过滤时,需要依次调用第三方系统的过滤接口
  public String filterSensitiveWords(String text) {
    String maskedText = aFilter.filterSexyWords(text);
    maskedText = aFilter.filterPoliticalWords(maskedText);
    maskedText = bFilter.filter(maskedText);
    maskedText = cFilter.filter(maskedText, "***");
    return maskedText;
  }
}

为了解决上面的痛点,就可以 使用适配器模式,将所有第三方系统的接口适配为统一的接口定义,就像下面这样:

// 使用适配器模式进行改造
public interface ISensitiveWordsFilter { // 统一接口定义
  String filter(String text);
}

public class ASensitiveWordsFilterAdaptor implements ISensitiveWordsFilter {
  private ASensitiveWordsFilter aFilter;
    
  public String filter(String text) {
    String maskedText = aFilter.filterSexyWords(text);
    maskedText = aFilter.filterPoliticalWords(maskedText);
    return maskedText;
  }
}

//...省略BSensitiveWordsFilterAdaptor、CSensitiveWordsFilterAdaptor...

// 扩展性更好,更加符合开闭原则,如果添加一个新的敏感词过滤系统,
// 这个类完全不需要改动;而且基于接口而非实现编程,代码的可测试性更好。
public class RiskManagement { 
  private List<ISensitiveWordsFilter> filters = new ArrayList<>();
 
  public void addSensitiveWordsFilter(ISensitiveWordsFilter filter) {
    filters.add(filter);
  }
  
  public String filterSensitiveWords(String text) {
    String maskedText = text;
    for (ISensitiveWordsFilter filter : filters) {
      maskedText = filter.filter(maskedText);
    }
    return maskedText;
  }
}

这样再使用这些第三方系统的过滤接口时,便可以需要接口什么就添加进来,然后把依次过滤流程交给这个 List 来处理。

2.3 替换依赖的外部系统

当我们需要把项目中依赖的一个外部系统,替换为另一个外部系统时,利用适配器模式,可以减少对代码的改动

具体示例如下:

// 外部系统A
public interface IA {
  //...
  void fa();
}

public class A implements IA {
  //...
  public void fa() { //... }
}
    
// 在我们的项目中,外部系统A的使用示例
public class Demo {
  private IA a;
  public Demo(IA a) {
    this.a = a;
  }
  //...
}
Demo d = new Demo(new A());

// 使用适配器模式,将外部系统A替换成外部系统B
public class BAdaptor implemnts IA {
  private B b;
  public BAdaptor(B b) {
    this.b= b;
  }
    
  public void fa() {
    //...
    b.fb();
  }
}
    
// 借助BAdaptor,Demo的代码中,调用IA接口的地方都无需改动,
// 只需要将BAdaptor如下注入到Demo即可。
Demo d = new Demo(new BAdaptor(new B()));

3. Java 中的适配器模式

在 Java 中也有一些使用适配器模式的具体例子,下面列举两个常用的,分别是 Java IO 和 Java 日志框架。

3.1 Java IO 中的适配器模式

上一章中讲了 Java IO 中的装饰器模式,其实 Java IO 中还使用到了适配器模式。我们知道,Java IO 大体上分为四类,如下所示:

类型字节流字符流
输入流InputStreamReader
输出流OutputStreamWriter

这四大类又派生除了很多针对不同场景的子类,它们的关系图如下:

我们可以发现一个规律,字节流的类都带有 XxxInputStream/XxxOutputStream,字符流的类都带有 XxxReader/XxxWriter。而在上图中,我们可以在 Reader 和 Writer 中找到叫 InputStreamReader 和 OutputStreamWriter 的类,这是个什么类呢?结合了字节流和字符流?我们就拿 InputStreamReader 来举例说明。

通过查看源码可知,其实 InputStreamReader 是用于将字节流转换成字符流的一个桥梁,它内部使用 StreamDecoder 将读取到的字节数据使用指定的字符集将它们解码为字符数据

具体的源码如下:

public class InputStreamReader extends Reader {

    private final StreamDecoder sd;

    public InputStreamReader(InputStream in) {
        super(in);
        sd = StreamDecoder.forInputStreamReader(in, this,
                Charset.defaultCharset()); // ## check lock object
    }

    // ... 其他构造方法

    public int read(CharBuffer target) throws IOException {
        return sd.read(target);
    }
    
    public int read() throws IOException {
        return sd.read();
    }

    public int read(char[] cbuf, int off, int len) throws IOException {
        return sd.read(cbuf, off, len);
    }

    public boolean ready() throws IOException {
        return sd.ready();
    }

    public void close() throws IOException {
        sd.close();
    }
}

OutputSteamWriter 与之类似,使用 StreamEncoder 将读取到的字节数据使用指定的字符集将它们编码为字符数据

所以 InputStreamReader 和 OutputStreamWriter 其实就是两个适配器(Adapter),它们提供了一个桥梁(StreamDecoder 和 StreamEncoder),让字节流可以转化为字符流,而 InputStream 和 OutputStream 的子类就是被适配者

所以我们在使用时,可以利用适配器模式,将字节流的读写适配成字符流的读写;同时,字符也使用了装饰器模式,可以对功能进行增强,就像下面这样:

// 适配器模式:将字节流转为字符流读取,InputStreamReader 为适配器,InputStream 的子类(FileInputStream)为被适配者
InputStreamReader isr = new InputStreamReader(new FileInputStream("/text.txt"));
// 装饰器模式:添加缓存,增强 InputStreamReader 的功能
BufferedReader br = new BufferedReader(isr);

3.2 Java 日志中的适配器模式

在 Java 中有很多日志框架,比如 log4j、logback、JDK 提供的 JUL(java.util.logging)等,这些日志都提供了相似的功能,比如按照不同级别(debug、info、warn、error 等)打印日志信息,不过 它们并没有实现统一的接口

所以如果我们在开发时,使用了多套日志框架,例如项目中使用的组件是使用 log4j,而我们项目本身使用的是 logback。这样我们的项目就 需要编写两套日志框架的配置文件(日志存储的文件地址、打印日志的格式等)。如果引入多个组件,每个组件使用的日志框架都不同,那么日志本身的管理工作就会变得非常复杂。所以我们需要 统一日志打印框架

Java 中的 Slf4j 日志框架就提供了一套打印日志的统一接口。不过它只定义了接口,并没有具体实现,所以需要配合其他日志框架来使用。

而 Slf4j 的出现又晚于 log4j、JUL 等,所以为了兼容它们,Slf4j 还针对不同的日志框架提供了对应的适配器,对不同日志框架的接口进行了二次封装,适配成统一的 Slf4j 接口定义

具体代码示例如下:

// slf4j 统一的接口定义
package org.slf4j;
public interface Logger {
  public boolean isTraceEnabled();
  public void trace(String msg);
  public void trace(String format, Object arg);
  public void trace(String format, Object arg1, Object arg2);
  public void trace(String format, Object[] argArray);
  public void trace(String msg, Throwable t);
 
  public boolean isDebugEnabled();
  public void debug(String msg);
  public void debug(String format, Object arg);
  public void debug(String format, Object arg1, Object arg2)
  public void debug(String format, Object[] argArray)
  public void debug(String msg, Throwable t);

  //...省略info、warn、error等一堆接口
}

/*
    log4j 日志框架的适配器:
    Log4jLoggerAdapter 实现了 LocationAwareLogger接口,
    其中 LocationAwareLogger 继承自 Logger 接口,
    也就相当于 Log4jLoggerAdapter 实现了 Logger 接口。
*/
package org.slf4j.impl;
public final class Log4jLoggerAdapter extends MarkerIgnoringBase implements LocationAwareLogger, Serializable {
    
  final transient org.apache.log4j.Logger logger; // log4j
 
  public boolean isDebugEnabled() {
    return logger.isDebugEnabled();
  }
 
  public void debug(String msg) {
    logger.log(FQCN, Level.DEBUG, msg, null);
  }
 
  public void debug(String format, Object arg) {
    if (logger.isDebugEnabled()) {
      FormattingTuple ft = MessageFormatter.format(format, arg);
      logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());
    }
  }
 
  //...省略一堆接口的实现...
}

这样我们在项目中就可以 统一使用 Slf4j 提供的接口来编写打印日志的代码,具体使用哪种日志框架实现(log4j、logback……),是可以动态地指定的(使用 Java 的 SPI 技术),只需要将相应的 SDK 导入到项目中即可。

4. 代理、装饰器、适配器模式的区别

代理、装饰器和适配器模式,都是常用的结构型设计模式,它们的代码结构非常相似。笼统来说,**它们都可以成为 Wrapper 模式,即通过 Wrapper 类二次封装原始类。

但是,它们封装的目的、要解决的问题、应用场景完全不同,这也就是它们的区别。

代理模式:在不改变原始类的条件下,为原始类定义一个代理类,主要目的提供前后扩展点,增加一些非业务性的功能,例如统计、限流、日志等

装饰器模式:在不改变原始类的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。例如 BufferdInputStream 对 InputStream 的子类(比如 FileInputStream)添加了缓存的增强,再嵌套一个 DataInputStream 又添加了对基本数据类型读取的增强。

适配器模式:它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作,属于是一种 “补偿模式”。例如 Java IO 中字节流与字符流是不兼容的,而它提供了 InputStreamReader 装饰器,内部通过 StreamDecoder 来将字节流转成字符流,从而让字符流 Reader 的子类能与字节流 InputStream 的子类能一起工作。

适配器模式提供的是跟原始类不同的接口,而代理模式和装饰器模式提供的都是跟原始类相同的接口

适配器模式有两种实现方式:

  • 类适配器:使用 继承 实现;
  • 对象适配器:使用 组合 实现。

其实,代理模式也可以有两种实现方式

  • 一般情况下,我们会让代理类和原始类实现相同的接口,然后在代理类中通过组合的方式,将原始类注入进来;
  • 但是如果原始类没有定义接口,并且原始类是第三方开发的,这是就可以让代理类继承原始类,在代理类方法中通过 super.xx() 方法来调用原始类的方法了。

而装饰器模式只能通过组合关系实现,因为它本身就是使用组合来替代继承,用于解决继承关系过于复杂的问题。

上次编辑于: