一文彻底带你搞懂什么是适配器模式!!
- 什么是适配器模式?
- 适配器的两种实现方式
- 适用情况
- 代码示例
- 背景
- 类适配器
- 对象适配器
- IO流中的实际应用
- 应用扩展
- 总结
什么是适配器模式?
适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。
主要目的是解决在不改变现有代码的情况下,使不兼容的接口之间能够正常工作,通过创建一个中间转换的适配器来将一个对象转换成我们所需要的接口
例如下图的 USB 转 Type-C 就是一个适配器
适配器的两种实现方式
通常有两种方式实现适配器模式,一种是类适配器,类适配器目前已不太使用,另一种实现方式是对象适配器,通常情况下采用对象适配器会使得代码更易扩展与维护。
不管采用何种方式,其基本的实现思想都是:对现有接口的实现类进行扩展,使其实现客户期望的目标接口。
类适配器通过继承现有接口类并实现目标接口,这样的话会使得现有接口类完全对适配器暴露,使得适配器具有现有接口类的全部功能,破坏了封装性。此外从逻辑上来说,这也是不符合常理的,适配器要做的是扩展现有接口类的功能而不是替代。
对象适配器持有现有接口类一个实例,并扩展其功能,实现目标接口。这是推荐的方式,优先采用组合而不是继承,会使得代码更利于维护。
适用情况
-
接口不兼容:你有两个接口或类,它们原本不兼容,但你又想让它们能够一起工作。
-
希望复用代码:你已经有一个功能完整的类或接口,但接口不符合新的需求。通过适配器,你可以重用现有的代码,而不是重新实现一个新的版本。
-
第三方集成:你可能需要使用第三方库或类,但第三方库的接口与你自己的接口不兼容。使用适配器可以轻松地将第三方库集成到你的系统中。
代码示例
背景
美国的正常供电电压为110V,一个中国人带了一款中国制造电器去美国,这个电器必须要在220V电压下才能充电使用。
这种情况下,客户(中国人)的期望接口是有一个220V的电压为电器充电,但实际的接口是仅有一个110V的电压供电器充电,所以就需要采用一根电压转换器(适配器)使得110V的电压能够转换为220V的电压,供客户使用。
- Target:客户期望获得的功能接口(220V电压供电)。
- Cilent:客户,期望访问Target接口(客户期望能有220V电压)。
- Adaptee:现有接口,这个接口需要被适配(现有110V电压供电,需要被适配至220V)。
- Adapter:适配器类,适配现有接口使其符合客户需求接口(适配110V电压,使其变为220V电压)。
在适配器模式中,Adapter扩展Adaptee以实现对应功能,Cilent调用Adapter以获得相应功能。
类适配器
类适配器结构图:
详细附代码图:
// 定义一个目标接口,它声明了期望实现的充电方法chargeBy220V
interface Target {
void chargeBy220V();
}
// 定义一个被适配的类接口,它有chargeBy110V充电方法
interface Adaptee {
void chargeBy110V();
}
// 创建一个美国供电器类,它实现了被适配的类接口,提供110V充电方法
class AmericanCharger implements Adaptee {
@Override
public void chargeBy110V() {
System.out.println("美国供电器,为您服务,正在给您充 110V 电压");
}
}
// 创建一个适配器类,它继承了美国供电器类,并且实现了目标接口
class Adapter extends AmericanCharger implements Target {
// 适配器重写目标接口的chargeBy220V方法,
// 调用被适配类的chargeBy110V方法,并额外转换电压
@Override
public void chargeBy220V() {
super.chargeBy110V(); // 调用被适配类的110V充电方法
System.out.println("再加 110V,现在给您充 220V 电压!!"); // 实现额外的电压转换逻辑
}
}
// 测试类,测试适配器模式
public class Client{
public static void main(String[] args) {
// 创建适配器对象,并通过目标接口调用chargeBy220V方法
new Adapter().chargeBy220V();
}
}
这种类适配器可能会让人误以为适配器本身就是提供110V充电功能的美国供电器,而忽略了它实际上是一个转换器,逻辑上容易混乱和让人混淆
对象适配器
/**
* 定义一个目标接口Target,包含一个充电方法chargeBy220V,该方法用于充电220V电压。
*/
interface Target {
void chargeBy220V(); // 定义接口方法,使用220V电压充电
}
/**
* 定义一个适配者接口Adaptee,包含一个充电方法chargeBy110V,该方法用于充电110V电压。
*/
interface Adaptee {
void chargeBy110V(); // 定义接口方法,使用110V电压充电
}
/**
* 实现适配者接口Adaptee的AmericanCharger类,代表美国的充电器,提供110V的充电服务。
*/
class AmericanCharger implements Adaptee {
@Override
public void chargeBy110V() { // 覆写chargeBy110V方法,输出充电信息
System.out.println("美国供电器,为您服务,正在给您充 110V 电压");
}
}
/**
* 实现目标接口Target的Adapter类,通过持有Adaptee对象来适配不同的电压充电需求。
*/
class Adapter implements Target {
private Adaptee adaptee; // 私有变量,用于持有Adaptee类型的对象
/**
* 构造器,接收一个Adaptee对象以便在内部调用其方法。
* @param adaptee 适配者对象
*/
public Adapter(Adaptee adaptee) { // 构造器初始化适配器,并传入适配者对象
this.adaptee = adaptee;
}
@Override
public void chargeBy220V() { // 覆写chargeBy220V方法,实现适配功能
adaptee.chargeBy110V(); // 首先使用110V充电
System.out.println("再加 110V,现在给您充 220V 电压!!"); // 然后输出适配后的信息
}
}
/**
* 测试类Test,用于演示对象适配模式的使用。
*/
public class Client{
public static void main(String[] args) {
Adaptee adaptee = new AmericanCharger(); // 创建一个AmericanCharger对象
Adapter adapter = new Adapter(adaptee); // 使用AmericanCharger对象来创建Adapter对象
adapter.chargeBy220V(); // 通过Adapter对象调用chargeBy220V方法,输出适配后的充电信息
}
}
对象适配器采用组合的方式实现对现有接口的扩展以达到客户期望的接口。
IO流中的实际应用
让我们来看JavaIO流中的一个实例:
FileInputStream fis = new FileInputStream("qe");
InputStreamReader isrAdapter = new InputStreamReader(fis);
BufferedReader bf = new BufferedReader(isrAdapter);
BufferedReader
(此处也就是客户)需要读取文件字符流进行工作,读取文件字符流就是客户的需求部分,但是根据现有的接口,想要读取文件就只能读取字节流
FileInputStream
就是现有接口的一个具体实现类,为了满足客户的需求,我们要对现有的接口进行适配,
InputStreamReader
就是一个适配器,它持有一个现有接口类的实例,通过这个实例读取文件字节流并将其扩展为字符流以满足客户的需求,这是标准的对象适配器模式。
如果仔细研究源码,发现JavaIO库将适配器定义为抽象的,并由具体的适配器继承该抽象适配器,如这里的 InputStreamReader
就是具体的适配器之一。
应用扩展
如果实现适配有多种方式的话,我们可以将适配器类Adapter声明为抽象类,并由子类扩展它:
/**
* 定义一个目标接口Target,
* 包含一个充电方法chargeBy220V,该方法用于充电220V电压。
*/
interface Target {
void chargeBy220V(); // 目标接口的充电方法,使用220V电压充电
}
/**
* 定义一个适配者接口Adaptee,
* 包含一个充电方法chargeBy110V,该方法用于充电110V电压。
*/
interface Adaptee {
void chargeBy110V(); // 适配者接口的充电方法,使用110V电压充电
}
/**
* 实现适配者接口Adaptee的AmericanCharger类,
* 代表美国的充电器,提供110V的充电服务。
*/
class AmericanCharger implements Adaptee {
@Override
public void chargeBy110V() {
System.out.println("美国供电器,为您服务,正在给您充 110V 电压");
}
}
/**
* 抽象类Adapter,作为适配器的基础类,实现目标接口Target。
*/
abstract class Adapter implements Target {
Adaptee adaptee; // 私有变量,用于持有Adaptee类型的对象
/**
* 构造器,接收一个Adaptee对象以便在内部调用其方法。
* @param adaptee 适配者对象
*/
public Adapter(Adaptee adaptee) { // 构造器初始化适配器,并传入适配者对象
this.adaptee = adaptee;
}
}
/**
* 实现Adapter类的具体子类ChinaMakeAdapter,
* 用于将美国充电器的110V充电适配为中国标准的220V充电。
*/
class ChinaMakeAdapter extends Adapter{
public ChinaMakeAdapter(Adaptee adaptee){
super(adaptee); // 调用父类的构造器
}
/**
* 实现目标接口Target的chargeBy220V方法,
* 通过调用适配者接口Adaptee的chargeBy110V方法来实现。
*/
@Override
public void chargeBy220V() {
adaptee.chargeBy110V(); // 调用适配者的110V充电方法
System.out.println("再加110V,达到220V,认准中国制造!"); // 输出说明适配器正在工作
}
}
/**
* 测试类Client,用于演示对象适配器模式的使用。
*/
public class Client{
public static void main(String[] args) {
// 创建一个AmericanCharger对象
Adaptee adaptee = new AmericanCharger();
// 使用AmericanCharger对象来创建ChinaMakeAdapter对象
Adapter adapter = new ChinaMakeAdapter(adaptee);
// 通过ChinaMakeAdapter对象调用chargeBy220V方法,输出适配后的充电信息
adapter.chargeBy220V();
}
}
总结
优点:
- 解耦合:适配器模式允许不相关的类或接口协同工作,这为系统的不同部分提供了更大的灵活性。
- 复用:通过适配器,可以复用现有的类,而不需要修改它们的源代码。
- 透明性:适配器提供了一种封装机制,允许客户端代码与目标接口交互,而不必关心实际的实现类。
- 灵活性:适配器模式提供了很大的灵活性,因为它允许在运行时动态地结合不同的类和接口。
缺点:
- 过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。
- 适配可能复杂或不可能:有时,适配两个不兼容的接口可能非常困难,甚至不可能,比如让母猪飞上天。
当我们有动机地修改一个正常运行的系统的接口,这时应该考虑使用适配器模式。
注意事项:适配器不是在详细设计时添加的,而是解决正在服役的项目的问题,即现有接口可能无法改变(去美国不可能把人家110V电压供给改成220V电压供给)。
往期设计模式专题文:
一文带你彻底搞懂什么是代理模式!!
一文带你彻底搞懂设计模式之单例模式!!由浅入深,图文并茂,超超超详细的单例模式讲解!!