子曰:君子不器
——《论语》
定义
将一个类的接口转换成客户(client)所希望的另一个接口。适配器模式使得原本由于接口不兼容而无法一起工作的类可以一起工作
图纸
对象级别的适配器
类级别的适配器
类级别的适配器要求适配器同时是 被转换接口对象(Adaptee) 和 转换结果接口对象(Target) 的子类
严格上来说,这在Java中是不可能实现的,因为Java根本不允许多重继承
一个例子:当系统里出现奇奇怪怪的打印机
适配器模式完成了这样一个壮举:
即在不改变互不兼容的两个模块的代码的情况下,实现对接口行为的改变,使其互相兼容
也就是说,适配器模式就像一个中介,协同两个互不相关甚至互相排斥又不能修改的模块,以求实现这样的效果:
这次的例子会有点怪,但他改编自我自己跟过的一个真实项目,我会尽量把他简化并讲清楚
准备好了?那我们开始了:
假定现在我们的系统有两个业务是这样的:
- 用户输入数据,后台会把数据组合成数据bean A,然后连接打印机,打印带二维码的标贴
- 用户输入数据,后台会把数据组合成数据bean B,然后连接打印机,打印方框+文字的标贴
已知,跟我们合作的打印机厂商 X ,给我们提供了一份用于调用 X 型号打印机的 API,这份 API 中定义了一个名为 XPrinterDriver
的类,通过创建这个类的对象,我们就可以连接操作 X 型号的打印机
第一次实现
于是乎,为了实现这样的需求,我们有了这样的类簇:
public abstract class Printer<E>{
//用于调用X打印机的driver对象
private XPrinterDriver driver;
//初始化driver 略
protected void getDriver(){
return driver;
}
public abstract void print(E e);
}
public class Printer_A extends Printer<A>{
public void print(A a){
getDriver().printQCCode();//打印二维码
}
}
public class Printer_B extends Printer<B>{
public void print(B b){
getDriver().printBlock();//打印方框
getDriver().printText();//打印文字
}
}
这个类簇是可以正常工作的,而且调用他的方式也很简单
- 在业务A中,我们会创建 Printer_A 的对象并用她打印标贴
- 在业务B中,我们会创建 Printer_B 的对象并用她打印标贴
自始至终,client代码 不会和 XPrinterDriver 有任何交集
被迫改变
天有不测风云,某天我们接到通知,说X倒闭了,我们后续会和Y打印机厂商合作
Y打印机厂商给我们提供了他们的打印机API,里面有个 YPrinterDriver
可以用来调用 Y 型号的打印机
但是已经买的X打印机不可能直接全报废
这就意味着现在系统里面会同时存在两种打印机的调用代码,而且这两种打印机是完全不兼容的
我当然可以像创建 X打印机 的调用类簇一样,再创建一套 Y打印机 的调用类簇,就像这样:
但是这就意味着每次新增打印业务,我都 必须要为两种打印机编写各自的绘制代码
我是个特别懒的人,我绝不允许这种情况的发生
重构
于是乎,我们要来重构上文的类簇
先把改变的部分和不变的部分拆开来
我们发现业务生成各自的bean对象,并调用打印机所绘制的标贴图案是不变的。改变的是具体画内容的方法,比如 printQCCode(绘制二维码)、printBlock(绘制方框)和printText(绘制文本)
如果我们可以把他们拆开来,不要让Printer直接调用厂家的PrinterDriver里的方法,而是有一个通用的画图接口,那我们就可以只写一次代码
就像这样:
public interface Delineator<E>{
void print(PrinterDriver driver,E e);
}
public class Delineator_A implements Delineator<A>{
public void print(PrinterDriver driver,A a){
driver.printQCCode();//打印二维码
}
}
public class Delineator_B implements Delineator<B>{
public void print(PrinterDriver driver,B b){
driver.printBlock();//打印方框
driver.printText();//打印文字
}
}
public class PrinterDriver{
private XPrinterDriver xDriver = 初始化;
public void printQCCode(){
xDriver.printQCCode();//x型号的打印机打印二维码
}
public void printBlock(){
xDriver.printBlock();//x型号的打印机打印方框
}
public void printText(){
xDriver.printText();//x型号的打印机打印文字
}
}
我们定义了 Delineator(绘图器)
接口,用来表示业务层绘制标贴的逻辑;把直接调用打印机的动作,全部集中到 PrinterDriver(打印机驱动)
这个类中。从而实现了 业务层的绘制标贴 和 打印机硬件层的绘制标贴 的解耦
,无论以后用什么打印机,都不会影响业务层的绘制标贴代码
但是现在还有一个问题,那就是 打印机硬件层的绘制标贴 依然是硬编码,在加入 Y类型打印机 的情况下,我依然需要两种不同的 PrinterDriver 对象
现在,我要整合 X型号打印机 和 Y型号打印机 接口
对,你没听错,我要整合这两种完全不兼容的API接口。这听起来不可能,但是用适配器能做到,就像这样:
public abstract class PrinterDriverAdapter{
public static final int DRIVER_X_MODEL = 0;//用于切换成X打印机
public static final int DRIVER_Y_MODEL = 1;//用于切换成Y打印机
private static int nowModel = 0;//默认使用X打印机
private static final PrinterDriverAdapter[] driverArray = new PrinterDriverAdapter[2];
//返还当前用户设定的打印机驱动 不需要考虑多线程,因为外部要保证单线程调用
public static PrinterDriver getPrinterDriver(){
PrinterDriver driver = driverArray[nowModel];
if(driver == null){
初始化,并赋值
}
return driver;
}
public static void setNowModel(int model){
switch(model){
case DRIVER_X_MODEL:
case DRIVER_Y_MODEL:
nowModel = model;
return;
default:
抛出异常
}
}
public abstract void printQCCode();
public abstract void printBlock();
public abstract void printText();
}
//X型号打印机
protected class PrinterDriverAdapter_X extends PrinterDriverAdapter{
private XPrinterDriver xDriver = 初始化;
public void printQCCode(){
xDriver.printQCCode();
}
public void printBlock(){
xDriver.printBlock();
}
public void printText(){
xDriver.printText();
}
}
//Y型号打印机
protected class PrinterDriverAdapter_Y extends PrinterDriverAdapter{
private YPrinterDriver yDriver = 初始化;
public void printQCCode(){
yDriver.printQCCodeY();
}
public void printBlock(){
yDriver.printBlockY();
}
public void printText(){
yDriver.printTextY();
}
}
我们通过 PrinterDriverAdapter(打印机驱动适配器)
这个类,做了两件事
- 我们实现了对
PrinterDriverAdapter_X
和PrinterDriverAdapter_Y
这两个类的单例管理 - 我们让 X型号的API 和 Y型号的API 实现了统一化
最终,我们得到的整个结构,就会变成这样:
因为 getPrinterDriver 方法的存在,所以我们不再需要在调用 Delineator.print 的时候传入驱动
Delineator 只和 PrinterDriverAdapter 打交道,甚至不用关心自己到底是在调用哪种类型的打印机在执行最终的打印指令
-
如果新的业务需要打印标贴的功能,那么我们会新增 Delineator 的子类
-
如果新增打印机供应商,我们会新增 PrinterDriverAdapter 的子类
完全不需要修改已有的子类
而这是一个标准的 对象级适配器模式 实现
适配器的复杂程度
适配器的复杂程度完全取决于你要转换的接口的复杂程度,上文只是截取了一小部分。事实上还有诸如 开始打印、连接打印机、分页、取消打印 等等方法。这些方法在适配器中都要体现出来
类级别的适配器
Java无法展示类级别的适配器模式,因此在这里略过
但是思路是一样的,无非是类级别的适配器同时是转换方和被转换方的子类而已,这样更方便双向适配器的实现
写在最后的碎碎念
适配器和装饰器、外观
适配器和 装饰者模式、外观模式 都是可以改变接口最终行为的,很神奇的结构性设计模式
区别在于
- 适配器关注的是接口之间的转换
- 装饰者模式会保留接口原来的行为,而对接口所做的事情上增加些内容(如果你接触过AOP,一定不陌生)
- 外观模式实现了对接口的组合,他把N个方法像字段一样包装起来
装饰器模式和外观模式将会在后文中陆续登场,敬请期待
AOP
面向切面编程
上游模块,下游模块
你一定发现了,我们用到适配器的情况都是在一个流程中,存在不兼容甚至截然相反的两个模块。而这两个模块以适配器为分界线,我将其称为 上游模块 和 下游模块
在上文的打印机适配器实现中,上下游模块都是绘制标贴的 行为
但是也有一些适配器模块实现了把不同类型的模块进行转换的壮举
比如如果我们要编写一个 统计图模块
- 上游模块会是一份 待数据可视化的数据
- 下游模块会是各种 图表绘制器(比如柱状图绘制器、折线图绘制器……)
这时候你就需要一个适配器,可以把这份数据转换成柱状图,折线图。但是这两个模块一个是 数据,一个是 视图
这样的设计是有意义的,除了方便应对不断拓展的绘制策略之外。还可以实现对图表的动态切换
打个比方:
在运行的过程中,你做了一个允许用户切换图表类型的按钮。那么你就可以通过在点击按钮时切换不同的适配器来实现这样的效果
双向适配器
当上下游模块是非固定的,适配器既允许把A的接口转换成B,也允许从B转换成A。那么我们说这个适配器是双向的
这种适配器一般都是类级别适配器,Java里基本是见不到的(因为Java中的适配器只能是其中一个模块的子类)
那你会说,不对啊,虽然Java单继承,但是不是可以用接口进行拓展吗?
是这样的,但是适配器存在的意义,是对两个已有实现的模块进行嫁接,如果让其中一个模块是接口不提供实现,那何必还用适配器呢,直接实现不就好了
适配器和翻译官
适配器模式就像是一个翻译官一样,他接收A所说的话,转换成B所能理解的语言。可能当A和B打得火热的时候会忘记适配器这个翻译官的存在,但是翻译官是不可或缺的,否则双方都不知道对方所要表达的意思
在你的生活里一定也有像适配器一样的人。他不善言表,从不强调自己做了什么,只是埋头苦干。他往往不是团队的焦点,甚至存在感低到你有时会忽略他的存在。可当你回首往事,你会发现他所作的事是润物细无声的;他从不需要言语来论述自己的伟大,时间会证明他存在的价值
万分感谢您看完这篇文章,如果您喜欢这篇文章,欢迎点赞、收藏。还可以通过专栏,查看更多与【设计模式】有关的内容