从零开始学习Java设计模式 | 结构型模式篇:适配器模式
Posted 李阿昀
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零开始学习Java设计模式 | 结构型模式篇:适配器模式相关的知识,希望对你有一定的参考价值。
在本讲,我们来学习一下结构型模式里面的第二个设计模式,即适配器模式。
概述
在学习适配器模式之前,我们先来看下下面这个场景。
如果去欧洲国家旅游的话,他们的插座如下图最左边,是欧洲标准,而我们使用的插头如下图最右边,很显然,我们的插头是不能直接插到欧标的插座上面的。因此我们的笔记本电脑、手机在当地都不能直接充电,所以此时我们就需要一个插座转换器了,转换器第1面插入当地的插座,第2面供我们充电,这样使得我们的插头在当地就能使用了,也就是说我们的笔记本电脑、手机等在当地就可以正常的进行充电操作了。
生活中这样的例子很多,手机充电器(将220v的电压转换为5v的电压)、读卡器等,其实它们就使用到了适配器模式。
以上我们通过一个案例简单的去认识了一下适配器模式,接下来我们就来看一下到底什么是适配器模式。
适配器模式指的是将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
上述适配器模式的概念是说的什么意思呢?我们还是结合上面那张图来分析一下。从上图中我们知道,我们希望的是能使用咱们两头插头的这样的一个接口,但是欧洲国家提供的都是欧标的接口(插座),这样,我们就需要将欧标接口转换成我们所希望使用到的接口了。要想做到这点,就不得不用到插座转换器了,有了它之后,我们就可以正常的使用咱们之前的充电头了,继而就能插入对应的插座对我们的笔记本电脑、手机进行充电了。不过,这一切都得借助于插座转换器。
相信经过我上面的解释,大家对于适配器模式的概念一定有了一个更深入的认识。接下来,我们再来看一下适配器模式的分类。
适配器模式分为类适配器模式和对象适配器模式,前者类之间的耦合度比后者高(这是因为类适配器模式使用的是继承的方式,而对象适配器模式使用的是聚合或者组合的方式),且类适配器模式要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些,用的更多的还是对象适配器模式。
结构
适配器模式里面总共拥有三个角色,它们分别是:
- 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。以上面案例为例,目标接口指的就是我们所希望要的两头的插座,这样的插座在欧洲国家是没有提供的
- 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。对应上面案例中的欧标插座,该插座现在在欧洲国家里面是已经存在的了
- 适配器(Adapter)类:它是一个转换器,通过继承(类适配器模式)或引用适配者的对象(对象适配器模式),把适配者接口转换成目标接口(也就是使用转换器将三头的欧标插座转换成适合我们使用的两头插座),让客户按目标接口的格式访问适配者。很显然,它对应上面案例中的插座转换器
类适配器模式案例
分析
接下来,我们来实现一个类适配器模式的案例,不过在实现之前,我们先来看一下实现的方式。
类适配器模式实现的方式是定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件。
What’s Up(卧槽)!这说的是个啥意思啊?上面说的适配器类、当前系统的业务接口和现有组件库中已经存在的组件分别表示什么啊?别急,下面我就会给大家解释解释一下。
- 适配器类:这个比较好理解,就是类适配器模式角色里面的适配器类
- 当前系统的业务接口:就是类适配器模式角色里面的目标接口,我们当前系统可以使用的接口就是目标接口
- 现有组件库中已经存在的组件:就是类适配器模式角色里面的适配者类
所以,类适配器模式的实现方式就包含了类适配器模式里面的三个角色。
类适配器模式的实现方式明确之后,接下来,我们就来看一个具体的案例,即读卡器案例。
现有一台电脑只能读取SD卡,而要读取TF卡中的内容的话就需要使用到适配器模式。那如何来说实现呢?创建一个读卡器类(或者适配器类),将TF卡中的内容读取出来。
简单地分析了一下以上读卡器案例之后,接下来,我们来看下下面的类图,通过该类图再去了解一下该案例牵扯到了哪些类和接口以及接口和类之间的关系。
首先,我们来看一下以上类图的右边部分,这一部分表示的其实是适配者类。在这一部分,我们为了给适配者类提供一个规范,所以就定义了一个接口(即TFCard),里面有两个方法,一个是从TF卡里面去读取数据,一个是往TF卡里面去写数据,读取数据的方法肯定是有返回值的,而写数据的方法未必有返回值,但是肯定是有参数的,只不过我在这里面没有体现出来而已。看完TFCard接口之后,再来看一下它的子实现类(即TFCardImpl),它里面重写了父接口中的两个方法。
然后,我们再来看一下以上类图的上边部分,这一部分表示的其实是目标接口。同样,我们也定义了一个接口(即SDCard),它里面也有两个方法,一个是从SD卡里面去读取数据,一个是往SD卡里面去写数据。同时,我们又给该接口提供了一个子实现类(即SDCardImpl),该子实现类同样重写了父接口中的两个抽象方法。
接着,我们来看一下Computer类,它是只能读取SD卡的,所以我们给它里面定义了一个从SD卡里面去读取数据的readSD方法。当然了,你也可以定义一个往SD卡里面去写数据的方法,只不过我在这里面没有定义出来而已,而只是通过readSD方法来模拟了。很显然,该方法需要一个SDCard接口类型的对象,返回的是一个字符串,所以Computer类和SDCard接口是属于依赖的关系。
最后,大家来思考一个问题,如果我们想要使用这台电脑去读取TF卡里面的数据,那么怎么办呢?能不能去创建TFCardImpl子实现类对象,把它作为参数进行一个传递呢?很显然肯定是不行的。不行的话那又该怎么办呢?此时,我们应该定义一个适配器类(即SDAdapterTF,也就是说SD卡来兼容TF卡),而且我们还要让该适配器类去实现SD卡的目标接口(即SDCard),这是因为咱们的电脑只能读取SD卡,这样一来,该适配器类就要去重写SDCard接口中的两个抽象方法了。同时,我们还要让该适配器类去继承TFCardImpl类,这样的话,我们在适配器类中提供的两个方法看似好像是从SD卡里面去读取数据或者是往SD卡里面去写数据,但实际上我们用的是TF卡里面的功能。
以上就是我对以上类图的一个分析。当然了,我还没有带着大家分析一下客户端类(即Client),因为它很简单,就只是依赖了Computer、SDCardImpl、SDAdapterTF这三个类。
分析完以上类图之后,接下来,我们就得通过具体的代码来实现了。
实现
上面我们简单分析了一下读卡器案例,并且详细地去分析了一下该案例所表示的类图里面涉及到的接口以及类,接下来,我们就要通过具体的代码来实现了。
首先,打开咱们的maven工程,并在com.meimeixia.pattern包下新建一个子包,即adapter.class_adapter,也即类适配器模式的具体代码我们是放在了该包下。
然后,创建一个接口,我们不妨把该接口命名为TFCard。
package com.meimeixia.pattern.adapter.class_adapter;
/**
* 适配者类的接口
* @author liayun
* @create 2021-07-30 21:02
*/
public interface TFCard {
// 从TF卡中读取数据
String readTF();
// 往TF卡中写数据
void writeTF(String msg);
}
以上接口创建完毕之后,我们创建它的一个实现类,该实现类我们不妨就叫为TFCardImpl,其实,该实现类就是适配者类。
package com.meimeixia.pattern.adapter.class_adapter;
/**
* 适配者类
* @author liayun
* @create 2021-07-30 21:07
*/
public class TFCardImpl implements TFCard {
/*
* 注意,这里只是模拟从TF卡里面读取数据
*/
@Override
public String readTF() {
String msg = "TFCard read msg : hello world TFCard";
return msg;
}
/*
* 往TF卡里面写数据时,我们是直接将拿到的数据输出到了控制台
*/
@Override
public void writeTF(String msg) {
System.out.println("TFCard write msg : " + msg);
}
}
接着,创建目标接口,我们不妨把该目标接口命名为SDCard。为何说它是目标接口呢?因为咱们的电脑只能读取SD卡里面的数据。
package com.meimeixia.pattern.adapter.class_adapter;
/**
* 目标接口
* @author liayun
* @create 2021-07-30 21:15
*/
public interface SDCard {
// 从SD卡中读取数据
String readSD();
// 往SD卡中写数据
void writeSD(String msg);
}
目标接口创建完毕之后,我们就要给它创建一个具体的实现类了,该实现类我们不妨就叫为SDCardImpl。
package com.meimeixia.pattern.adapter.class_adapter;
/**
* 具体的SD卡类
* @author liayun
* @create 2021-07-30 21:22
*/
public class SDCardImpl implements SDCard {
/*
* 注意,这里只是模拟从SD卡里面读取数据
*/
@Override
public String readSD() {
String msg = "SDCard read msg : hello world SD";
return msg;
}
/*
* 往SD卡里面写数据时,我们是直接将拿到的数据输出到了控制台
*/
@Override
public void writeSD(String msg) {
System.out.println("SDCard write msg : " + msg);
}
}
紧接着,创建Computer类。在该类里面,我们需要定义一个从SD卡里面去读取数据的方法。
package com.meimeixia.pattern.adapter.class_adapter;
/**
* 计算机类
* @author liayun
* @create 2021-07-30 21:29
*/
public class Computer {
// 从SD卡中读取数据
public String readSD(SDCard sdCard) { // 读取数据的话,你得给我一个SD卡,我才能从里面去读取数据,是不是啊?
// 所以,在这里我们的实现就是传递一个SDCard接口的子实现类对象。当然了,
// 在这里我们声明的是接口类型,因为这样的话会更通用一些
if (sdCard == null) {
throw new NullPointerException("sd card is not null");
}
return sdCard.readSD();
}
}
Computer类创建完毕之后,咱们也先别着急着去创建适配器类,而是先来创建客户端类。
package com.meimeixia.pattern.adapter.class_adapter;
/**
* @author liayun
* @create 2021-07-30 21:36
*/
public class Client {
public static void main(String[] args) {
// 创建计算机对象
Computer computer = new Computer();
// 读取SD卡中的数据
String msg = computer.readSD(new SDCardImpl());
System.out.println(msg);
}
}
此时,我们运行一下以上客户端类,来看一下是不是我们想要的结果。如下图所示,读取到的内容确实是SDCard read msg : hello world SD
,可见确实是从SD卡里面读取到的数据。
现在我们有一个需求,就是使用该计算机读取TF卡中的数据,那么该怎么办呢?能直接去读取TF卡中的数据吗?很显然是不能够的,因为咱们的计算机是没有提供从TF卡里面读取数据的功能的。
所以,在这里我们就要去做一件事了,即去创建适配器类,该类我们就不妨命名为SDAdapterTF了,意思就是让SD卡适配TF卡。那么怎么去创建该适配器类呢?上面我讲过类适配器模式的实现方式,就是定义一个适配器类来实现当前系统的业务接口(目前业务接口就是SDCard),同时又继承现有组件库中已经存在的组件(目前已经存在的组件就是TFCardImpl)。
package com.meimeixia.pattern.adapter.class_adapter;
/**
* 适配器类
* @author liayun
* @create 2021-07-30 22:01
*/
public class SDAdapterTF extends TFCardImpl implements SDCard {
@Override
public String readSD() {
System.out.println("adapter read tf card");
return readTF(); // 如果我们使用适配器的话,那么真正的读取数据是从TF卡里面去读取。所以,此处我们直接调用TFCardImpl类里面的readTF方法
}
@Override
public void writeSD(String msg) {
System.out.println("adapter write tf card");
writeTF(msg); // 同理,此处我们直接调用TFCardImpl类里面的writeTF方法
}
}
以上适配器类创建完毕之后,回到咱们的客户端类中,测试一下使用该电脑读取TF卡中的数据是否可行。
package com.meimeixia.pattern.adapter.class_adapter;
/**
* @author liayun
* @create 2021-07-30 21:36
*/
public class Client {
public static void main(String[] args) {
// 创建计算机对象
Computer computer = new Computer();
// 读取SD卡中的数据
String msg = computer.readSD(new SDCardImpl());
System.out.println(msg);
System.out.println("=======================");
// 使用该电脑读取TF卡中的数据
String msg1 = computer.readSD(new SDAdapterTF()); // 创建适配器类对象,并进行一个传递
System.out.println(msg1);
}
}
此时,我们运行一下以上客户端类,来看一下是不是我们想要的结果。如下图所示,在分割线下面读取到的内容确实是TFCard read msg : hello world TFCard
,可见确实是从TF卡里面读取到的数据。
至此,我们就使用类适配器模式实现了电脑从TF卡里面去读取数据的需求。
不过,最后我要说一点,就是类适配器模式违背了合成复用原则,而且类适配器是客户类有一个接口规范的情况下可用,反之则不可用。这是啥意思呢?回看一下一开始的那张类图,如果我们没有定义SDCard接口,而只是就有一个类(即SDCardImpl),那么我们就不能使用类适配器模式来实现了。为什么呢?上面咱们使用了类适配器模式,可以看到适配器类不仅继承了适配者类,而且还实现了目标接口,要是没有目标接口而只有一个类的话,那么你猜一个类能不能同时继承两个类呢?很显然是不可以的啊!所以这不就没办法去实现了嘛!
对象适配器模式案例
分析
接下来,我们还是通过读卡器案例来学习一下适配器模式里面的对象适配器模式。在正式学习之前,我们先来看一下对象适配器模式的实现方式。
对象适配器模式可釆用将现有组件库中已经实现的组件引入适配器类中(当然,在这里我们使用的就是聚合方式),并且该类同时实现当前系统的业务接口的方式来实现。
明确实现方式之后,回过来我们再来看一下读卡器案例。由于现在我们要使用的对象适配器模式,所以我们就得对以上读卡器案例进行一个改进了。
使用对象适配器模式将读卡器案例进行改写之后的类图,如下所示。
可以看到,该类图和上面的类图基本上是一样的,只不过在适配器类中发生了一点变化,它聚合了TFCard,也就是说聚合了现有组件库的组件,因为我们要读取的就是TF卡里面的数据。
也就是说现在咱们的适配器类是聚合进来了适配者类,而不是再去直接继承它了,这样,其实就满足了合成复用原则。
上面我说过了,类适配器模式是客户类有一个接口规范的情况下可用,反之则不可用。那么对于对象适配器模式来说,还是这样吗?如果我们没有定义SDCard接口,而只是就有一个类(即SDCardImpl),那么现在咱们的适配器类是不是直接去继承该类了啊?毕竟现在咱们的适配器类还没有继承任何类呢,它当然就可以去继承其他类了啊,你说是不是啊!
分析至此,相信大家能得出来一个结论,就是对象适配器模式将类适配器模式的两个缺点全部已经弥补了,可能这就是对象适配器模式在开发中用的多的原因吧!
分析完以上类图之后,接下来,我们就得通过具体的代码来实现了。
实现
首先,在com.meimeixia.pattern.adapter包下新建一个子包,即object_adapter,也即对象适配器模式的具体代码我们是放在了该包下。
然后,将class_adapter包下的全部代码拷贝到object_adapter包下,拷贝过来之后,接下来,我们只需要修改适配器类(即SDAdapterTF)和客户端类(即Client)的代码即可。
我们先修改一下适配器类(即SDAdapterTF)的代码吧!现在适配器类不用再继承适配者类了,而是应该聚合适配者类,这就意味着我们应该在适配器类的成员位置声明适配者类,当然这里我们是以接口的形式来声明的,因为这样的话通用性会更好一些。
package com.meimeixia.pattern.adapter.object_adapter;
/**
* 适配器类
* @author liayun
* @create 2021-07-30 22:01
*/
public class SDAdapterTF implements SDCard {
// 声明适配者类
private TFCard tfCard;
/*
* 在适配器类的成员位置声明好了适配者类之后,我们肯定是要对它进行赋值的,所以,我们在这儿就提供了一个有参构造方法,
* 通过该有参构造方法为适配者类赋值
*/
public SDAdapterTF(TFCard tfCard) {
this.tfCard = tfCard;
}
public String readSD() {
System.out.println("adapter read tf card");
return tfCard.readTF(); // 如果我们使用适配器的话,那么真正的读取数据还是从TF卡里面去读取。所以,此处我们应调用TFCardImpl类里面的readTF方法
}
public void writeSD(String msg) {
System.out.println("adapter write tf card");
tfCard.writeTF(msg); // 同理,此处我们应调用TFCardImpl类里面的writeTF方法
}
}
适配器类的代码修改完毕之后,接下来,我们就要客户端类的代码了,暂时先将其修改为下面这样。
package com.meimeixia.pattern.adapter.object_adapter;
/**
* @author liayun
* @create 2021-07-30 21:36
*/
public class Client {
public static void main(String[] args) {
// 创建计算机对象
Computer computer = new Computer();
// 读取SD卡中的数据
String msg = computer.readSD(new SDCardImpl());
System.out.println(msg);
}
}
此时,我们通过计算机去读取SD卡里面的数据,你猜它能不能正常的进行读取呢?如下图所示,确实是可以正常的去读取SD卡里面的数据的。
现在我们的需求是这样的,使用该计算机读取TF卡中的数据,那么又应该如何去读取呢?大家不妨好好思考一下。我们最终是不是还得调用Computer类里面的读取方法(即readSD)啊,但是该方法只能读取SD卡里面的数据,而且还有返回值,是不是啊!不过,readSD方法中需要的就是一个SDCard接口的子实现类对象,而恰巧的是咱们的适配器类正好实现了SDCard接口,所以我们就可以直接往readSD方法里面传递一个SDAdapterTF适配器类的对象了。
思考完了,代码不就是这样写出来了吗?
package com.meimeixia.pattern.adapter.object_adapter;
/**
* @author liayun
* @create 2021-07-30 21:36
*/
public class Client {
public static void main(String[] args) {
// 创建计算机对象
Computer computer = new Computer();
// 读取SD卡中的数据
String msg = computer.readSD(new SDCardImpl());
System.out.println(msg);
System.out.println("=======================");
// 使用该电脑读取TF卡中的数据
// 创建适配器类对象
SDAdapterTF sdAdapterTF = new SDAdapterTF(new TFCardImpl());
String msg1 = computer.readSD(sdAdapterTF);
System.out.println(msg1);
}
}
此时,我们运行一下以上客户端类,来看一下是不是我们想要的结果。如下图所示,在分割线下面读取到的内容确实是TFCard read msg : hello world TFCard
,可见确实是从TF卡里面读取到的数据。
至此,我们就使用对象适配器模式实现了电脑从TF卡里面去读取数据的需求。
很明显,对象适配器模式要比类适配器模式要好一些,因为它满足了两个要求:
- 符合合成复用原则
- 如果客户类没有接口规范的话,那么我们也可以去使用它
最后,我还讲一个注意事项,还有一种适配器模式是接口适配器模式,当不希望实现一个接口中所有的方法时,可以创建一个抽象类Adapter,让它去实现所有方法,只是没有具体的实现体,而此时我们只需要继承该抽象类即可。当然了,因为这种接口适配器模式实现起来比较简单,所以我在这儿就不做具体的代码演示了。
应用场景
适配器模式的应用场景,我总结下来了下面两个。
-
以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
不一致的话,按照开闭原则,我们应尽可能的不要去修改之前的系统,此时,我们就可以使用适配器模式了,即创建一个适配器,让我们的新旧系统进行一个无缝的对接
-
使用第三方提供的组件,但组件接口定义和我们自己要求的接口定义不同。
很明显我们是无法修改第三方提供的组件的,既然无法修改,那么此时我们就可以使用适配器模式了,让第三方组件和我们自己做的系统进行一个无缝的对接
适配器模式在JDK源码中的应用
接下来,我们来看一下适配器模式在JDK源码中具体的应用。
先来看一下下面两个类:
- Reader:字符输入流顶层父类
- InputStream:字节输入流顶层父类
它俩之间的一个适配使用的就是InputStreamReader。
什么意思呢?就是说将字节数据转换成字符数据,我们使用的就是转换流InputStreamReader。而InputStreamReader是继承自java.io包下的Reader的,并且对它里面的如下两个read方法给出了实现,如下图所示。
一个是未带任何参数的read方法,一个是带有三个参数的read方法,它们位于Reader类里面的如下位置。
从上可以看到,未带任何参数的read方法已经被实现了,而带有三个参数的read方法还是抽象的未实现的,但不管怎样,子类(即InputStreamReader)最终还是实现(或者重写)了父类(即Reader)中的以上两个方法。
相信大家也能看到,在InputStreamReader类中,实现(或者重写)了父类(即Reader)中的两个read方法都是直接调用了sd对象里面的read方法。那么,有些同学就会问了,这个sd是什么东东啊?其实,它就是StreamDecoder类的对象。
看到StreamDecoder类的类名,你能猜出它主要是干嘛的吗?我就不绕弯子了,就直说了,就是流的一个解码操作。大家还记得什么是解码和编码吗?不记得,我帮大家回忆回忆。
- 解码:将字节数据转换成字符数据
- 编码:将字符数据转换成字节数据
现在大家就清楚了,StreamDecoder类是用来解码的,也就是将字节数据转换成字符数据。
上面我也说过了,在InputStreamReader类中,实现(或者重写)了父类(即Reader)中的两个read方法都是直接调用了sd对象里面的read方法。现在,大家应该能明白,在Sun的JDK实现中,InputStreamReader类中的两个read方法的实际的方法实现是就对sun.nio.cs.StreamDecoder类的同名方法的调用封装。
接下来,我们来看下下面的这张类图。
以上类图中有一个InputStream,上面我也说过了,它就是字节输入流顶层父类,从上可以看到它里面有三个重载的read方法。而在StreamDecoder类里面,它聚合了InputStream,此外,它还重写了父类(即Reader)中的两个read方法。
我讲到这里,你就会发现StreamDecoder其实就是一个适配器类,而该适配器类继承了Reader类(注意了,Reader其实是一个抽象类),相信你也不难看出Reader类代表的就是适配器模式里面的目标接口角色,只不过该角色在这儿是以抽象类的形式体现出来的。StreamDecoder类除了继承了以上目标接口之外,还聚合了适配者类(即InputStream),嘻嘻😝,这不就是标准的对象适配器模式吗?其实,老实来说,这儿的对象适配器模式和InputStreamReader这个类的关系并不是特别大,只是Reader、InputStream以及StreamDecoder这三个类用到了标准的对象适配器模式。
再来看一下以上类图,可以看到:
- InputStreamReader是对同样实现了Reader的StreamDecoder的封装
- StreamDecoder不是Java SE API中的内容,是Sun JDK给出的自身实现,但我们知道它们对构造方法中的字节流类(即InputStream)进行了封装,其实就是把字节输入流类(即InputStream)聚合进来了,最终并通过该类进行了字节流和字符流之间的解码转换。也就是说,虽然字节输入流类(即InputStream)读取到的是字节数据,但是最终我们可以通过StreamDecoder类将字节数据转换成字符数据并返回,这就是所谓的解码操作😀
通过上面我们的研究与分析,我们最终可以得出这样一个结论:从表层来看,InputStreamReader做了InputStream字节流类到Reader字符流之间的转换(也就是说InputStreamReader把字节流转换成了字符流)。而从如上Sun JDK中的实现类关系结构中可以看出,是StreamDecoder的设计实现在实际上采用了适配器模式,而且还是对象适配器模式。
以上是关于从零开始学习Java设计模式 | 结构型模式篇:适配器模式的主要内容,如果未能解决你的问题,请参考以下文章