适配器模式
本文内容
前言
适配器模式也是一种常用的结构型模式,它主要有两种实现方式,类适配器和对象适配器。下面就来介绍下适配器模式的原理和它的应用场景,以及它和前面的装饰器、代理模式又有何区别?
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 大体上分为四类,如下所示:
类型 | 字节流 | 字符流 |
---|---|---|
输入流 | InputStream | Reader |
输出流 | OutputStream | Writer |
这四大类又派生除了很多针对不同场景的子类,它们的关系图如下:
我们可以发现一个规律,字节流的类都带有 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()
方法来调用原始类的方法了。
而装饰器模式只能通过组合关系实现,因为它本身就是使用组合来替代继承,用于解决继承关系过于复杂的问题。