🔥 核心
适配器模式使得原本不兼容的两个类可以合作工作。
适配器的本质是欺骗,它有着A类的外表,却有着B类的内容。
🙁 问题场景
你是一个手机制造商。最近的科技发展迅猛,产生了一个让你头疼不已的问题。
之前处于3G时代,卡很小,手机的卡槽也很小;现在进入6G时代,电话卡很大,手机的卡槽也很大。很多人会选择不断更换更先进的手机,但不愿意丢弃之前的电话卡——问题出现了,3G时代的小电话卡,怎么插入6G时代的大卡槽呢?
将旧电话卡升级改造一下?你不想重构旧电话卡的代码。
给新手机多安排几个卡槽?你也不想重构新手机的代码。
那么,如何在不改变双方代码的情况下,使其可以适配、并协作工作呢?
🙂 解决方案
适配器模式通过封装对象,将复杂的转换过程隐藏于幕后。被封装者、被封装者的使用者,甚至都感觉不到适配器的存在。这是如何实现的呢?关键就在于适配器的本质是欺骗,它有着A类的外表,却有着B类的内容。
回到上面的例子,卡槽使需要的是A类的电话卡,而你有的是B类的电话卡。没关系,你就给B类电话卡套上一个卡套,然后欺骗卡槽,这就是A类电话卡。
给B类电话卡套上卡套,就是对象的一次封装过程——封装为了一个适配器。
这个适配器(A的外表,B的内容)为什么可以放心的看作A类使用,而不会出现问题?因为在这个适配器中,已经把需要用到的A类的方法都写好了。
终于,通过这种有点儿暴力的方案,电话卡不匹配的风波终于过去了…
🌈 有趣的例子
有一种幼儿玩具——尝试将不同形状的积木,通过不同形状的孔洞,装进盒子里。
我们简化一下。这里只有一个 圆孔(RoundHole)
,两种积木,一个是 圆钉(RoundPeg)
,一个是 方钉(SquarePeg)
。
将 圆钉
装入 圆孔
的代码非常简单,比较半径即可;但是 方钉
的参数为边长而非半径。你可以写一个适配器,使得 方钉
也适配这个算法吗?
圆孔
class RoundHole {
private double radius;
public RoundHole(double radius) {
this.radius = radius;
}
public boolean fits(RoundPeg roundPeg) {
return radius >= roundPeg.getRadius();
}
}
圆钉
class RoundPeg {
private double radius;
public RoundPeg() { }
public RoundPeg(double radius) { this.radius = radius; }
public double getRadius() {
return radius;
}
}
方钉
class SquarePeg {
private double width;
public SquarePeg() { }
public SquarePeg(double width) { this.width = width; }
public double getWidth() {
return width;
}
}
适配器(方钉->圆钉)
class Adapter extends RoundPeg {
// 圆钉的外表,方钉的内容
private SquarePeg squarePeg;
public Adapter(SquarePeg squarePeg) {
this.squarePeg = squarePeg;
}
// 要想假扮成圆钉,当然要具有getRadiu方法返回半径
@Override
public double getRadius() {
return Math.sqrt(Math.pow((squarePeg.getWidth() / 2), 2) * 2);
}
}
public class AdapterPatternDemo {
public static void main(String[] args) {
// 生成一个圆孔
RoundHole roundHole = new RoundHole(5);
// 生成俩个方钉
SquarePeg squarePeg1 = new SquarePeg(4);
SquarePeg squarePeg2 = new SquarePeg(8);
// 使用适配器(圆钉的外表,方钉的内容)
Adapter adapter1 = new Adapter(squarePeg1);
Adapter adapter2 = new Adapter(squarePeg2);
// 方钉匹配圆孔
System.out.println(roundHole.fits(adapter1) ? "fits" : "not fits");
System.out.println(roundHole.fits(adapter2) ? "fits" : "not fits");
}
}
fits
not fits
☘️ 使用场景
◾️当你希望使用某个类,但是其接口与其他代码不兼容时,可以使用适配器类。
适配器模式允许你创建一个中间层类,其可作为代码与遗留类、第三方类或提供怪异接口的类之间的转换器。
◾️如果您需要复用这样一些类,他们处于同一个继承体系,并且他们又有了额外的一些共同的方法,但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。
你可以扩展每个子类,将缺少的功能添加到新的子类中。但是,你必须在所有新子类中重复添加这些代码,这样会使得代码有坏味道。
将缺失功能添加到一个适配器类中是一种优雅得多的解决方案。然后你可以将缺少功能的对象封装在适配器中,从而动态地获取所需功能。如要这一点正常运作,目标类必须要有通用接口,适配器的成员变量应当遵循该通用接口。这种方式同装饰模式非常相似。
🧊 实现方式
(1)确保至少有两个类的接口不兼容。
(一个无法修改,通常是第三方、遗留系统或者存在众多已有依赖的类。)
(一个或多个将受益于使用服务类的客户端类。)
(2)声明客户端接口,描述客户端如何与服务交互。
(3)创建遵循客户端接口的适配器类。所有方法暂时都为空。
(4)在适配器类中添加一个成员变量用于保存对于服务对象的引用。通常情况下会通过构造函数对该成员变量进行初始化,但有时在调用其方法时将该变量传递给适配器会更方便。
(5)依次实现适配器类客户端接口的所有方法。适配器会将实际工作委派给服务对象,自身只负责接口或数据格式的转换。
(6)客户端必须通过客户端接口使用适配器。这样一来,你就可以在不影响客户端代码的情况下修改或扩展适配器。
🎲 优缺点
➕ 可以让任何两个没有关联的类一起运行。
➕ 单一职责原则。你可以将接口或数据转换代码从程序主要业务逻辑中分离。
➕ 开闭原则。只要客户端代码通过客户端接口与适配器进行交互,你就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。
➖ 代码整体复杂度增加,因为你需要新增一系列接口和类。有时直接更改服务类使其与其他代码兼容会更简单。
➖ 过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。
🌸 补充
适配器不是在详细设计时添加的,而是解决正在服役的项目的问题。