Menu
CoddyTech

适配器模式

本课属于 Coddy C++ 学习旅程的“面向对象编程”单元。第 97 课,共 104 课。

适配器模式可以让不兼容的接口协同工作。它充当两个类之间的桥梁,将一个类的接口转换为客户端所需的接口。当你集成与系统接口不匹配的旧代码或第三方库时,这种模式尤其有用。

假设你有一个现有的类,它能满足你的需求,但它的接口与你的代码所期望的不匹配。你不必修改任何一方,只需创建一个适配器来包装现有的类,并转换调用:

#include <iostream>
#include <string>

// 接口不兼容的现有类
class LegacyPrinter {
public:
    void printMessage(const std::string& msg) {
        std::cout << "Legacy: " << msg << "\n";
    }
};

// 客户端期望的目标接口
class Printer {
public:
    virtual void print(const std::string& text) = 0;
    virtual ~Printer() = default;
};

// 适配器封装 LegacyPrinter 并实现 Printer 接口
class PrinterAdapter : public Printer {
    LegacyPrinter& legacy;
public:
    PrinterAdapter(LegacyPrinter& lp) : legacy(lp) {}
    
    void print(const std::string& text) override {
        legacy.printMessage(text);  // 转换这个调用
    }
};

int main() {
    LegacyPrinter oldPrinter;
    PrinterAdapter adapter(oldPrinter);
    
    // 客户端使用预期的接口
    adapter.print("Hello World");
}

适配器持有对 LegacyPrinter 的引用,并实现 Printer 接口。调用 print() 时,它会委托给 printMessage()。客户端代码通过 Printer 接口进行操作,无需了解底层的旧版实现。

当你想使用现有的类,但它的接口与你的需求不匹配时,可以使用适配器模式;如果你需要创建一个可复用的类,让它与接口不兼容的类协同工作,也可以使用适配器模式。

challenge icon

挑战

简单

让我们构建一个媒体播放器适配器系统,连接不同音频库的接口。想象一下,你要将一个旧版音频库集成到现代媒体播放器应用中:旧库运行良好,但它的接口与播放器的预期不匹配。这正是适配器模式发挥作用的地方。

你要将代码分布在四个文件中:

  • MediaPlayer.h:定义现代媒体播放器所需的目标接口。

    创建一个抽象类 MediaPlayer,并包含以下纯虚方法:

    • play(const std::string& filename):播放指定文件
    • stop():停止播放
    • setVolume(int level):设置音量(0-100)

    包含一个虚析构函数。

  • LegacyAudioLib.h:创建现有的“旧版”音频库,它的接口与目标接口不兼容。

    LegacyAudioLib 类表示一个工作方式与播放器预期不同的旧库。它应包含:

    • loadAudio(const std::string& path):输出 Legacy: Loading [path]
    • startPlayback():输出 Legacy: Playback started
    • stopPlayback():输出 Legacy: Playback stopped
    • adjustVolume(double percentage):接收一个小数(0.0 到 1.0),并输出 Legacy: Volume set to [percentage](显示小数值)
  • AudioAdapter.h:构建连接这两个接口的适配器。

    AudioAdapter 类应继承 MediaPlayer,并持有对 LegacyAudioLib 对象的引用。适配器会将现代接口的调用转换为对旧版接口的调用:

    • play() 应同时调用旧版库的 loadAudio() 和 startPlayback()
    • stop() 应委托给 stopPlayback()
    • setVolume() 接收一个整数(0-100),但旧版库需要小数(0.0-1.0),因此你需要进行转换
  • main.cpp:演示适配器的工作方式。

    读取两个输入:

    1. 要播放的文件名(字符串)
    2. 音量级别(整数,0-100)

    创建一个 LegacyAudioLib 实例,以及一个用于包装它的 AudioAdapter。然后通过 MediaPlayer 接口使用适配器(将其存储为 MediaPlayer 指针),来完成以下操作:

    1. 将音量设置为输入的级别
    2. 播放输入的文件名
    3. 停止播放

例如,输入 song.mp3 和 75 时:

Legacy: Volume set to 0.75
Legacy: Loading song.mp3
Legacy: Playback started
Legacy: Playback stopped

输入 podcast.wav 和 50 时:

Legacy: Volume set to 0.5
Legacy: Loading podcast.wav
Legacy: Playback started
Legacy: Playback stopped

请注意,客户端代码完全通过 MediaPlayer 接口进行操作。它并不知道底层实际工作的是旧版库。适配器负责所有转换,包括将整数音量转换为小数。这就是适配器模式的核心:无需修改任一端,就能让不兼容的接口协同工作。

亲手试试

#include <iostream>
#include <string>
#include "MediaPlayer.h"
#include "LegacyAudioLib.h"
#include "AudioAdapter.h"

using namespace std;

int main() {
    // 读取输入
    string filename;
    int volumeLevel;
    cin >> filename;
    cin >> volumeLevel;
    
    // TODO: 创建一个 LegacyAudioLib 实例
    
    // TODO: 创建一个封装旧版库的 AudioAdapter
    
    // TODO: 将适配器存储为 MediaPlayer 指针
    
    // TODO: 使用 MediaPlayer 接口完成以下操作:
    // 1. 将音量设置为输入的级别
    // 2. 播放输入的文件名对应的文件
    // 3. 停止播放
    
    return 0;
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: C++ 在线编译器