基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

设计模式--适配器模式 (Adapter)

知识点图片

适配器模式通过创建适配器,将一个类的接口转换成客户端期望的接口,使原本不兼容的类能协同工作。

我们来详细、系统地讲解一下设计模式中的适配器模式(Adapter Pattern)

我会通过 "是什么" -> "为什么" -> "怎么做" -> "优缺点" -> "实际案例" 的结构来为你解释。


1. 是什么:核心思想与生活中的比喻

适配器模式的核心思想非常简单,就像它的名字一样,它扮演着一个“适配器”或“转换器”的角色。

一句话定义:
将一个类的接口转换成客户端(Client)所期望的另一个接口。从而使得原本由于接口不兼容而不能在一起工作的类可以一起工作。

生活中的比喻:电源适配器

这是理解适配器模式最经典的例子:

  • 你的笔记本电脑(客户端 Client): 它需要 19V 的直流电才能工作。这是它期望的“目标接口(Target)”。
  • 墙上的插座(被适配者 Adaptee): 它提供的是 220V 的交流电。这是一个已经存在的、但接口不兼容的“服务”。
  • 电源适配器(适配器 Adapter): 你把它一头插在墙上,另一头连接到笔记本电脑。它的作用就是将 220V 交流电转换成 19V 直流电。

在这个过程中:

  • 你的笔记本电脑完全不知道墙上插座的存在,它只认识那个能提供 19V 直流电的插头(目标接口)。
  • 电源适配器(Adapter)封装了转换的复杂逻辑,使得不兼容的两者能够协同工作。

2. 为什么:解决什么问题?

适配器模式主要解决以下问题:

  1. 复用现有代码: 当你有一个现成的类(Adaptee),它的功能很强大,完全符合你的需求,但它的接口(方法名、参数等)与你系统中其他部分所期望的接口(Target)不一致时,你不想重写这个类,而是想复用它。
  2. 统一接口: 当你需要使用多个具有不同接口的第三方库或遗留系统时,可以为它们分别创建适配器,将它们都适配成一个统一的接口,这样你的主系统就可以用同样的方式与它们交互,降低了系统的复杂性。

3. 怎么做:结构与实现

适配器模式通常涉及三个主要角色:

  • Target (目标接口): 客户端代码所期望和直接调用的接口。
  • Adaptee (被适配者): 已经存在的、拥有不兼容接口的类。
  • Adapter (适配器): 实现 Target 接口,并内部包装一个 Adaptee 对象的实例。它负责将来自 Target 接口的请求转换成对 Adaptee 接口的调用。

适配器模式主要有两种实现方式:类适配器对象适配器

3.1 对象适配器模式 (Object Adapter Pattern) - 推荐使用

这是最常用、最灵活的方式,它基于对象组合(Composition)

  • 结构: Adapter 类实现 Target 接口,并且内部持有一个 Adaptee 类的实例。
  • 工作方式: 当客户端调用 Adapter 的方法时,Adapter 内部会将这个调用委托(delegate)给它持有的 Adaptee 实例来完成实际工作。

UML 图:

plaintext
+-------------+      +----------------+      +---------------+
|   Client    |----->|     Target     |      |    Adaptee    |
+-------------+      +----------------+      +---------------+
                         ^                     | doSomething() |
                         | (implements)        +---------------+
                         |
                   +----------------+
                   |    Adapter     |<>----(has a)----+
                   +----------------+
                   | - adaptee      |
                   +----------------+
                   | + request()    |
                   +----------------+

代码示例 (Java):

假设我们有一个播放器系统。

1. 目标接口 (Target): 我们的系统只认识 MediaPlayer 接口。

java
// 目标接口
public interface MediaPlayer {
    void play(String audioType, String fileName);
}

2. 被适配者 (Adaptee): 我们现在有一个现成的、功能强大的 AdvancedMediaPlayer,但它的接口和我们的系统不兼容。

java
// 被适配者 - 高级播放器
interface AdvancedMediaPlayer {
    void playMp4(String fileName);
    void playVlc(String fileName);
}

class Mp4Player implements AdvancedMediaPlayer {
    @Override
    public void playMp4(String fileName) {
        System.out.println("Playing mp4 file. Name: " + fileName);
    }
    @Override
    public void playVlc(String fileName) {
        // do nothing
    }
}

class VlcPlayer implements AdvancedMediaPlayer {
    @Override
    public void playMp4(String fileName) {
        // do nothing
    }
    @Override
    public void playVlc(String fileName) {
        System.out.println("Playing vlc file. Name: " + fileName);
    }
}

3. 适配器 (Adapter): 创建一个适配器,让 AdvancedMediaPlayer 能被我们的系统使用。

java
// 适配器
public class MediaAdapter implements MediaPlayer {

    private AdvancedMediaPlayer advancedMusicPlayer; // 持有被适配者实例

    public MediaAdapter(String audioType) {
        if (audioType.equalsIgnoreCase("mp4")) {
            advancedMusicPlayer = new Mp4Player();
        } else if (audioType.equalsIgnoreCase("vlc")) {
            advancedMusicPlayer = new VlcPlayer();
        }
    }

    @Override
    public void play(String audioType, String fileName) {
        // 将 MediaPlayer 的调用转换为 AdvancedMediaPlayer 的调用
        if (audioType.equalsIgnoreCase("mp4")) {
            advancedMusicPlayer.playMp4(fileName);
        } else if (audioType.equalsIgnoreCase("vlc")) {
            advancedMusicPlayer.playVlc(fileName);
        }
    }
}

4. 客户端 (Client): 客户端通过 MediaPlayer 接口来播放所有格式。

java
// 客户端播放器
public class AudioPlayer implements MediaPlayer {
    MediaAdapter mediaAdapter;

    @Override
    public void play(String audioType, String fileName) {
        // 内置支持播放 mp3
        if (audioType.equalsIgnoreCase("mp3")) {
            System.out.println("Playing mp3 file. Name: " + fileName);
        }
        // 使用适配器支持其他格式
        else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {
            mediaAdapter = new MediaAdapter(audioType);
            mediaAdapter.play(audioType, fileName);
        } else {
            System.out.println("Invalid media. " + audioType + " format not supported");
        }
    }
}

// 使用
public class AdapterPatternDemo {
   public static void main(String[] args) {
      AudioPlayer audioPlayer = new AudioPlayer();
      audioPlayer.play("mp3", "beyond the horizon.mp3");
      audioPlayer.play("mp4", "alone.mp4");
      audioPlayer.play("vlc", "far far away.vlc");
      audioPlayer.play("avi", "mind me.avi");
   }
}

3.2 类适配器模式 (Class Adapter Pattern)

这种方式基于类继承(Inheritance)

  • 结构: Adapter 类同时继承 Adaptee 类并实现 Target 接口。这种方式在像 C++ 这样支持多重继承的语言中更直接,在 Java 中需要 Adaptee 是一个具体类,而 Target 是一个接口。
  • 缺点:
    • 耦合度高: Adapter 与一个具体的 Adaptee 类绑定,无法适配 Adaptee 的子类。
    • 语言限制: Java 不支持类的多重继承,所以 Adapter 只能继承一个 Adaptee 类,限制了其使用场景。

UML 图:

plaintext
+-------------+      +----------------+      +---------------+
|   Client    |----->|     Target     |      |    Adaptee    |
+-------------+      +----------------+      +---------------+
                         ^                     ^
                         | (implements)        | (extends)
                         |                     |
                   +---------------------------+
                   |         Adapter           |
                   +---------------------------+
                   | + request()               |
                   +---------------------------+

由于其局限性,在实际开发中对象适配器更为常用和推荐。


4. 优缺点总结

优点:

  1. 更好的复用性: 可以透明地复用现有的类,而无需修改其源代码。
  2. 更好的解耦: 将客户端与具体实现解耦。客户端代码始终面向目标接口编程,无需关心背后实际工作的是哪个类,以及它是如何被适配的。
  3. 提高透明性: 客户端完全不知道适配器的存在,调用方式简单、统一。
  4. 灵活性高(特别是对象适配器): 一个适配器可以适配多个不同的被适配者(Adaptee 及其子类)。

缺点:

  1. 增加系统复杂性: 过多地使用适配器,会让系统中的类数量增加,代码的可读性在一定程度上会下降。如果仅仅是为了命名规范或微小差异而引入适配器,可能会得不偿失。

5. 实际案例

适配器模式在各种框架和库中被广泛应用。

  1. Java IO 流:InputStreamReaderOutputStreamWriter

    • InputStream (Adaptee) 是字节流。
    • Reader (Target) 是字符流。
    • InputStreamReader (Adapter) 作为一个适配器,它接收一个 InputStream 对象,并将其适配成 Reader 接口,从而可以将字节流转换为字符流来读取。
    java
    // FileInputStream是字节流 (Adaptee)
    FileInputStream fis = new FileInputStream("file.txt");
    // InputStreamReader是适配器,将字节流适配为字符流 (Target)
    Reader reader = new InputStreamReader(fis, "UTF-8");
    // 现在可以像操作字符流一样操作它
    reader.read();
  2. 日志框架门面:SLF4J (Simple Logging Facade for Java)

    • SLF4J 定义了一套标准的日志接口(Target)。
    • Log4j, Logback, java.util.logging 等是具体的日志实现(Adaptees)。
    • slf4j-log4j12.jar 这样的桥接包就是适配器,它实现了 SLF4J 接口,内部则调用 Log4j 的 API。这使得应用程序可以统一使用 SLF4J 接口,而底层可以灵活切换不同的日志框架。
  3. java.util.Arrays.asList()
    这个方法也体现了适配器模式的思想。它接收一个数组(Adaptee),并返回一个实现了 List 接口(Target)的对象,让你能够以列表的方式操作数组。

总结

当你想使用一个已经存在的类,而它的接口不符合你的需求时,就应该首先考虑使用适配器模式。它是一种非常有用的“补救”模式,能够优雅地解决接口不兼容的问题,是连接新旧代码、异构系统的利器。在实践中,优先选择对象适配器,因为它更符合“组合优于继承”的设计原则,提供了更好的灵活性和解耦性。

00:00
00:00