此系列文章为清华大学出版社出版刘伟编著《Java设计模式》的学习笔记。
1 概述
为了让手机等电子产品与家庭用电的 220V 交流电兼容适配,我们设计了电源适配器(AC Adapter)。在软件开发中也存在类似的不兼容情况,也可以引入类似电源适配器的角色来协调这些存在不兼容的结构,这种设计方案即为适配器模式。
在适配器模式中,我们引入一个被称为适配器(Adapter)的包装类,而它所包装的对象称为适配者(Adaptee),即被适配的类。适配器的实现就是把客户类的请求转化为对适配者的相应接口的调用。也就是说,当客户类调用适配器的方法时在适配器类的内部将调用适配者类的方法,而这个过程对客户类是透明的,客户类并不直接访问适配者类。因此,适配器让那些由于接口不兼容而不能交互的类可以一起工作。
适配器模式可以将一个类的接口和另一个类的接口匹配起来,而无需修改原来的适配者接口和抽象目标类接口。
适配器模式:将一个类的接口转换成客户希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作。
2 结构与实现
2.1 适配器模式结构
- Target(目标抽象类):目标抽象类定义客户所需要的接口,可以是一个抽象类或接口,也可以是具体类。在类适配器中,由于 Java 语言不支持多重继承,它只能是接口。
- Adapter(适配器类):它可以调用另一个接口,作为一个转换器,对 Adaptee 和 Target 进行适配。适配器 Adapter 是适配器模式的核心,在类适配器中,它通过实现 Target 接口并继承 Adaptee 类来使得二者产生联系,在对象适配器中,它通过继承 Target 并关联一个 Adaptee 对象使二者产生联系。
- Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下甚至没有适配类的源代码。
2.2 系统结构 UML
2.2.1 类适配器 UML
2.2.2 对象适配器 UML
2.3 适配器模式举例
一、背景介绍
某公司要开发一款儿童玩具汽车,为了更好的吸引小朋友的注意力,该玩具汽车在移动工程中伴随着灯光闪烁和声音提示。在该公司的以往产品中已经实现了控制灯光闪烁(例如警灯闪烁)和声音提示(例如警笛音效)的程序,为了重用先前的代码并且使汽车控制软件具有更好的灵活性和扩展性,现使用适配器模式设计该玩具汽车控制软件。
二、项目结构
三、抽象目标
public abstract class CarController {
public void move(){
System.out.println("玩具汽车移动!");
}
public abstract void phonate();//发出声音
public abstract void twinkle();//灯光闪烁
}
四、适配者
- 警灯适配者
public class PoliceLamp {
public void alarmLamp() {
System.out.println("发出警灯闪烁!");
}
}
- 警笛适配者
public class PoliceSound {
public void alarmSound(){
System.out.println("发出警笛声音!");
}
}
五、适配器
import StructuralPattern.AdapterPattern.Adaptee.PoliceLamp;
import StructuralPattern.AdapterPattern.Adaptee.PoliceSound;
import StructuralPattern.AdapterPattern.Target.CarController;
public class PoliceCarAdapter extends CarController {
private PoliceSound sound;
private PoliceLamp lamp;
public PoliceCarAdapter() {
sound = new PoliceSound();
lamp = new PoliceLamp();
}
@Override
public void phonate() {
sound.alarmSound();
}
@Override
public void twinkle() {
lamp.alarmLamp();
}
}
六、config.xml
<?xml version="1.0" encoding="UTF-8"?>
<config>
<className>StructuralPattern.AdapterPattern.Adapter.PoliceCarAdapter</className>
</config>
七、XMLUtil 解析工具类
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
public class XMLUtil {
public static Object getBean(){
try {
//todo:创建 DOM 文档对象
DocumentBuilderFactory documentBuilderFactory= DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document doc;
doc = documentBuilder.parse("src\\StructuralPattern\\AdapterPattern\\config.xml");
//todo:获取包含类名的文本节点
NodeList nodeList = doc.getElementsByTagName("className");
Node classNode = nodeList.item(0).getFirstChild();
String cName = classNode.getNodeValue();
//todo:通过类名生成实例对象并将其返回
Class clz = Class.forName(cName);
Object obj = clz.newInstance();
return obj;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
八、客户类
import StructuralPattern.AdapterPattern.Target.CarController;
public class Client {
public static void main(String[] args) {
CarController car;
car = (CarController)XMLUtil.getBean();
car.move();
car.phonate();
car.twinkle();
}
}
九、测试结果
3 缺省适配器模式
3.1 概述
缺省适配器模式是适配器模式的一种变体,其应用也较为广泛。
缺省适配器模式:当不需要实现一个接口提供的所有方法时,可先设计一个抽象类实现该接口,并为接口中的每一个方法提供一个默认实现(空方法),那么该抽象类的子类可以选择型地覆盖父类的某些方法来实现需求,它适用于不想使用一个接口中的所有方法的情况,又称为单接口适配器模式。
3.2 缺省适配器结构
缺省适配器模式包含三个角色:
- ServiceInterface(适配者接口):它是一个接口,通常在该接口中声明了大量的方法。
- AbstractServiceClass(缺省适配器类):它是缺省适配器模式的核心类,使用空方法的形式实现了在 ServiceInterface 接口中声明的方法。通常将它定义为抽象类,因为对它进行实例化没有任何意义。
- ConcreteServiceClass(具体业务类):它是缺省适配器类的子类,在没有引入适配器之前它需要实现适配者接口,因此需要实现在适配者接口中定义的所有方法,而对于一些无需使用的方法不得不提供空实现。在有了缺省适配器之后可以直接继承该适配器类,根据需要有选择性地覆盖在适配器类中定义的方法。
其中,缺省适配器类的经典代码片段如下:
public abstract class AbstractServiceClass implements ServiceInterface{
public void serviceMethod1(){}//空方法
public void serviceMethod2(){}//空方法
public void serviceMethod3(){}//空方法
}
4 双向适配器
在对象适配器的使用过程中,如果在适配器中同时包含对目标类和适配者类的引用,适配者可以通过它调用目标类中的方法,目标类也可以通过它调用适配者类中的方法,那么该适配器就是一个双向适配器。
双向适配器实现复杂,经典代码如下:
public class Adapter implements Target, Adaptee{
//同时维持对抽象目标类和适配者的引用
private Target target;
private Adaptee adaptee;
public Adapter(Target target){
this.target = target;
}
public Adapter(Adaptee adaptee){
this.adaptee = adaptee;
}
public void request(){
adaptee.specificRequest();
}
public void specificRequest(){
target.request();
}
}
5 总结
5.1 适配器模式优点
- 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无需修改原有结构。
- 增加了类和透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用。
- 灵活性和扩展性都非常好,通过使用配置文件可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合开闭原则。
具体来说,类适配器模式还有以下优点:
由于适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强。
对象适配器模式还有以下优点:
- 一个对象适配器可以把多个不同的适配者适配到同一个目标。
- 可以适配一个适配者的子类,由于适配器和适配者之间的关联关系,根据里氏替换原则,适配者的子类也可以通过该适配器进行适配。
5.2 适配器模式缺点
- 对于 Java 、C# 等不支持多重类继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配者。
- 适配者类不能为最终类,例如在 Java 中不能为 final 类。
- 在 Java、C# 等语言中,类适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性。
对象适配器的缺点如下:与类适配器模式相比,在该模式下要在适配器中置换适配者类的某些方法比较麻烦,如果一定要置换适配者类的一个或多个方法,可以先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当成真正的适配者进行适配,实现过程较为复杂。
5.3 适配器模式适用环境
- 系统需要使用一些现有的类,而这些类的接口(例如方法名)不符合系统的需要,甚至没有这些类的源代码。
- 想创建一个可以重复使用的类,用于和一些彼此之间没有太大关联的类(包括以下可能在将来引进的类)一起工作。