适配器模式详解与使用示例

标签: 设计模式, Java

保留所有版权,请引用而不是转载本文(原文地址 https://yeecode.top/blog/53/ )。

1. 适配器思想

适配器模式(Adapter Pattern)是一种结构型模式,基于该模式设计的类能够在两个或者多个不兼容的类之间起到沟通桥梁的作用。

转换插头就是一个适配器的典型例子。不同的转换插头能都够适配不同国家的插座标准,从而使得一个电器能在各个国家使用。

适配器的思想在程序设计中非常常见,下面代码就体现了这种思想:

// 方法一
public <K, V> Map<K, V> selectMap(String statement, String mapKey) {
  return this.selectMap(statement, null, mapKey, RowBounds.DEFAULT);
}

// 方法二
public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey) {
  return this.selectMap(statement, parameter, mapKey, RowBounds.DEFAULT);
}

// 方法三
public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds) {
  final List<? extends V> list = selectList(statement, parameter, rowBounds);
  final DefaultMapResultHandler<K, V> mapResultHandler = new DefaultMapResultHandler<>(mapKey,
          configuration.getObjectFactory(), configuration.getObjectWrapperFactory(), configuration.getReflectorFactory());
  final DefaultResultContext<V> context = new DefaultResultContext<>();
  for (V o : list) {
    context.nextResultObject(o);
    mapResultHandler.handleResult(context);
  }
  return mapResultHandler.getMappedResults();
}

上述代码中,方法三是核心方法,它需要四个输入参数。而有些场景下,调用方只能提供三个参数或者两个参数。为了使得只有三个参数或者两个参数的调用方能够正常地调用核心方法,方法一和方法二充当了方法适配器的作用。这两个适配器通过为未知参数设置默认值的方式,搭建起了调用方和核心方法之间的桥梁。

不过,通常我们说起适配器模式是指类适配器或者对象适配器。(均参考自书籍《通用源码阅读指导书——MyBatis源码详解》)

2. 类适配器

下图给出了类适配器的类图。

图 10-1 类适配器类图

上图中,Target接口是Client想调用的标准接口,而Adaptee是提供服务但不符合标准接口的目标类。Adapter便是为了Client能顺利调用Adaptee而创建的适配器类。如下面代码所示,Adapter即实现了Target接口又继承了Adaptee类,从而使得Client能够与Adaptee适配。

public class Adapter extends Adaptee implements Target {
    @Override
    public void sayHi() {
        super.sayHello();
    }
}

3. 对象适配器

对象适配器Adaptee不再继承目标类,而是直接持有一个目标类的对象。下图给出了对象适配器的类图。

图 10-2 对象适配器

下面便给出了使用对象适配器的示例。

public class Adapter implements Target {
    // 目标类的对象
    private Adaptee adaptee;

    // 初始化适配器时可以指定目标类对象
    public Adapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }

    @Override
    public void sayHi() {
        adaptee.sayHello();
    }
}

这样,Adapter可以直接将Client要求的操作委托给目标类对象处理,也实现了Client和Adaptee之间的适配。而且这种适配器更为灵活一些,因为要适配的目标对象是作为初始化参数传给Adapter的,更为灵活一些。

适配器模式能够使得原本不兼容的类可以一起工作。通常情况下,如果目标类是可以修改的,则不需要使用适配器模式,直接修改目标类即可。但如果目标类是不可以修改的(例如目标类由外部提供,或者目标类被众多其他类依赖必须保持不变),那么适配器模式则会非常有用。

4. MyBatis中适配器模式的使用

MyBatis在打印运行日志时大量使用了适配器模式。这是因为日志框架都是外部提供的,例如log4j、Logging、commons-logging、slf4j、logback等等,MyBatis要做的是对接这些日志框架,并通过它们将日志打印出来。在这个过程中,适配器必不可少。

因此,除了少量的装饰器外,MyBatis的Log接口的实现类都是对象适配器,最终的实际工作要委托给被适配的目标对象来完成。因此是否存在一个可用的目标对象成了适配器能否正常工作的关键所在。于是LogFactory的主要工作就是尝试生成各个目标对象。如果一个目标对象能够被生成出来,那该目标对象对应的适配器就是可用的。

LogFactory生成目标对象的工作在静态代码块中被触发。下面代码展示了LogFactory的静态代码块。

static {
  tryImplementation(LogFactory::useSlf4jLogging);
  tryImplementation(LogFactory::useCommonsLogging);
  tryImplementation(LogFactory::useLog4J2Logging);
  tryImplementation(LogFactory::useLog4JLogging);
  tryImplementation(LogFactory::useJdkLogging);
  tryImplementation(LogFactory::useNoLogging);
}

我们首先查看下代码中的tryImplementation方法:

/**
 * 尝试实现一个日志实例
 * @param runnable 用来尝试实现日志实例的操作
 */
private static void tryImplementation(Runnable runnable) {
  if (logConstructor == null) {
    try {
      runnable.run();
    } catch (Throwable t) {
      // ignore
    }
  }
}

tryImplementation方法会在logConstructor为null的情况下调用Runnable对象的run方法。要注意一点,直接调用Runnable的run方法并不会触发多线程,因此LogFactory中的多个tryImplementation方法是依次执行的。这也意味着useNoLogging方法中引用的NoLoggingImpl实现是最后的保底实现,而且NoLoggingImpl不需要被适配对象的支持,一定能够成功。因此,最终的保底日志方案就就是不输出日志。

我们以“tryImplementation(LogFactory::useCommonsLogging)”为例继续追踪源码,该方法通过useCommonsLogging方法调用到了setImplementation方法。下面是setImplementation方法的带注释源码。

/**
 * 设置日志实现
 * @param implClass 日志实现类
 */
private static void setImplementation(Class<? extends Log> implClass) {
  try {
    // 当前日志实现类的构造方法
    Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
    // 尝试生成日志实现类的实例
    Log log = candidate.newInstance(LogFactory.class.getName());
    if (log.isDebugEnabled()) {
      log.debug("Logging initialized using '" + implClass + "' adapter.");
    }
    // 如果运行到这里,说明没有异常发生。则实例化日志实现类成功。
    logConstructor = candidate;
  } catch (Throwable t) {
    throw new LogException("Error setting Log implementation.  Cause: " + t, t);
  }
}

上述代码显示setImplementation方法会尝试获取参数中类的构造函数,并用这个构造函数创建一个日志记录器。如果这次创建是成功的,则意味着以后的创建也是成功的,即当前参数中的类是可用的。因此把参数中类的构造方法赋给了logConstructor属性。这样,当外部调用getLog方法时,便可以由logConstructor创建出一个Log类的实例。

在静态代码块中,我们发现StdOutImpl类并没有参与设置logConstructor属性的过程,这是因为它不在默认日志输出方式的备选列表中。不过这并不代表着它毫无用处,因为MyBatis允许我们自行指定日志实现类。例如,我们在配置文件的settings节点下配置如下信息,则可以自定义StdOutImpl类作为日志输出方式,使得MyBatis的日志输出到控制台上。(均参考自书籍《通用源码阅读指导书——MyBatis源码详解》)

<setting name="logImpl" value="STDOUT_LOGGING"/>

自行指定日志实现类是在XML解析阶段通过调用LogFactory中的useCustomLogging方法实现的。它相比于静态代码块中的方法执行的更晚,会覆盖前面的操作,因此具有更高的优先级。

通过MyBatis的源码,我们了解了适配器模式的使用以及他们如何发挥作用。并且,还了解了MyBatis中日志实现的配置方法及其生效原理。可见,阅读源码对于深入理解设计模式的实现大有裨益。这里,推荐大家阅读《通用源码阅读指导书——MyBatis源码详解》。

通用源码阅读指导书-京东自营

《通用源码阅读指导书》

《通用源码阅读指导书——MyBatis源码详解》是一本以MyBatis的源码为实例讲述源码阅读方法的书籍,并且附带有示例项目源码,MyBatis的全中文注解。书籍还总结了大量的编程知识和架构经验,对提升编程和架构能力十分有用,非常推荐。

可以访问个人知乎阅读更多文章:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。

作者书籍推荐