Java设计模式-适配器模式(Adapter Pattern)
目录
- 什么是适配器模式
- 适配器模式的3种类型
- JavaSE适配器模式的应用
- Struts2适配器模式的应用
适配器模式是一种“补救模式”,是系统开发完上线运行后需要扩展时使用,而不是系统设计时使用
一、什么是适配器模式
适配器模式把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。–《JAVA与模式》中的定义
将一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。–《设计模式之禅》中的定义
举个例子,VGA转HDMI线
早期的电脑、投影仪都是有VGA口的,但随着时代发展,都变成了HDMI口,这就导致新电脑(HDMI)在接入旧投影仪(VGA)时没法用,然后网上就出现了一种线,就是VGA转HDMI转接线,而这个线就是所谓的适配器。
转接线让本来无法在一起工作的电脑和投影仪能够在一起工作,所以转接线就是适配器
适配器模式的UML图如下
- **目标(Target)角色:**这就是所期待得到的接口
- **源(Adapee)角色:**现在需要适配的接口
- **适配器(Adaper)角色:**适配器类是本模式的核心。适配器把源接口转换成目标接口。
现在需要将Adaptee转为Target,但两者并未继承、实现关系,如何让源(Adapee)适配目标(Target)角色,就是通过适配器(Adaper)角色实现,使用的方式是成为两者共同的子类,就像父母通过孩子来产生血缘关系一样。这就联想到刚开始的定义:使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。
我们通过UML图来看下VGA转HDMI的例子
HDMI支持数字信号(digitalSignal方法),VGA支持模拟信号(analogSignal方法),电脑现在只有HDMI接口,投影仪只有VGA接口,所以需要一个数字信号转模拟信号的适配器(VGA2HDMI类完成)
用代码实现该例子
package org.Adapter.version1;
interface IHDMI{
public void digitalSignal();
}
class HDMI implements IHDMI{
@Override
public void digitalSignal() {
System.out.println("HDMI数字信号传输");
}
}
class VGA{
public void analogSignal(){
System.out.println("VGA模拟信号传输");
}
}
class VGA2HDMI extends VGA implements IHDMI {
@Override
public void digitalSignal() {
System.out.println("适配器将数字信号转为模拟信号");
super.analogSignal();
}
}
class Computer{
public void show(IHDMI hdmi){
if (hdmi == null){
throw new NullPointerException("hdmi is null");
}
System.out.println("电脑开始投屏");
hdmi.digitalSignal();
}
}
public class ClassAdapter {
public static void main(String[] args) {
IHDMI hdmi = new HDMI();
Computer computer = new Computer();
computer.show(hdmi);
System.out.println("------------------------------");
VGA2HDMI vga2HDMI = new VGA2HDMI();
computer.show(vga2HDMI);
}
}
// 输出结果
电脑开始投屏
HDMI数字信号传输
------------------------------
电脑开始投屏
适配器将数字信号转为模拟信号
VGA模拟信号传输
可以看到当Computer使用适配器时,最终调用了VGA实现了图像传输,完成HDMI转VGA的目的。需要注意的是Computer中接收的是目标(Target)角色,因为我们需要的是Target,整个过程也是将Adapee转为Target。
二、适配器模式的3种类型
2.1 类适配器模式
刚才讲的是第一种类型:类适配器模式,特点是把适配的类的API转换为目标类的API
2.2 对象适配器模式
第二种是对象适配器模式,作用同样是适配,但类适配器模式是通过继承实现,而对象适配器是通过委派,看下UML图
在Adaptee中没有sampleOperation2方法,需要Adapter去适配,该类持有Adaptee对象,从而实现对Adapteee的功能扩展,Adapter与Adaptee是委派关系,这就是对象适配器模式。
还是使用转接线的案例实现下
package org.Adapter.version2;
interface IHDMI{
public void digitalSignal();
}
class HDMI implements IHDMI {
@Override
public void digitalSignal() {
System.out.println("HDMI数字信号传输");
}
}
class VGA{
public void analogSignal(){
System.out.println("VGA模拟信号传输");
}
}
class VGA2HDMI implements IHDMI {
private VGA vga;
public VGA2HDMI(VGA vga){
this.vga = vga;
}
@Override
public void digitalSignal() {
System.out.println("适配器将数字信号转为模拟信号");
vga.analogSignal();
}
}
class Computer{
public void show(IHDMI hdmi){
if (hdmi == null){
throw new NullPointerException("hdmi is null");
}
System.out.println("电脑开始投屏");
hdmi.digitalSignal();
}
}
public class ObjectAdapter {
public static void main(String[] args) {
IHDMI hdmi = new HDMI();
Computer computer = new Computer();
computer.show(hdmi);
System.out.println("------------------------------");
VGA vga = new VGA();
VGA2HDMI vga2HDMI = new VGA2HDMI(vga);
computer.show(vga2HDMI);
}
}
// 输出结果
电脑开始投屏
HDMI数字信号传输
------------------------------
电脑开始投屏
适配器将数字信号转为模拟信号
VGA模拟信号传输
可以看出Adapter(VGA2HDMI)只实现了Target(HDMI)的接口,与VGA通过委派建立关系。
对象适配器和类适配器其实算是同一种思想,只不过实现方式不同。对象适配器根据合成复用原则,使用聚合替代继承,所以它解决了类适配器必须继承 Adaptee(VGA) 的局限性问题,也不再要求Target(HDMI)必须是接口,对象适适配器模式的使用成本更低,更加灵活,甚至可以适配多个源(Adapee)角色,试想一下下面的场景
Adapter中持有不同类型充电口的对象即可完成适配。
2.3 缺省适配器模式
也称为接口适配器模式,但表达的都是同一个意思。前面的例子都是非常简单的类,Target只有一个方法,但是如果Target有10个方法呢?Adapter都要实现一遍吗?但Adapter只需要其中几个而已,全部都实现感觉违反了接口隔离原则(只是有点相似),我不需要为什么要实现?这导致Adapter中会有很多空方法,就很奇怪,所以缺省适配器模式就是为了解决这个问题。
当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求
还是用前面例子,HDMI有很多功能,例如音频传输、4K高清显示、普通画质显示、48Gbps的告诉传输,但我只需要普通画质显示,开个会而已,不是看电影,用不了那么高的需求,所以Adapter只需要实现普通画质显示就行了,但这会报语法错误,Java要求implements的接口必须全部实现,哪怕是空实现。所以要使用缺省适配器模式来解决这个问题。
看代码实现
package org.Adapter.version3;
interface IHDMI{
public void transmitVoice();
public void video1080();
// 我们只需要他的数字信号功能,至于能不能1080P播放,不关心
public void digitalSignal();
public void transmit48Gbps();
}
abstract class AHDMI implements IHDMI{
// 必须是空方法,如果不实现空方法,还会让子类去实现
public void transmitVoice(){};
public void video1080(){};
public void transmit48Gbps(){};
@Override
public void digitalSignal() {
System.out.println("HDMI数字信号传输");
}
}
class HDMI extends AHDMI {
@Override
public void digitalSignal() {
System.out.println("HDMI数字信号传输");
}
}
class VGA{
public void analogSignal(){
System.out.println("VGA模拟信号传输");
}
}
class VGA2HDMI extends AHDMI {
private VGA vga;
public VGA2HDMI(VGA vga){
this.vga = vga;
}
@Override
public void digitalSignal() {
System.out.println("适配器将数字信号转为模拟信号");
vga.analogSignal();
}
}
class Computer{
public void show(IHDMI hdmi){
if (hdmi == null){
throw new NullPointerException("hdmi is null");
}
System.out.println("电脑开始投屏");
hdmi.digitalSignal();
}
}
public class DefaultAdapter {
public static void main(String[] args) {
IHDMI hdmi = new HDMI();
Computer computer = new Computer();
computer.show(hdmi);
System.out.println("------------------------------");
VGA vga = new VGA();
VGA2HDMI vga2HDMI = new VGA2HDMI(vga);
computer.show(vga2HDMI);
}
}
// 运行结果
电脑开始投屏
HDMI数字信号传输
------------------------------
电脑开始投屏
适配器将数字信号转为模拟信号
VGA模拟信号传输
运行结果和前面的案例是一模一样的,只不过我们扩展了HDMI的功能,但只需要它的某个功能。
在任何时候,如果不准备实现一个接口的所有方法时,就可以使用“缺省适配模式”制造一个抽象类,给出所有方法的空实现。这样,从这个抽象类再继承下去的子类就不必实现所有的方法了。
看到这里会发现一个问题,缺省适配器模式是不是就是装饰模式?我们使用Adapter去装饰Adaptee,去增强它的功能,但又有点区别,缺省模式调用了Target的方法,而装饰模式没有,但装饰模式会调用其它类的方法,是不是有点晕?
其实缺省适配器模式和装饰模式的用的思想是一样的,都是包装模式(Wrapper)但它们的目的不一样
- 适配器模式的意义是要将一个接口转变成另外一个接口,它的目的是通过改变接口来达到重复使用的目的。
- 装饰器模式不是要改变被装饰对象的接口,而恰恰要保持原有的接口,但是增强原有对象的功能,或者改变原有对象的处理方法而提高性能。
三、JavaSE适配器模式的应用
3.1 JDK中的RunnableAdapter就使用了对象适配器模式
看代码
package org.Adapter.version4;
import java.util.concurrent.Callable;
class Task implements Callable<Integer> {
private int num;
public Task(int num) {
this.num = num;
}
/**
* 模拟下载功能
* @return 下载成功的个数
* @throws Exception
*/
public Integer call() throws Exception {
System.out.println("Callable开执行下载任务");
int result = 0;
for (int i = 1; i <= this.num; i++) {
try {
System.out.println("正在下载资源:第" +i+ "个");
result += i;
} catch (Exception e){
System.out.println("下载异常");
return result;
}
}
return result;
}
}
class RunnableAdapter implements Runnable {
// 源(Adapee)角色,将其转为Runnalbe对象
private Callable<?> callable;
public RunnableAdapter(Callable<?> callable) {
this.callable = callable;
}
// 实现目标对象的接口
public void run() {
try {
// 实际交给Callable执行
System.out.println("RunnableAdapter委托给Callable执行");
callable.call();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public class Test {
public static void main(String[] args) {
Callable<Integer> callable = new Task(10);
// 这是报错的,Thread需要一个Runnable类
// Thread thread = new Thread(callable);
// thread.start();
System.out.println("Thread开始执行下载任务");
RunnableAdapter adapter = new RunnableAdapter(callable);
Thread thread1 = new Thread(adapter);
thread1.start();
}
}
// 执行结果
Thread开始执行下载任务
RunnableAdapter委托给Callable执行
Callable开执行下载任务
正在下载资源:第1个
正在下载资源:第2个
正在下载资源:第3个
正在下载资源:第4个
正在下载资源:第5个
正在下载资源:第6个
正在下载资源:第7个
正在下载资源:第8个
正在下载资源:第9个
正在下载资源:第10个
刚开始时Thread的构造函数直接传Callable是报错的,编译不通过,怎么办?
- 要么修改Task类,实现Runnable接口,显然不行,违反开闭原则
- 要么使用适配器模式,将Callable变成Runnable,这个过程就是适配的过程
3.2 JavaIO流使用了适配器模式
3.2.1 IO流的适配器模式
在装饰模式中,我们提到了IO流使用了装饰模式,IO流同样也使用了对象适配器模式
InputStream类类型的原始流处理器是对象适配器模式的应用,我们以此为例,看看如何使用
package org.Adapter.version4;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
public class TestIO {
public static void main(String[] args) throws Exception {
// 下面两个方法是独立的,无任何关联关系
// 普通的InputStream的读取byte
inputStream();
// 将数据流读取到char
inputStreamReader();
}
/**
* 仅仅展示InputStream的使用方法
* @throws Exception
*/
public static void inputStream() throws Exception{
InputStream inputStream = Files.newInputStream(Paths.get("a.txt"));
byte b[] = new byte[8];
while (inputStream.read(b) > -1){
System.out.println(new String(b));
}
inputStream.close();
}
/**
* readChar函数需要的reader类,但InputStream并不能满足要求
* 得将但InputStream转为Reader
* @throws Exception
*/
public static void inputStreamReader() throws Exception{
InputStream inputStream = Files.newInputStream(Paths.get("a.txt"));
// 将inputsStream”转换为“为Reader
Reader reader = new InputStreamReader(inputStream);
// 读取文件的方法
readChar(reader);
}
public static void readChar(Reader reader) throws IOException {
char c[] = new char[8];
while (reader.read(c) > -1){
System.out.println(new String(c));
}
reader.close();
}
}
// 运行结果
aaaaaa
aaaaaa
分析下IO流在对象适配器模式下的角色,先回顾下对象适配器的UML图
- **目标(Target)角色:**Reader
- **源(Adapee)角色:**InputStream,在InputStreamReader中持有InputStream对象
- **适配器(Adaper)角色:**InputStreamReader,继承了Reader
- **客户端角色:**TestIO.readChar()
所以总结来说,InputStreamReader完成了将InputStream“转为”Reader的功能,使其调用read(java.nio.CharBuffer target)
方法(其实在InputStreamReader中将InputStream封装为了一个StreamDecoder,可以指定编码,这个可以忽略,了解即可 )
read(java.nio.CharBuffer target)方法内部调用的是自己的抽象方法read(char cbuf[], int off, int len),该抽象方法由InputStreamReader实现,方法如下
public int read(char cbuf[], int offset, int length) throws IOException {
return sd.read(cbuf, offset, length);
}
这里的sd是就是InputStreamReader构造函数中传入的InputStream对象,也就是说InputStreamReader类(也可以说是Reader类)的read方法实际是调用了InputStream的read方法,从而实现了InputStream“转为”Reader的功能,并未实现功能的增强,只是起到转换作用。
3.2.2 IO流的装饰模式
比如下面代码,在InputStreamReader外层又包装了BufferedReader,因为要使用它BufferedReader的readLine方法
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(Files.newInputStream(Paths.get("a.txt"))));
// 或者下面代码,和上面功能相同
InputStream inputStream = Files.newInputStream(Paths.get("a.txt"));
// 将inputsStream”转换为“为Reader
Reader reader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(reader);
String line = bufferedReader.readLine();
在readLine方法内部调用的就是InputStreamReader的read方法,并对InputStreamReader的read方法进行了增强,实现逐行读取,这就是装饰模式,调用过程为readLine()->fill()->InputStreamReader.read(),所以对于BufferedReader来说是装饰模式
对于InputStreamReader是适配器模式,BufferedReader中不能直接传FileInputStream,所以InputStreamReader就是适配器,完成了将InputStream转为Reader的过程。
- **目标(Target)角色:**Reader
- **源(Adapee)角色:**InputStream,在InputStreamReader中持有InputStream对象
- **适配器(Adaper)角色:**InputStreamReader,继承了Reader
- **客户端:**BufferedReader,需要传Reader
3.2.3 一点疑问
先看下适配模式的代码,如下代码1
InputStream inputStream = Files.newInputStream(Paths.get("a.txt"));
// 将inputsStream”转换为“为Reader
Reader reader = new InputStreamReader(inputStream);
reader.read();
那如果变为这样呢,代码2,两段代码仅仅是new InputStreamReader(inputStream);的接收类型不一样而已
InputStream inputStream = Files.newInputStream(Paths.get("a.txt"));
InputStreamReader reader = new InputStreamReader(inputStream);
reader.read();
代码1调用的是Reader.read方法,而代码2调用的是InputStreamReader.read方法,这两个最终调用的都是InputStreamReader.read方法,所以这两段代码从宏观上来说都是适配器模式的使用,而单从InputStream和InputStreamReader的视角看,就是装饰模式的使用。
这两个模式就是这样交叉使用,所以从业务视角看下两个设计模式的区别
- 适配器模式:
在由InputStream,OutputStream,Reader和Writer代表的等级结构内部,有一些流处理器是对其它类型的流源的适配。这就是适配器模式的应用。如InputStreamReader和OutputStreamWriter做InputStream/OutputStream字节流类到Reader/Writer之间的转换。
- 装饰者模式:
在由 InputStream,OutputStream,Reader和Writer代表的等级结构内部,有一些流处理器可以对另一些流处理器起到装饰作用,形成新的,具有改善了的功能的流处理器。装饰者模式是Java I/O库的整体设计模式。这样的一个原则是符合装饰者模式的。如 BufferedInputStream bis = new BufferedInputStream(new FileInputStream())
四、Struts2适配器模式的应用
留个坑,暂时没发现,如果有,还请私信我增加,谢谢。