引言
俗话说强扭的瓜不甜,但那是在现实的世界,在我们程序开发的世界里,什么都是可以尝试的,具体怎么理解呢?相信在开发中遇到过这样的情况:原本两个类是毫无联系的,但是有些时候我们想让他们进行交互协作,当然最直接暴力的就是修改各自的源码,但没有源码呢,怎么以最简洁的成本实现交互?再比如我们只能确定一个统一的输出,但是输入的类型无法确定,怎么才能更优雅的实现这样的架构呢?这篇文章所介绍的一种设计模式将会给你提供一个较好的选项。
一、适配器模式概述
适配器模式(Adapter Pattern)是一种简单的结构型模式,它可以将一个类的接口变换成客户端所期待的另一种接口,从而使得原本不兼容的两个类能够在一起工作 (Convert the interface of a class into another interface clients expect.Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.),如果一定要与外观、装饰者模式严格区分的话,装饰者模式主要是添加新的功能,而适配器模式主要做的是转换工作。适配器将一个对象包装起来以改变其接口,装饰者将一个对象包装起来以增加新的行为和责任,适配器模式的核心在于“转换”——尽量通过适配器把现有资源转为可用的目标资源,通常适配器模式所涉及的角色包括三种:
-
目标(Target)——客户端期望使用的接口或类
-
被适配者(Adaptee)——一个现存需要适配的接口。
-
适配器(Adapter)——负责将Adaptee的接口转换为Target的接口。适配器是一个具体的类,这是该模式的核心。
根据与Adaptee角色的连接关系不同,通常适配器还分为类适配器模式和对象适配器模式。
-
类适配器模式
在类适配器模式中,适配器Adapter需要继承自被适配者Adaptee并实现目标接口Target。由于Java中是单继承,所以类适配器仅仅能服务于所继承的被适配者Adaptee
-
对象适配器模式
在对象适配器模式中,适配器Adapter需要实现目标接口Target并持有Adaptee的引用,通过代理方式来连接到Adaptee。
二、适配器模式的优点和缺点及可用场景
1、适配器模式的优点
-
通过适配器模式可以让两个原本没有任何关系的类能够在一起协调运行,而无需改动原来的代码,极大的增加了可扩展性。
-
将目标类和适配者类解耦
-
增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性,符合开闭原则
2、适配器模式的缺点
过多的使用适配器,可能会导致系统逻辑非常零乱,不易整体进行把握,降低代码的可读性,并增加了一些冗余的代码,如果可以对系统进行重构,尽量不要使用使用适配器模式。
##3、适配器模式的可用场景及注意事项
-
已经存在的类的接口不符合具体的业务的需求。
-
创建一个可以复用的类,使得该类可以与其他不相关的类或不可预见的类(即那些接口可能不一定兼容的类)协同工作。
-
在不对每一个都进行子类化以匹配它们的接口的情况下,使用一些已经存在的子类。
-
适配器适用于对不兼容的两个类进行转换,在可能面对无限类型的输入但输出都是统一的类型,可以在Adapter里返回统一的输出
-
适配器模式中被适配的接口 Adaptee 和适配成为的接口 Target 是没有关联的,Adaptee 和 Target 中的方法既可以是相同的,也可以是不同的
-
适配器在适配的时候,可以适配多个 Apaptee,也就是说实现某个新的 Target 的功能的时候,需要调用多个模块的功能,适配多个模块的功能才能满足新接口的要求-
三、适配器模式的实现
1、类适配器模式
以电源适配器为例,民用电压标准输出为220V,但是现在我们的手机设备只需要5V的电压输出为背景,首先定义Target角色
//Target接口角色,此时我们想要的是5V的电压
public interface TargetInterface {
public int get5Volt();
}
但此时我们拥有的是民用电压输出Adaptee角色为220V,需要适配才能一起协调工作
//Adaptee 角色,目前我们现有的民用电压标准输出为220V
public class CivilVoltage {
public int outPutVolt(){
return 220;
}
...
}
实现核心角色Adapter
//Adapter角色,为了在不改变Target角色和Adaptee 角色原有逻辑,将Adaptee 角色 方法 转为 Target角色的接口方法
public class PowerAdapter extends CivilVoltage implements TargetInterface {
@Override
public int get5Volt() {
System.out.println("把Adaptee 角色的电源转化为Target需要的5V...");
return 5;
}
}
客户端通过调用适配器得到期望的电压:
public class DigitalDev {
public static void main(String[] args) {
PowerAdapter adapter=new PowerAdapter();
System.out.println("得到需输出的电压:"+adapter.get5Volt());
}
}
2、对象适配器模式
比如说当前我们实现了把日志保存到文件中的功能,此时产品突然要求增加一项上传到服务器的功能为背景,首先按照面向对象的思想先定义一个JavaBean和操作接口
public class LogInfo {
private String date;
private String msg;
...
}
/*
* 原有操作日志文件工具接口
*/
public interface LogOperateApi {
public List<LogInfo> readLog();
public void saveLog(List<LogInfo> list);
}
定义Adaptee角色
/*
* Adaptee角色类 实现对日志文件的操作
*/
public class FileLogOperateImpl implements LogOperateApi {
@Override
public List<LogInfo> readLog() {
List<LogInfo> list = null;
System.out.println("从文件中读取保存的日志信息...");
//具体实现略。
return list;
}
@Override
public void saveLog(List<LogInfo> list) {
System.out.println("把日志保存到从文件中...");
}
}
定义Target角色
//Target 接口此时我们希望把数据保存到服务器中
public interface LogOperateDBTarget {
public void saveLogToDB(List<LogInfo> list);
}
实现Target接口持有Adaptee引用定义Adapter
public class LogAdapter implements LogOperateDBTarget {
private LogOperateApi adaptee;
public LogAdapter(LogOperateApi adaptee) {
this.adaptee = adaptee;
}
public void saveLog(List<LogInfo> list) {
System.out.println("把日志保存到文件中...");
adaptee.saveLog(list);
}
@Override
public void saveLogToDB(List<LogInfo> list) {
System.out.println("通过DB方式保存日志");
}
}
客户端通过调用适配器得到期望的电压
public class LogClient {
public static void main(String[] args) {
LogInfo logbean = new LogInfo();
logbean.setDate("2018-01-18 17:09:44");
logbean.setMsg("run by cmo");
List<LogInfo> list = new ArrayList<LogInfo>();
LogOperateApi logFileApi = new FileLogOperateImpl();
logFileApi.saveLog(list);
// 创建操作日志的接口对象
LogOperateDBTarget api = new LogAdapter(logFileApi);
api.saveLogToDB(list);
}
}
3、双向适配器模式
适配器有一个潜在的问题——被适配的对象不再兼容 Adaptee 的接口,因为适配器只是实现了 Target 的接口,导致了并不是所有 Adaptee 对象可以被使用的地方都能是使用适配器,当然适配器也可以实现双向的适配,前面所讲的都是把 Adaptee 适配成为 Target,其实也可以反过来把 Target 适配成为 Adaptee。即这个适配器可以同时当作 Target 和 Adaptee 来使用
package adapter.objadapter;
import java.util.List;
/*
* 双向适配器对象案例
*/
public class TwiceAdapter implements LogOperateApi, LogOperateDBTarget {
/*
* 持有需要被适配的文件存储日志的接口对象
*/
private LogOperateApi fileLog;
/*
* 持有需要被适配的 DB 存储日志的接口对象
*/
private LogOperateDBTarget dbLog;
public TwiceAdapter(LogOperateApi fileLog, LogOperateDBTarget dbLog) {
this.fileLog = fileLog;
this.dbLog = dbLog;
}
@Override
public void saveLogToDB(List<LogInfo> list) {
}
@Override
public List<LogInfo> readLog() {
return null;
}
@Override
public void saveLog(List<LogInfo> list) {
}
}
四、类适配器模式和对象适配器模式
-
类适配器使用对象继承的方式,属于静态的定义方式;而对象适配器模式则使用对象组合的方式,属于动态组合的方式
-
类适配器是直接继承了 Adaptee,使得适配器不能和 Adaptee 的子类一起工作;而对象适配器允许一个 Adapter 和多个 Adaptee,包括 Adaptee 和它所有的子类一起工作
-
类适配器可以重新定义 Adaptee 的部分行为,相当于子类覆盖父类的部分实现方法,而对象适配器仅仅是持有Adaptee的引用,无权重定义 Adaptee 的原有逻辑。
-
类适配器仅仅引入了一个对象,并不需要额外的引用来间接得到 Adaptee,对象适配器需要额外的引用来间接得到 Adaptee。
五、多态和适配器模式
多态是面向对象编程的一个重要特性,它允许我们使用父类的引用来引用子类的对象,从而实现代码的复用。多态的实现方式是通过继承和接口实现。例如,我们定义一个父类Animal,然后定义多个子类Dog、Cat等,每个子类都实现了父类Animal的方法。然后我们定义一个父类Person,它有一个方法feed(animal),这个方法接受一个Animal类型的参数。然后我们定义一个子类Teacher,它继承自父类Person,并且重写了父类Person的feed(animal)方法,使其可以接受Dog和Cat类型的参数。这样,我们就可以通过父类Person的引用来调用子类Teacher的feed(animal)方法,从而实现多态。
适配器模式和多态也有相似之处,它也可以通过父类的引用来引用子类的对象,从而实现代码的复用。但是,适配器模式的目的不仅仅是实现代码的复用,更重要的是将不兼容的接口或类进行适配,使其能够协同工作。适配器模式通常通过继承或实现接口来实现。例如,我们定义一个父类Target,然后定义一个子类TargetImpl,它实现了父类Target的所有方法。然后我们定义一个适配器类Adapter,它实现了父类Target的所有方法,并在这些方法中调用子类TargetImpl的方法,从而实现对子类TargetImpl的适配。然后我们可以通过父类Target的引用来调用适配器类Adapter的方法,从而实现适配器模式。
// 假如我们有一个目标接口MediaPlayer 可能未来需要支持播放mp4 vlc格式的
interface MediaPlayer {
void play(String audioType, String fileName);
}
// 目标接口的实现类,如果没有加入Adapter角色,可能直接实现播放mp3的功能,将来需要支持mp4、vlc再改实现类
class AudioPlayer implements MediaPlayer {
private MediaAdapter mediaAdapter;
@Override
public void play(String audioType, String fileName) {
// 支持mp3格式的播放
if (audioType.equalsIgnoreCase("mp3")) {
System.out.println("Playing mp3 file: " + fileName);
}
// 使用适配器來支持其他格式的播放
else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {
mediaAdapter = new MediaAdapter(audioType);
mediaAdapter.play(audioType, fileName);
}
else {
System.out.println("Invalid media type: " + audioType);
}
}
}
// 被适配对象,他可以提供播放mp4、vlc的能力,但是与MediaPlayer 没有联系,而且参数列表也不同,怎么与目标接口的实现类一起协同工作呢?很简单就是通过适配器MediaAdapter
class AdvancedMediaPlayer {
void playVlc(String fileName) {
System.out.println("Playing vlc file: " + fileName);
}
void playMp4(String fileName) {
System.out.println("Playing mp4 file: " + fileName);
}
}
// 让适配器也去实现这个目标接口,然后在目标实现类内部使用的时候直接使用适配器去交互
class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedMediaPlayer;
MediaAdapter(String audioType) {
if (audioType.equalsIgnoreCase("vlc") || (audioType.equalsIgnoreCase("mp4")) {
advancedMediaPlayer = new AdvancedMediaPlayer();
}
}
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMediaPlayer.playVlc(fileName);
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedMediaPlayer.playMp4(fileName);
}
}
}
public class AdapterPatternExample {
public static void main(String[] args) {
AudioPlayer audioPlayer = new AudioPlayer();
audioPlayer.play("mp3", "song.mp3"); // 直接播放mp3
audioPlayer.play("vlc", "movie.vlc"); // 使用适配器播放vlc
audioPlayer.play("mp4", "video.mp4"); // 使用适配器播放mp4
audioPlayer.play("avi", "video.avi"); // 不支持的类型
}
}
小结
总的来说,建议尽量使用对象适配器方式,适配器模式中被适配的接口 Adaptee 和适配成为的接口 Target 是没有关联的,Adaptee 和 Target 中的方法既可以是相同的,也可以是不同的,另外通常充当适配器角色的类应该是实现已有接口的抽象类(此类是不要被实例化的,而只充当适配器的角色,即为其子类提供了一个共同的接口,但其子类又可以将精力专注于业务),总之适配器模式属于补偿模式,可用来在系统后期扩展、修改时使用,但要注意不要过度使用适配器模式。