浅谈设计模式 - 适配器模式(八)
前言:
适配器模式大概是系统用的最多的模式,在spring
框架当中可以看到他的各种应用,比如我们想要注册自己的拦截器,或者需要沿用旧接口完成一些自己的实现,都可以使用适配器模式进行实现,适配器模式是一种非常贴合最少知识原则的设计模式,这篇文章将会详细介绍一下适配器模式。
文章目的:
- 了解什么是适配器模式
- 适配器模式的优缺点
- 实战,了解适配器模式
什么是适配器模式?
定义:在不改动客户代码的情况下实现一个接口向另一个接口的自由转化,让原本不能适配的接口具备相似的功能。
适配器存在三个角色,客户端,适配器,被适配者。适配器实现目标的接口,并且持有被适配者的实例
适配器模式是一种:行为型模式。因为他将一个接口的行为转化为另一个接口的行为。
适配器模式优缺点:
先说说适配器模式的优点:
- 可以让客户从接口的实现当中解放
- 让客户由原本的面向实现转变为面向接口
- 让被适配对象具备接口功能的同时可以实现自由的扩展
下面说说缺点,其实适配器的缺点也比较明显:
- 由于JAVA不支持多继承,无法完成多个对象的适配工作,只能使用多接口的形式适配,实现起来要比其他的语言稍微复杂一些。
- 适配器最难改动的地方在于适配目标的方法,假设适配目标的方法组合了多个被适配对象,此时改动任意一个被适配对象,都会对适配的方法带来影响,同时适配方法也是最难以改动的。
关于适配器使用的建议:
- 一个适配器最好做一个类的适配工作。
- 如果一个适配器需要适配多个类,需要考虑是否存在关联性
- 可以使用双向接口适配器,既可以实现旧接口的方法不改动,同时实现新接口的新实现。要做到这一步,关键是确保:两个接口
使用继承还是使用组合
对于适配这一个概念,我们可以使用两种形式:继承 和 组合
首先说下继承,继承是指对于一个类进行“超类”的扩展,如果此时我们使用继承的形式去扩展目标对象,虽然从理论上可以实现一个适配器直接具备两个对象的功能,但是由于JAVA本身是不支持多继承的,同时多用组合,少用继承是软件设计行业一条非常推崇的定律。所以继承的形式“不太友好”。
再说下使用组合的形式,组合是比较推崇的形式,我们在实现目标接口的基础之上,组合被适配的对象,让旧接口的功能可以兼容新接口的实现。这也是JAVA代码当中经常会见到的一种形式,同时在框架中以类似“套版”的形式出现。
适配器模式的特点:
适配器的实现就是把客户类的请求转化为对适配者的相应接口的调用,当客户端进行请求的时候,目标对象通过适配器,将请求的逻辑转变为“适合”被适配对象的请求,由适配器完成这一转化细节,适配过后执行被适配对象的功能,这种转化对于客户端来说是隐蔽的,同时会让客户端误认为是目标对象完成的了工作,所以这种模式的最大特点是:请求代码可以完全不需要改变。
适配器模式理解
下面根据适配器模式,介绍一下个人对于适配器模式的一些理解。
从插头引申适配器模式
插头有很多中规范,但是日常生活常用的插头一般是两孔或者三孔的插头,一般情况下一个新厂商的插头需要兼容旧厂商生产的插头,同时需要兼容旧厂商的方法,不想改变旧厂商的实现情况下,需要实现新厂商的实现,这时候通常需要依赖一个适配器去做适配,适配器如同一个中间通信人,可以将看起来毫不相干的两个对象之间产生一定的关联特性。
单向适配和双向适配
需要注意的是适配器模式一定不要教条的认为只能单向的适配,适配器是可以进行双向适配的,但是此时我们通常需要两个接口来完成双向的适配。
适配器模式和最少知识原则
适配器模式对应的一个软件设计原则是:最少知识原则
最少知识原则:接口负责尽可能少的功能。用白话来讲就是简洁
遵循最少知识原则
对于任何对象,在他的方法内他应该做这些事情
- 只操作对象自己本身
- 传递的对象参数或者和该对象返回的结果对象(但是会有依赖传递的问题)
- 方法本身创建的对象
- 对象的任何组件:HAS-A(组合)的内部对象
这里需要小心依赖磁铁,也就是依赖了依赖对象的对象,简单理解就是使用了其他对象返回的另一个对象,这种情况很容易被忽视,而且通常情况下是 不会出现问题的,但是一旦要进行重构,这种代码就很容易造成逻辑混乱。
适配器的调用流程
- 客户端通过目标接口调用适配器的方法发送对应的请求。
- 适配器用适配方法转化为一个或者多个被适配器的多种方法
- 客户端得到的接口会误以为是目标对象在工作。同时不需要进行任何变动就可以实现新的功能或者方法。
适配器模式的结构图
下面是适配器模式的结构图:
+ client 客户端
+ Target 目标接口,
+ Adapter 适配器,负责将两个对象进行关联,产生相似的业务
+ Appropriate object 被适配对象,代表了需要适配的对象内容
上面的结构图展示了如何目标接口转化为被适配对象的行为。
实战
说了不少的理论内容,下面我们根据一个模拟场景制定一份适配器的代码:
模拟场景
在任天堂发售的switch
在日版和港版的两个版本当中,充电充电器的设计是不一样的,由于港版沿用了英国使用的是三插式(大部分电器都是这种情况),而我国使用的是较为通用二插式,日本在设计的时候也是使用二插式,所以在充电器的设计下,日本不需要进行适配,直接可以插到我国的插座上,而港版通常需要购买转接头或者买其他的适配器。
不使用设计模式
笔者在构思这一块没有考虑好不使用设计模式如何实现,其实这种情况下,不使用设计模式最好办法通常就是找一个第三方工具进行替代,比如我们可以买一个switch的充电宝,每次充电只要充充电宝就行了,连充电器都省了......
使用设计模式
还是直接从设计模式开干把,很显然,既然港版都是用的转接头,那么我们的代码就使用转接头来实现这一块的功能:
下面是根据模拟场景进行分析,我们依照适配器的结构设计出基本对象:
- switch:我们姑且把它想象是客户端,我们把它想象成一个想要充电的“人”。发起了充电这一个请求来匹配合适的充电器
- JapanMouth:日版充电器,直接对标国内的插口
- Mouth:插口接口。
- EngMouth:英式插口,按照英国的标准设计的插口。
- Adapter:适配器,在这里充当的是转接器,负责转接插口
有了上面这些定义,下面根据具体的设计出代码:
public class Switch {
private TwoHoleCharge twoHoleCharge;
public Switch(TwoHoleCharge twoHoleCharge) {
this.twoHoleCharge = twoHoleCharge;
}
/**
* 模拟充电方法
*/
public void recharge(){
twoHoleCharge.jack();
}
public TwoHoleCharge getTwoHoleCharge() {
return twoHoleCharge;
}
public void setTwoHoleCharge(TwoHoleCharge twoHoleCharge) {
this.twoHoleCharge = twoHoleCharge;
}
}
// 插口接口
public interface Mouth {
void jack();
}
public class JapanMouth implements Mouth {
public void jack(){
System.out.println("日版:开始充电");
}
}
public class EngMouth {
public void specialJack(){
System.out.println("港版:开始充电");
}
}
//==关键==适配器
public class Adapter implements Mouth {
private EngMouth engMouth;
public Adapter(EngMouth engMouth) {
this.engMouth = engMouth;
}
@Override
public void jack() {
engMouth.specialJack();
}
}
public class Main {
public static void main(String[] args) {
// 日版插口可以直接使用
Mouth mouth = new JapanCharge();
Switch aSwitch = new Switch(mouth);
aSwitch.recharge();
// 港版插口需要转接口
Mouth mouth2 = new Adapter(new EngMouth());
aSwitch.setMouth(mouth2);
aSwitch.recharge();
}/*运行结果:
日版:开始充电
港版:开始充电
*/
}
代码比较简单,应该比较好理解,这里重点关注一下Adapter
的适配器对象,通过组合的形式,将被适配对象“隐藏”到了目标对象的方法内部,实现了接口的适配,客户在使用的时候,只需要用一个适配器就可以让港版可以直接兼容到国内的电网。
当然,在现实生活中使用适配器其实比较麻烦,因为总要多带点东西,比如我个人就比较反感现在的手机都把耳机孔给削掉了,出门用个有线听歌还得买个转接口......
总结案例
适配器的案例生活中还是十分常见的,可以举出许许多多的场景出来,同时不需要考虑过多的其他因素,适配器非常贴合“开放-关闭”的原则,对于修改进行了开放,对于原有的代码没有进行修改,然开发人员只需要关注如何适配,而不需要太关心原来的实现,
总结
适配器模式是一个重点模式,他可以实现在不改动旧代码的基础上对于一个目标对象进行二次扩展和升级,并且只需要付出很小的代价就可以完成很多自定义的操作,总体上来说,适配器的弊端在实际的编码过程中往往被规范化的设计而弱化,当然,更多的使用场景可能还是在框架当中,因为框架中使用适配器是一种很常见的行为。