设计模式学习笔记(九):适配器模式
1 概述
1.1 引言
有的笔记本电脑工作电压为20V,而我国家庭用电为220V,如何让20V的笔记本在220V的电压下工作?答案就是引入一个电源适配器,有了这个电源适配器笔记本就能在220V的电压下工作。
在软件开发中,有时也会存在这类不兼容的状况,需要引入一个像电源适配器这样的称之为适配器的角色来协调这些不兼容的结构,这种设计方案就是适配器模式。
1.2 定义
将一个接口转换为客户希望的另一个接口,使接口不兼容的那些类可以一起工作,别名为包装器。
适配器中的接口是广义的接口,可以表示一个方法或者方法的集合。
适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。
1.3 分类
根据适配器与适配者类的关系不同,可以分为对象适配器模式以及类适配器模式。
1.3.1 对象适配器模式
对象适配器模式就是适配器与适配者之间是关联关系。
结构图如下:
1.3.2 类适配器模式
类适配器模式就是适配器与适配者之间是继承或实现关系。
结构图如下:
由于语言特性的限制,比如Java,C#不支持多重继承,类适配器模式受到很多限制,例如Target如果不是接口而是一个类,就无法使用类适配器模式。此外如果适配者为final
类也无法使用适配器模式,在Java等语言中大部分情况下使用对象适配器模式。
1.4 角色
- Target(目标抽象类):目标抽象类定义客户所需的接口,可以是一个抽象类或接口,也可以是一个具体类
- Adapter(适配器类):适配器可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配。适配器类是适配器模式的核心,在对象适配器模式中,它通过继承Target并关联一个Adaptee对象使两者产生联系,在类适配器模式,通过继承Adaptee并实现Target使两者产生联系
- Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码
2 典型实现
2.1 步骤
- 定义目标抽象类:接口/抽象类/具体类,客户端需要的接口,比如上面电源的例子,目标类就是给笔记本充电
- (可选)定义适配者类:定义适配者类,但是一般来说适配者已经存在,比如上面电源的例子,适配者类就是220V的电压,而且对于某些库来说可能没有适配者的源码
- 定义适配器类:继承/实现目标抽象类,并通过转发请求到适配者来完成操作
2.2 目标抽象类
这里实现为具体类:
class Target
{
public void request()
{
System.out.println("Target方法");
}
}
2.3 适配者类
适配者一般为具体类,但是很多情况下已经存在并且没有源码实现:
class Adaptee
{
public void specificRequest()
{
System.out.println("Adaptee方法");
}
}
2.4 适配器类
2.4.1 对象适配器
对象适配器种适配器与适配者是关联关系,适配器中包含一个适配者成员,代码如下:
class Adapter extends Target
{
private Adaptee adaptee = new Adaptee();
@Override
public void request()
{
adaptee.specificRequest();
}
}
适配器覆盖目标抽象类的request
,并将请求转发,交由适配者完成。
2.4.2 类适配器
类适配器中适配器与适配者是继承关系,其中适配者为父类,适配器为子类。但是在Java中由于不支持多重继承,因此想要在Java中实现类适配器模式,并且如果适配者是具体类的话,那么必须将目标抽象类指定为接口:
interface Target
{
void request();
}
class Adaptee
{
public void specificRequest()
{
System.out.println("Adaptee方法");
}
}
class Adapter extends Adaptee implements Target
{
@Override
public void request()
{
super.specificRequest();
}
}
在上述对象适配器的基础上,将目标抽象类修改为接口,同时适配器继承了适配者并实现了Target,并取消了适配者作为成员变量,在方法内直接调用super.xxx
,也就是适配者的方法。
2.5 客户端
客户端的代码很简单,针对目标抽象类进行编程:
public static void main(String[] args)
{
Target adapter = new Adapter();
adapter.request();
}
3 实例
假设目前只有一条Micro USB线以及一台只有Type-C接口的手机,需要对其进行充电,这时候就需要一个转接头把Micro USB转为Type-C接口,才能给手机充电,使用适配器模式对其进行设计。
设计如下:
- 目标抽象类:
TypeC
- 适配者类:
MicroUSB
- 适配器:
MicroUSBToTypeC
简化实现代码如下:
public class Test
{
public static void main(String[] args) {
TypeC typeC = new MicroUSBToTypeC();
typeC.chargeWithTypeC();
}
}
//Target:给TypeC接口的手机充电
interface TypeC
{
void chargeWithTypeC();
}
//Adaptee:适配者,MicroUSB线
class MicroUSB
{
public void chargeWithMicroUSB()
{
System.out.println("MicroUSB充电");
}
}
//Adapter:适配器,MicroUSB到TypeC的转接头
class MicroUSBToTypeC implements TypeC
{
private MicroUSB microUSB = new MicroUSB();
@Override
public void chargeWithTypeC()
{
microUSB.chargeWithMicroUSB();
}
}
4 双向适配器
在对象适配器的使用过程中,如果在适配器中同时包含对Target类和Adaptee类的引用,Adaptee类可以通过适配器调用Target类中的方法,Target类也可以通过适配器调用Adaptee类的方法,那么该适配器就是一个双向适配器。例子如下:
public class Test
{
public static void main(String[] args) {
Adapter adapter = new Adapter();
adapter.request();
adapter.specificRequest();
}
}
//适配者
interface Adaptee
{
void specificRequest();
}
//Target类
interface Target
{
void request();
}
//Target实现
class TargetImpl implements Target
{
@Override
public void request()
{
System.out.println("Target方法");
}
}
//适配者实现
class AdapteeImpl implements Adaptee
{
@Override
public void specificRequest()
{
System.out.println("Adaptee方法");
}
}
//适配器
class Adapter implements Adaptee,Target
{
private Target target = new TargetImpl();
private Adaptee adaptee = new AdapteeImpl();
@Override
public void request()
{
//Target的方法调用适配者方法
adaptee.specificRequest();
}
@Override
public void specificRequest()
{
//适配者方法调用Target的方法
target.request();
}
}
5 缺省适配器
5.1 定义
缺省适配器:当不需要实现一个接口所提供的所有方法时,可先设计一个抽象类实现该接口,并为接口中的每个方法都提供一个默认实现(空实现),那么该抽象类子类可以选择性覆盖父类的某些方法来实现需求,它适用于不想使用一个接口中所有方法的情况,又叫单接口适配器模式。
5.2 结构图
5.3 角色
ServiceInterface
(适配者接口):通常是一个声明了大量方法的接口AbstractServiceClass
(缺省适配器类):缺省适配器模式的核心类,使用空方法的形式实现了在ServiceInterface接口中声明的方法,通常定义为抽象类ConcreteServiceClass
(具体业务类):是缺省适配器类的子类,只需要有选择性地覆盖适配器者中定义的方法,其他的方法在缺省适配器类中提供了空实现
5.4 实例
Java AWT中一般可以通过两种方式来处理窗口事件:
- 实现
WindowListener
- 继承
WindowAdapter
其中WindowAdapter
实现了WindowListener
接口,但是都是提供了空实现,也就是说实现WindowsListener
的话需要实现里面所有的方法,而继承WindowAdapter
只需要选择性地覆盖方法即可,结构图:
6 主要优点
类适配器以及对象适配器的共同优点如下:
- 解耦:将Target与Adaptee解耦,引入适配器来重用现有的适配者类,无须修改原有结构
- 提高复用性:将具体的业务实现过程封装在适配者类中,对于客户端而言是透明的,而且提高了适配者类的复用性,同一个适配者类可以在多个不同的系统复用
- 扩展性好:可以很方便地更换适配器,也可以在不修改代码的基础上增加了新的适配器类,完全符合开闭原则,扩展灵活
类适配器的独有优点如下:
- 由于适配器类是适配者的子类,因此在适配器类中置换一些适配者的方法,使得适配器的灵活性更强。
对象适配器的独有优点如下:
- 一个对象适配器可以把多个不同的适配者适配到同一个Target
- 可以适配一个适配者的子类,由于适配器与适配者之间是关联关系,根据LSP(里氏代换原则),适配者的子类也可以通过该适配器进行适配
7 主要缺点
类适配器缺点:
- 对于Java,C#等不支持多重继承的语言,一次最多只能适配一个适配者类
- 适配者不能是“不能继承的类”,比如Java的
final
类,C#的sealed
类 - 在Java,C#等Target只能是接口不能是类
对象适配器缺点:
- 置换麻烦:相比起类适配器,在适配器中置换适配者的某些方法比较麻烦,需要先创建一个适配者类的子类,在子类将适配者类的方法置换掉,再把适配者的子类作为真正的适配者类进行适配,实现较为复杂
8 适用场景
- 系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需求,甚至没有这些类的源代码
- 想创建一个可以重复使用的类,用于与彼此之间没有太大关联的类,包括可能在将来引进的类一起工作