Java设计模式-3种适配器模式详解
文章目录
此文为课后笔记,视频资源: 尚硅谷图解Java设计模式韩顺平老师2019力作
3种适配器模式:类适配器模式,对象适配器模式,接口适配器模式
0.前言
讲到适配器,我最先想到的就是电源适配器(俗称充电头),以手机为例,手机不能直接接受220V的交流电,需要通过电源适配器处理一下,转换成手机能接受的电压(比如直流电压5V)。适配器模式中的适配器也是干这种事的,解决两个类不兼容的问题,加一些中间处理操作,使得他们兼容。
适配器模式(AdapterPattern):将一个类的接口(src原类/原接口)转换成客户希望的另外一个接口(dst目标类),使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
适配器模式又分为类适配器模式,对象适配器模式,接口适配器模式三种。他们的区别只是src原类在适配器类种出现的方式不同。
下面的代码将模拟这样的场景:220V交流电——>适配器——>5V直流电
具体到代码即:源电压类,适配器类,5V直流电接口,手机类,测试类
其中源电压类,适配器类都是一样的先贴出来:
源电压类(被适配的类):
/**
* 适配器模式——源类
* 被适配的类
* 220V交流电
*/
public class VoltageSrc {
private int voltageSize=220;
private String voltageType="交流";
//输出源电压 220v
public int outputVoltageSize(){
System.out.println("源电压 输出大小为 "+voltageSize+" V");
return voltageSize;
}
//输出源电压 220v
public String outputVoltageType(){
System.out.println("源电压 输出类型为"+voltageType);
return voltageType;
}
}
适配后的结果(目标接口类)
/**
* 目标接口
* 将来手机要使用的
*/
public interface IVoltageDst {
//输出目标电压 大小 5v
int outputVoltageDstSize();
//输出目标电压 类型 直流
String outputVoltageDstType();
}
1.类适配器
1.1类适配器简介
在类适配器中,Src源类(被适配的类)以类的形式出现在适配器中,即源类是适配器的父类。
所以适配器类需要继承src源类,实现dst目标接口:实现目标接口的方法,在方法中通过调用父类方法以及转换操作完成适配。
1.2 类适配器模式中的角色
- Target目标接口:目标接口类,定义最终需要的方法,在该例中即IVoltageDst接口
- Adaptee被适配者:被适配的类(src源类),持有既定的变量与在方法,该例子中即VoltageSrct类
- Adapter适配器:适配器类,将被适配者中的既定变量与方法转换成目标接口适合的,即本例中ClassAdapter类。
- Client请求者/使用者:请求类,即本例中的Phone手机类
1.3 类适配器模式UML类图
1.4 代码实现
该例子中适配器类代码如下:
/**
* 适配器模式
* 1.类适配器 继承src源类 实现dst目标接口
*/
public class ClassAdapter extends VoltageSrc implements IVoltageDst {
@Override
public int outputVoltageDstSize() {
//首先获取源电压 大小 类型
int voltageSize = outputVoltageSize();//使用父类的方法 获取源电压大小
//模拟源电压到目标电压的转换过程 电压大小转换
return voltageSize / 44;//硬核转换,意思一下
}
@Override
public String outputVoltageDstType() {
String voltageType = outputVoltageType();//使用父类的方法 获取源电压类型
//模拟源电压到目标电压的转换过程 电压类型转换
if (voltageType.equals("交流")){
voltageType = "直流";//硬核转换,意思一下
}
return voltageType;
}
}
手机类:
编写充电方法,传入的参数为目标接口的实例,完成充电操作(先判断电压大小和类型)
public class Phone {
//充电
public void charging(IVoltageDst iVoltageDst){
if (iVoltageDst.outputVoltageDstSize()==5
&& iVoltageDst.outputVoltageDstType().equals("直流")){
System.out.println("电压适配成功,手机冲电ing...");
}else {
System.out.println("电压适配失败,手机充不了电2333");
}
}
}
1.5 测试编码与分析
创建一个手机类的对象,调用充电方法,传入的参数为适配器类的对象,
public class Test {
public static void main(String[] args) {
//类适配器测试
System.out.println("适配器模式 1.类适配器测试");
Phone phone=new Phone();
phone.charging(new ClassAdapter());//传入适配器实例
}
}
测试结果
适配器模式 1.类适配器测试
源电压 输出大小为 220 V
源电压 输出类型为直流
电压适配成功,手机冲电ing...
结果分析:
-
因为适配器完成了目标接口的实例化(implements IVoltageDst),所以适配器实例可以作为参数传入手机类Phone的充电方法charging()。
-
因为适配器完成了源电压到目标电压的转换(220V交流电—>5V直流电),所以充电可以正常进行。
1.6 阶段小结
缺点:
- 由于需要继承src源类(被适配的类),加上Java单继承机制,导致适配目标只能为接口。
- src源类(被适配的类)中的方法都会在适配器类中暴露
优点:
- 由于继承自src源类(被适配的类),所以可重写该类中的方法,增加适配器的灵活性
2.对象适配器
2.1 简介
对象适配器基本于类适配器一致,区别是:
在类适配器中,Src源类(被适配的类)以类的形式出现在适配器中,即源类是适配器的父类。
在对象适配器中,持有src源类(被适配的类),即src源类作为适配器的属性出现。
所以适配器类只需要实现dst目标接口:实现目标接口的方法,在方法中通过调用持有src源类实例的方法以及转换操作完成适配。
2.2 对象适配器UML类图
对象适配器中的角色与类适配器中的一致,不重复写了。
UML类图略有改变,如下所示:
2.3 代码实现
需要修改适配器类:
/**
* 适配器模式
* 2.对象适配器
*
*/
public class ObjectAdapter implements IVoltageDst {
VoltageSrc voltageSrc;//持有被适配类的对象 聚合关系
//构造方法,传入被适配类的对象
public ObjectAdapter(VoltageSrc voltageSrc) {
this.voltageSrc = voltageSrc;
}
@Override
public int outputVoltageDstSize() {
return 0;
}
@Override
public String outputVoltageDstType() {
return null;
}
}
2.4 测试编码与分析
测试类也需要做些微调,Test类修改如下:
public class Test {
public static void main(String[] args) {
//类适配器测试
System.out.println("适配器模式 1.类适配器测试");
Phone phone = new Phone();
phone.charging(new ObjectAdapter(new VoltageSrc()));//传入适配器,其构造函数传入被适配类的实例
}
}
测试结果
适配器模式 2.对象适配器测试
源电压 输出大小为 220 V
适配电压大小完成,适配完后电压大小:5 V
源电压 输出类型为交流
适配电压类型完成,适配完后电压类型:直流
电压适配成功,手机冲电ing...
2.5 阶段小结
对象适配器与类适配器思想一致,只是实现方式由继承src源类(被适配的类)改为持有,利用到了合成复用原则(组合代替继承),所以解决了类适配器模式中适配器类必须继承Src源类的局限,而且不再要求dst(最终适配目标)必须是接口。
3.接口适配器
3.1 接口适配器简介
接口适配器模式也称缺省适配器模式,适用于一个类想使用一个接口中的部分方法,适配后只需实现需使用的方法即可。
3.2 接口适配器实现原理
根据上述简介的描述,如何实现的适配呢?其实就是设计一个抽象类实现原接口,每个方法都默认实现(即空实现),该抽象类就是接口适配器类。之后若只需使用原接口中的部分方法,则继承接口适配器类,便可选择性的实现所需要的方法。
下面以一个稍微具体一点的例子展现接口适配器:
原接口(被适配的接口)有4个方法:methodA(),methodB(),methodC(),methodD()
接口请求类/使用类1(ClientA)只需要使用原接口中的methodA(),methodC()
接口请求类/使用类2(ClientB)只需要使用原接口中的methodB(),methodC()
如果不使用接口适配器的话,代码如下:
原接口(被适配的接口):
/**
* 原接口 被适配的接口类
*/
public interface ISrc {
void methodA();
void methodB();
void methodC();
void methodD();
}
接口请求类/使用类1(ClientA):
/**
* 接口请求类/使用类 A
*
* 只需要使用原接口中的methodA(),methodC()
*/
public class ClientA implements ISrc {
@Override
public void methodA() {
//需要使用,重写后进行需要的操作
}
@Override
public void methodB() {
//需要使用,重写后进行需要的操作
}
@Override
public void methodC() {
//不需要使用,但依然要重写,空实现
}
@Override
public void methodD() {
//不需要使用,但依然要重写,空实现
}
}
同理,接口请求类/使用类2(ClientB):
/**
* 接口请求类/使用类 B
* <p>
* 只需要使用原接口中的methodB(),methodC()
*/
public class ClientB implements ISrc {
@Override
public void methodA() {
//不需要使用,但依然要重写,空实现
}
@Override
public void methodB() {
//需要使用,重写后进行需要的操作
}
@Override
public void methodC() {
//需要使用,重写后进行需要的操作
}
@Override
public void methodD() {
//不需要使用,但依然要重写,空实现
}
}
可看到,那些请求类不需要使用的方法,必须要重写(空实现)。随着后续请求类越多,这要重复的代码就会越来越多。而接口适配器就是消除请求类不要使用的方法的空实现,将空实现统一在一起(建立实现接口的抽象类:接口适配器类)。这样请求类就只需实现自己需要的方法即可。
3.3 代码实现
改造上述的例子,创建接口适配器类,即一个实现原接口的抽象类,每个方法都空实现:
/**
* 适配器模式
* 3.接口适配器
* 创建接口适配器类,即一个实现原接口的抽象类,每个方法都空实现
*/
public abstract class InterfaceAdapter implements ISrc {
@Override
public void methodA() {
}
@Override
public void methodB() {
}
@Override
public void methodC() {
}
@Override
public void methodD() {
}
}
这样之后,请求类只需要继承自该适配器类,之后实现所需要使用的方法即可:
修改后的 接口请求类/使用类1(ClientA):
/**
* 接口请求类/使用类 A
*
* 只需要使用原接口中的methodA(),methodC()
*/
public class ClientA extends InterfaceAdapter {
@Override
public void methodA() {
//需要使用,重写后进行需要的操作
}
@Override
public void methodB() {
//需要使用,重写后进行需要的操作
}
}
同理修改后的 接口请求类/使用类2(ClientB):
public class ClientB extends InterfaceAdapter {
@Override
public void methodB() {
//需要使用,重写后进行需要的操作
}
@Override
public void methodC() {
//需要使用,重写后进行需要的操作
}
}
3.4 阶段小结
优点:减少了大量不必要的代码,之前有大量不需使用的方法,都需空实现。使用了接口适配器抽象类后,该抽象类的子类可选择性的覆盖父类方法来实现需求。