一 什么是适配器模式?
1.1 定义
由于面向对象程序设计本身就是从实际生活中汲取的灵感,将大千世界抽象到程序设计领域,所以所有的设计模式都是可以在日常生活中感受的到的。例如适配器模式,这个在日常生活中就太普遍了。
例如我们程序员经常遇到的电脑上提供的端口与要插入的接头匹配不上,而我们则可以通过一个中间的适配器将两边连接起来。
这个设计模式在日常开发中出镜率极高,关键还不难,所以兄弟们一定要掌握啊。
适配器模式,顾名思义,就是把原本不兼容的接口,通过适配,使之兼容。
举个生活中简单的例子,以前的手机内存卡可以取出来,但是想和电脑之间传输音乐、视频等资料不能直接传输,需要通过 USB 读卡器,然后插入 USB 接口就可以传输了,这个 USB 读卡器就相当于适配器。
适配器模式还有个别名叫:Wrapper(包装器),顾名思义就是将目标类用一个新类包装一下,相当于在客户端与目标类直接加了一层。IT世界有句俗语:没有什么问题是加一层不能解决的。
1.2 使用场景
- 当需要使用一个现存的类,但它提供的接口与我们系统的接口不兼容,而我们还不能修改它时
- 当多个团队独立开发系统的各功能模块,然后组合在一起,但由于某些原因事先不能确定接口时(别和我说这不可能,这太 tm 可能了)
1.3 UML 类图
适配器模式中的角色:
- 目标接口(Target):客户端所期待的接口。目标可以是具体的或抽象的类,也可以是接口
- 需要适配的类(Adaptee):我们想要使用的接口与 Target 不兼容的类,它可以是一个接口,也可以是一个类
- 适配器(Adapter):适配器类,此模式的核心。它需要实现目标接口 Target,而且必须要引用 Adaptee,因为我们要在此类中包装 Adaptee 的功能
适配器模式实现方式一般分为三类:
- 类适配器模式(采用继承实现)
- 对象适配器模式(采用对象组合方式实现)
- 接口适配器模式
二 类适配器模式
一般手机充电器输出的直流电压为 5V,我们把交流电 220V 称为源,希望得到的直流电 5V 称为目标,而充电器即为适配器。
// 源,交流电
public class AC {
public int outputAC(){
return 220;
}
}
// 目标接口,直流电
public interface IDC {
public int outputDC();
}
// 适配器
public class ClsAdapter extends AC implements IDC {
@Override
public int outputDC() {
return outputAC()/44; // 直流电为交流电的电压值除以 44
}
public static void main(String[] args) {
ClsAdapter adapter = new ClsAdapter();
System.out.println("交流电电压:" adapter.outputAC());
System.out.println("直流电电压:" adapter.outputDC());
}
}
/**
输出结果为:
交流电电压:220
直流电电压:5
*/
可以看到,类适配器是通过继承源类,实现目标接口的方式实现适配的。但是,由于 Java 单继承的机制,这就要求目标必须是接口,有一定的局限性。
三 对象适配器模式
对象适配器,不是继承源类,而是依据关联关系,持有源类的对象,这也隐藏了源类的方法。在这里,适配器和源类的关系不是继承关系,而是组合关系。
public class ObjAdapter implements IDC {
// 持有源类的对象
private AC ac;
public ObjAdapter(AC ac){
this.ac = ac;
}
public int outputAC(){
return ac.outputAC();
}
@Override
public int outputDC() {
return ac.outputAC()/44;
}
public static void main(String[] args) {
ObjAdapter adapter = new ObjAdapter(new AC());
System.out.println("交流电电压:" adapter.outputAC());
System.out.println("直流电电压:" adapter.outputDC());
}
}
// 输出结果同上
四 接口适配器模式
设想,我现在的目标接口有多个方法,可以输出 5V,12V,20V 的电压。按照正常逻辑,设计一个适配器去实现这个接口,很显然需要实现所有的方法。但是,实际使用中,其实只需要使用其中一个方法就可以了,比如我 mac 电脑直流电压 20V,只需要实现 20V 的方法就可以了。
因此,设计一个中间类去把目标接口的所有方法空实现,然后适配器类再去继承这个中间类,选择性重写我所需要的方法,岂不是更好。代码如下:
// 目标接口,有多个方法
public interface IDCOutput {
public int output5V();
public int output12V();
public int output20V();
}
// 中间类,空实现所有方法,这是一个抽象类
public abstract class DefaultAdapter implements IDCOutput {
@Override
public int output5V() {
return 0;
}
@Override
public int output12V() {
return 0;
}
@Override
public int output20V() {
return 0;
}
}
// 我的mac电源适配器只需要实现 20V 的方法即可
public class MacAdatper extends DefaultAdapter {
private AC ac;
public MacAdatper(AC ac){
this.ac = ac;
}
@Override
public int output20V() {
return ac.outputAC()/11;
}
public static void main(String[] args) {
MacAdatper adatper = new MacAdatper(new AC());
System.out.println("mac电脑电压:" adatper.output20V());
}
}
// 输出结果:
// mac 电脑电压:20
至于为什么中间类使用抽象类,相信你看过我介绍的软件七大设计原则,就明白了。它需要符合里氏替换原则(尽量基于抽象类和接口的继承)。
不太明白接口适配模式的童鞋,建议看一下 JDK 里边提供的一个键盘监听适配器KeyAdapter,它就是一个抽象类,去空实现了 KeyListener 接口的所有方法。你就会感受到这种模式的奥妙。
总结:
- 类适配器模式,继承源类,实现目标接口
- 对象适配器模式,持有源类的对象,把继承关系改变为组合关系
- 接口适配器模式,借助中间抽象类空实现目标接口所有方法,适配器选择性重写
三种模式,各有优缺点,可根据实际情况选择使用。
五 总结
适配器模式的优缺点:
5.1 优点
- 通过适配器,客户端可以调用同一接口,因而对客户端来说是透明的。这样做更简单、更直接、更紧凑
- 复用了现存的类,解决了现存类和复用环境要求不一致的问题
- 将目标类和适配者类解耦,通过引入一个适配器类重用现有的适配者类,而无需修改原有代码
- 一个对象适配器可以把多个不同的适配者类适配到同一个目标,也就是说,同一个适配器可以把适配者类和它的子类都适配到目标接口
5.2 缺点
- 类适配器在 java 等单继承语言环境下,一次最多只能适配一个适配者类,不能同时适配多个适配者
- 类适配器中适配者类不能为最终类
- 类适配器中的目标抽象类只能为接口不能为类,使其使用存在了一定的局限性
- 对象适配器中置换适配者的某些方法比较麻烦