适配器模式
适配器模式的英文翻译是 Adapter Design Pattern。顾名思义,这个模式就是用来做适配的,它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。对于这个模式,有一个经常被拿来解释它的例子,就是USB转接头充当适配器,把两种不兼容的接口,通过转接变得可以一起工作。
原理很简单,适配器模式有两种实现方式:类适配器和对象适配器。其中,类适配器使用继承关系来实现,对象适配器使用组合关系来实现。两种模式的类图如下:
具体实现的示例代码如下。
public interface Target {
void operation1();
void operation2();
}
public class Adaptee {
public void fun_a() { //... }
public void fun_b() { //... }
public void fun_c() { //... }
}
// 类适配器: 基于继承
public class Adaptor extends Adaptee implements Target {
@Override
public void operation1() {
super.fun_a();
}
@Override
public void operation2() {
//...重新实现fun_b()...
}
// 这里fun_c()不重写,直接继承自Adaptee
}
// 对象适配器:基于组合
public class Adaptor implements Target {
//组合
private Adaptee adaptee;
public Adaptor(Adaptee adaptee) {
this.adaptee = adaptee;
}
@Override
public void operation1() {
//委托给Adaptee
adaptee.fun_a();
}
@Override
public void operation2() {
//...重新实现fun_b()...
}
public void operation3() {
adaptee.fun_c();
}
}
类适配器与对象适配器都符合开闭原则。针对这两种实现方式,在实际的开发中,到底该如何选择使用哪一种呢?
如果Adaptee类定义的方法很多,而且Target接口定义的方法大部分都相同,那我们推荐使用类适配器,因为Adaptor复用父类Adaptee的方法,比起对象适配器的实现方式代码量要少一些。如果Adaptee类定义的方法很多,而且Target接口定义的方法大部分都不相同,此时推荐使用对象适配器,因为组合结构相对于继承更加灵活。
适配器模式应用到Java日志体系
Java中有很多日志框架,在项目开发中,我们常常用它们来打印日志信息。其中,比较常用的有log4j、logback,以及JDK提供的 JUL(java.util.logging) 和Apache的JCL(Jakarta Commons Logging)等。
大部分日志框架都提供了相似的功能,比如按照不同级别(debug、info、warn、error)打印日志等,但它们却并没有实现统一的接口。这主要可能是历史的原因,它不像JDBC那样,一开始就制定了数据库操作的接口规范。
如果我们只是开发一个自己用的项目,那用什么日志框架都可以,log4j、logback随便选一个就好。但是,如果我们开发的是一个集成到其他系统的组件、框架、类库等,那日志框架的选择就没那么随意了。
当前SpringBoot默认日志是较新的logback日志,但是在以前流行的日志却是log4j。再比如,项目中用到的某个组件使用log4j来打印日志,而我们项目本身使用的是logback。将组件引入到项目之后,我们的项目就相当于有了两套日志打印框架。每种日志框架都有自己特有的配置方式。所以,我们要针对每种日志框架编写不同的配置文件(比如,日志存储的文件地址、打印日志的格式)。如果引入多个组件,每个组件使用的日志框架都不一样,那日志本身的管理工作就变得非常复杂。所以,为了解决这个问题,我们需要统一日志打印框架。
在Java的日志体系中,Slf4j这个志框架就相当于JDBC规范,它提供了一套打印日志的统一接口规范。不过,它只定义了接口,并没有提供具体的实现,需要配合其他日志框架(log4j、logback等)来使用。不仅如此,Slf4j 的出现晚于JUL、JCL、log4j等日志框架,所以,这些日志框架也不可能牺牲掉版本兼容性,将接口改造成符合Slf4j接口规范。Slf4j也事先考虑到了这个问题,所以,它不仅仅提供了统一的接口定义,还提供了针对不同日志框架的适配器。对不同日志框架的接口进行二次封装,适配成统一的Slf4j接口定义。
// slf4j统一的Logger接口定义
package org.slf4j;
public interface Logger {
final public String ROOT_LOGGER_NAME = "ROOT";
public String getName();
public boolean isInfoEnabled();
public void info(String msg);
public void info(String format, Object arg);
public void info(String format, Object arg1, Object arg2);
public void info(String format, Object... arguments);
public void info(String msg, Throwable t);
public boolean isInfoEnabled(Marker marker);
public void info(Marker marker, String msg);
public void info(Marker marker, String format, Object arg);
public void info(Marker marker, String format, Object arg1, Object arg2);
public void info(Marker marker, String format, Object... arguments);
public void info(Marker marker, String msg, Throwable t);
public boolean isErrorEnabled();
public void error(String msg);
public void error(String format, Object arg);
public void error(String format, Object arg1, Object arg2);
public void error(String format, Object... arguments);
public void error(String msg, Throwable t);
public boolean isErrorEnabled(Marker marker);
public void error(Marker marker, String msg);
public void error(Marker marker, String format, Object arg);
public void error(Marker marker, String format, Object arg1, Object arg2);
public void error(Marker marker, String format, Object... arguments);
public void error(Marker marker, String msg, Throwable t);
//...省略trace、warn等众多方法
}
Slf4j提供的log4j日志框架适配器源码如下,其中Log4jLoggerAdapter实现了LocationAwareLogger接口,而其中LocationAwareLogger继承了Logger接口。
// log4j日志框架的适配器
package org.slf4j.impl;
public final class Log4jLoggerAdapter extends MarkerIgnoringBase
implements LocationAwareLogger, Serializable {
// log4j
final transient org.apache.log4j.Logger logger;
static final String FQCN = Log4jLoggerAdapter.class.getName();
final boolean traceCapable;
Log4jLoggerAdapter(Logger logger) {
this.logger = logger;
this.name = logger.getName();
this.traceCapable = this.isTraceCapable();
}
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());
}
}
public void debug(String format, Object arg1, Object arg2) {
if (logger.isDebugEnabled()) {
FormattingTuple ft = MessageFormatter.format(format, arg1, arg2);
logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());
}
}
public void debug(String format, Object[] argArray) {
if (logger.isDebugEnabled()) {
FormattingTuple ft = MessageFormatter.arrayFormat(format, argArray);
logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());
}
}
public void debug(String msg, Throwable t) {
logger.log(FQCN, Level.DEBUG, msg, t);
}
//...省略其他接口的实现...
}
在开发业务系统或者开发框架、组件的时候,我们统一使用Slf4j提供的接口来编写打印日志的代码,具体使用哪种日志框架实现(log4j、logback等),是可以动态地指定的(使用Java的SPI技术),只需要将相应的SDK导入到项目中即可。
但是,如果一些老的项目没有使用 Slf4j,而是直接使用比如JCL来打印日志,那如果想要替换成其他日志框架,比如log4j,该怎么办呢?实际上,Slf4j不仅仅提供了从其他日志框架到Slf4j的适配器,还提供了反向适配器,也就是从Slf4j到其他日志框架的适配。我们可以先将JCL切换为Slf4j,然后再将Slf4j切换为log4j。经过两次适配器的转换,我们就能成功将JCL切换成了log4j。
关于Java日志体系的更详细解析,可参见从一个Logger异常开始梳理Java日志体系这篇博客。
适配器模式与装饰器模式的区别
装饰器与适配器都有一个别名叫做包装模式(Wrapper),它们看似都是起到包装一个类或对象的作用,但是使用它们的目的很不一一样。
适配器模式是一种事后的补救策略,它提供跟原始类不同的接口,适配器模式的意义是将一个接口转变成另一个接口,它的目的是通过改变接口来达到重复使用的目的。而装饰器模式不是要改变被装饰对象的接口,而是恰恰要保持原有的接口,但是增强原有对象的功能,或者改变原有对象的处理方式而提升性能。