我们都见过电脑的电源适配器,它的作用就是将 220V 的家用交流电通过变压器变压,再通过整流器变直流,这样就得到了电脑可用的 12V 直流电。在这个过程中适配器就是将不可用、不合适的 220V 交流电转变为可用的 12V 直流电供电脑使用。
在软件开发过程中也会出现类似的情况。
假如你在开发一款股价走向预测软件,它会从不同来源下载 XML 格式的股票数据,然后向用户呈现美观的图表。
在开发过程中,你决定在程序中整合一个第三方的智能分析程序库,但是有个问题,这个程序库只兼容 JSON 格式的数据。
你可能无法修改分析函数库的代码,因为它可能正被其他代码引用,此外更糟糕的是你可能也没有它的源码。
此时,只兼容 JSON 的分析函数库就相当于 220V 的交流电,而你的应用程序就像只支持 10V 直流电的电脑,此时你需要一个电源适配器,通过适配器将只兼容 JSON 的分析函数库转变为可支持 XML 的函数库。
适配器模式解析
通过刚才的描述,相信大家都对适配器模式的作用有了一定的概念。适配器能将一个类的接口转变为另一个客户端想要的接口,使得原本不能一起工作的接口能一起工作。
根据适配器类和适配者类的关系,适配器模式分为类适配器和对象适配器。
适配器模式包含三个角色:适配者、适配器、目标接口。
- 适配者:已存在的接口,包含了客户端想要使用的业务接口,常见于第三方 jar 包,没有源码,类比于 220V 交流电;
- 目标接口:是客户所需要的接口,类比于电脑;
- 适配器:作为一个转换器,通过继承或引用适配者,把适配者接口转换成目标接口,供客户端访问,类比于电源适配器。
适配器模式的使用场景
那么哪些情况下适用适配器模式呢?
当你希望使用某个类,但是其接口与其他代码不兼容时,可以使用适配器类。
举个栗子:
可以参考上面股票分析软件和第三方智能分析程序库,他俩本身不能一起工作,但是通过一个适配器,就可以做到了。
当你想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些类不一定有一致的接口。
举个栗子:
意思就是通过一个类 A,他来提供一个兼容性比较高的外观,通过适配器的作用,来使其他类 B、C、D 能一起对外提供服务。
通过接口转换,将一个类插入另一个类系中。
举个栗子:
比如有一个鸭子类,还有一个火鸡类,现在我们要把火鸡加入到鸭子中,这样我们就得到了一只看起来像鸭子的火鸡。
对象适配器
对象适配器的实现方式为适配器实现目标接口,并且依赖于适配者。
下面为对象适配器的类图。
接下来用一个小小的例子来感受一下:
public class RoundHole {
private double radius;
public RoundHole(double radius){
this.radius = radius;
}
public void fits(RoundNail roundNail){
if(radius >= roundNail.getRadius()){
System.out.println("半径为"+roundNail.getRadius()+"的钉子和半径为"+radius+"洞可以契合");
}else{
System.out.println("半径为"+roundNail.getRadius()+"的钉子和半径为"+radius+"洞不能契合");
}
}
}
public class RoundNail {
private double radius;
public RoundNail(double radius) {
this.radius = radius;
}
public RoundNail(){
}
public double getRadius() {
return radius;
}
public void setRadius(double radius) {
this.radius = radius;
}
}
public class SquareNail {
private double width;
public SquareNail(double width) {
this.width = width;
}
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
}
public class NailAdapter extends RoundNail {
private SquareNail squareNail;
public NailAdapter(SquareNail squareNail) {
this.squareNail = squareNail;
}
@Override
public double getRadius(){
double width = squareNail.getWidth();
double radius = width/2 * Math.sqrt(2);
return radius;
}
}
public class Client {
public static void main(String[] args) {
SquareNail squareNail1 = new SquareNail(4);
SquareNail squareNail2 = new SquareNail(8);
NailAdapter adapter1 = new NailAdapter(squareNail1);
NailAdapter adapter2 = new NailAdapter(squareNail2);
RoundHole roundHole = new RoundHole(4);
roundHole.fits(adapter1);
roundHole.fits(adapter2);
}
}
输出:
半径为 2.8284271247461903 的钉子和半径为 4.0 洞可以契合
半径为 5.656854249492381 的钉子和半径为 4.0 洞不能契合
这个例子就是 SquareNail 类和 RoundHole 类不能兼容,因为 RoundHole 只支持 RoundNail,RoundHole 与 SquareNail 不能一起工作,所以使用了适配器将 SquareNail 转变为了 RoundNail,这样使得 RoundHole 与 SquareNail 能一起工作。
对象适配器的优点:
- 一个对象适配器可以把多个不同的适配者适配到同一目标接口
- 可以适配一个适配者的子类,根据“里氏替换原则”,适配者的子类也可以通过该适配器进行适配
类适配器
类适配器的实现方式为适配器继承适配者,并且实现目标接口。
下面为类适配器的类图:
类适配器模式的优点:
- 可以在适配器类中重写一些适配者的方法,使得适配器的灵活性更强
类适配器模式的缺点:
- 对于不支持多重继承的语言,一次最多只能适配一个适配者类;
- 适配者类不能为最终类;
- 在 Java、C# 等语言中,类适配器模式中的目标抽象类只能为接口,不能为类。
缺省适配器
缺省适配器是适配器模式的一种变体,应用也较为广泛。
实现方式为先设计一个声明大量方法的接口 A,然后设计一个抽象类 B 实现接口 A,并使用空方法的形式实现接口 A 中的方法,是缺省适配器模式的核心类,最后再设计一个具体业务类 C,他继承于抽象类 B,可以有选择性的覆盖 B 中的方法。
下面为缺省适配器模式的类图:
将枚举适配到迭代器
我们经常会发现有些遗留的代码会返回一个枚举器接口 Enumeration,但是我们想新的代码中只使用迭代器接口 Iterator,现在动手试试构造一个适配器解决这个问题。
目标接口 Iterator 如下:
适配者接口 Enumeration 如下:
编写一个 EnumeratorIterator 适配器:
public class EnumeratorIterator implements Iterator {
private Enumeration enumeration;
public boolean hasNext() {
return enumeration.hasMoreElements();
}
public Object next() {
return enumeration.nextElement();
}
public void remove() {
throw new UnsupportedOperationException();
}
}
适配器模式与其他模式的关系和区别
桥接模式通常会于开发前期进行设计,使你能够将程序的各个部分独立开来以便开发。而适配器模式通常在已有程序中使用,让相互不兼容的类能很好地合作。
适配器可以对已有对象的接口进行修改,装饰模式则能在不改变对象接口的前提下强化对象功能。此外,装饰还支持递归组合,适配器则无法实现。
适配器能为被封装对象提供不同的接口,代理模式能为对象提供相同的接口,装饰则能为对象提供加强的接口。
外观模式为现有对象定义了一个新接口,适配器则会试图运用已有的接口。适配器通常只封装一个对象,外观通常会作用于整个对象子系统上。
桥接、状态模式和策略模式(在某种程度上包括适配器) 模式的接口非常相似。实际上,它们都基于组合模式——即将工作委派给其他对象,不过也各自解决了不同的问题。模式并不只是以特定方式组织代码的配方,你还可以使用它们来和其他开发者讨论模式所解决的问题。