前言
在上一篇文章一次性搞懂设计模式–迭代器模式中,我们已经初步的了解了设计模式中一些最基本的思想。实际上,设计模式是大佬们对于开发过程中常见问题的通用解决方案。学习设计模式能够提升编程内功,灵活应用设计模式在代码中,有助于帮助我们开发更加灵活,易拓展的系统。
适配器(Adapter)模式
OK,现在言归正传,接下来简述一下适配器模式的核心思想:
在不同的代码中,经常会存在现有的程序无法直接使用,需要做适当的变换后才能使用的情况。适配器模式的作用就是连接现有程序和所需程序。好像这个描述有点抽象,那举一个实际一点的例子
这是一个我们常用的插头
这是一个插座
这个插头和插座可以很简单的匹配上right?假如现在情况发生改变,我们发现这种的插座有着无法避免的安全隐患(比如容易着火)。因此我们迫不得已只能使用另一种插座
现在场景假设已经完成,让我们来思考一下,如果大规模发生这种情况应该怎么解决(可以极端一点假设所有的插座都已经被更换了)。显然,最彻底的解决方法就是将所有电器的插头都更换成能够适配新插座的形状,但是如果这样做,我们原先已有的电器就只能全部抛弃或者是全部进行更新,这有时是无法接受的。有没有什么这种一点的方案呢?还真有(万能的淘宝总是能帮你解决许多问题)
只需要购买一个转换接头我们就可以使用较低的成本解决接口不匹配的问题。在设计模式中,我们管转换接头叫做适配器。
现在回过头来思考一下适配器的定义是不是就很容易理解了,相信这个时候你已经可以理解适配器模式的作用就是连接现有程序和所需程序这句话了,那么我们就可以进入适配器模式的实现细节。
适配器模式类图
对于面向对象的编程语言,适配器模式有两种比较常见的实现方式:
-
类适配器模式(使用继承)
-
对象适配器模式(使用组合)
同样,我们先通过类图来从整体上看看适配器模式的构造
类适配器模式
部分同学可能不理解Target接口的作用,为什么Client不直接调用Adapter而是要通过调用Target接口。实际上,调用接口有助于实现拓展性更强的系统。通常我们会使用接口定义一系列规范,通过接口可以利用多态来实现更加灵活的拓展功能。简单理解,我们可以理解接口定义了新的插座是什么形状的,电压是多少,有多大电流,以及一系列标准。而Adapter可以理解是不同的厂商根据接口中定义的标准生产的插座,不管是什么厂商生产的插座有一定符合接口中定义的标准,各个厂商还可以给插座加上各种新功能,比如定时开关等等。但是对于我们这些使用者来说,我们暂时只想使用最基本的插头转换功能,而不需要了解那些细节。以上是忽然想到的一些题外话,大家可以选择性的阅读,如果能够对大家有所帮助也算是不负使命。
如果想要直接学习类适配器的代码实现,可以直接跳至下一节的代码细节。当然,如果觉得对于以上内容能够较好的理解吸收,也可以按顺序阅读接下来的内容,实际上对象适配器模式与类适配器并没有实质上的区别,只是在实现细节上有着细微的变化。
对象适配器模式
哈哈哈,是不是很像,对象适配器只是将适配器与被适配对象的关系由继承变为了组合而已,至于二者之间各有何优劣,这也是一个值得探讨的问题,关于设计模式中多用组合少用继承的讨论,大家可以在一文搞懂设计模式–外传之为什么要使用设计模式中的合成复用原则部分找到答案
适配器模式中出现的角色
在适配器模式中,主要存在四个角色
Client(请求者)
即需要使用适配器的对象,对应到上文转换接头的例子中,就是需要使用转换接头的人。Client希望能够通过调用定义好的Target接口就能够完成他希望的功能,而不需要关心功能实现真实细节。在上文例子中,作为要使用插头的人,我希望的是能够获得已经经过转换好的插头,直接往插座上一插就能够使用,而不会想要知道电压或者电流(具体细节)在旧插头和转换插头之间经过了怎样复杂的变化。
Target(目标)
与其说它是目标不如说Target定义了Client所要使用的功能,是一种标准,而这个标准的具体实现可以交由不同的人采用不同的方法进行实现,但是这些具体实现都应该符合Target中定义的标准。
Adapter(适配器)
负责真实实现转换功能的对象,同时其功能的实现需要符合Target中定义的标准。
Adaptee(被适配的对象)
需要被适配器转换的对象。
适配器模式的具体实现
聊了这么多,相信你应该已经对适配器模式有了一个全面的了解。接下来让我们进入适配器模式的具体实现:
类适配器实现
Adaptee(被适配对象)
public class Adaptee {
/**
* 需要被适配的功能
* 这里用插座转换举例
*/
public void commonThreeHoleSocket(){
System.out.println("hi,我是一个常见的三孔插座");
}
}
Tatget(目标)
public interface Target {
/**
* 定义新插座的规范是新插座的形状应该是一个双头插座
*/
public void doubleHoleSocket();
}
Adapter(适配器)
public class Adapter extends Adaptee implements Target {
/**
* 采用继承的方式实现转换功能
*/
@Override
public void doubleHoleSocket() {
// 调用继承的方法,对其进行增强或处理
this.commonThreeHoleSocket();
System.out.println("==========开始转换==========");
System.out.println("oh,我变成了两孔插座");
}
}
Client(请求者)
public class Client {
public static void main(String[] args) {
Target newPlug = new Adapter();
// 对于使用者来说,它只需要知道适配器提供了一个两孔插头即可
newPlug.doubleHoleSocket();
System.out.println("这是一个两孔插头");
}
}
对象适配器模式
实际上对象适配器模式只是在Adapter的实现方式上与类适配模式,为了减少篇幅,这里仅将Adapter部分进行展示
Adapter(适配器)
public class Adapter implements Target {
/**
* 这是两种实现方式唯一的不同之处
*/
private Adaptee adaptee;
public Adapter() {
this.adaptee = new Adaptee();
}
/**
* 采用组合的方式实现转换功能
*/
public void doubleHoleSocket() {
// 调用组合成员的方法,对其进行增强或处理
this.adaptee.commonThreeHoleSocket();
System.out.println("==========开始转换==========");
System.out.println("oh,我变成了两孔插座");
}
}
拓展思路
好了,我们终于完成了适配器模式的学习。平心而论,适配器模式实际上是一个在思路上相当好理解的设计模式,但是对于实际中如何使用以及什么时候使用,可能会给部分较为青涩的读者造成困扰,在这里我基于我自己的理解拓展一些可能会使用到适配器模式的情景。当然,我们要知道设计模式的应用实际上非常灵活,大家可以根据自己的需要进行各种变换,以下所举的例子只是抛砖引玉,而并非刻板的教条。各位读者可以在实践中不断结合自己的理解进行改造。
什么时候使用适配器模式
相当一部分人会认为“如果某个方法就是我们需要的方法,我们直接在程序中使用就可以了,为什么会需要适配器模式?”
实际上,我们在开发过程中经常会使用到的是已经由别人开发完成的类(众所周知,程序员的键盘上只需要保留ctrl+c+v三个按键),这些类通常经过了充分的测试,稳定性较高。使用适配器模式可以对这些现有类进行适配,当出现bug时,我们很明确的知道Bug不大可能出现在Adaptee(已有组件)中,只需要检查扮演Adapter角色的类即可。这样可以大大缩短检错时间(这下知道为啥同样的ctrl+c大佬的代码就更加稳定的原因了吗)
没有现成的代码
让现有类适配新接口是使用适配器模式似乎顺理成章,但是现成的代码通常并不能完全匹配我们的需求,因此我们经常产生“我不如简单修改一下这段代码的想法”,总是难以控制修改已有代码的欲望。但是修改之后的代码同样需要经过充分的测试,否则就有可能出现没有发现的Bug。但是使用适配器模式,就可以在不修改已有代码的情况下使现有代码适配新的接口。
发布升级版本
在实际开发中,版本的升级是必然会经历的一个阶段,通常用户会希望新升级的版本需要对旧版本有足够的兼容性。然而对于开发者来说,却更希望能够抛弃旧版本,因为这会给他们带来更多的工作量。适配器模式则可以帮助开发者们在开发过程中维护旧版本,使用适配器模式使新版本和旧版本兼容,可以让开发者们更少的关注旧版本的细节,使其更加关注新版本的开发。