适配器模式的核心思想是通过一个中间层,将目标接口和适配者接口进行转换,让调用方只需要面向目标接口编程,不需要关心适配者的具体实现细节。在C++中实现适配器模式主要有类适配器和对象适配器两种常见方式,下面分别进行讲解。
适配器模式的核心角色
在讲解具体实现之前,先明确适配器模式涉及的三个核心角色:
- 目标接口(Target):调用方期望使用的接口,通常是抽象类或者接口定义。
- 适配者(Adaptee):需要被适配的原有接口或者第三方接口,其接口形式和调用方期望的目标接口不兼容。
- 适配器(Adapter):核心实现类,负责将适配者的接口转换为目标接口,让调用方可以通过目标接口调用适配者的功能。
类适配器实现方式
类适配器通过公有继承目标接口,私有继承适配者类的方式实现接口转换,下面通过一个实际案例说明。
假设我们有一个旧的音频播放类,只能播放MP3格式的文件,现在需要让它可以支持播放WAV格式的文件,调用方期望的统一接口是play_audio方法,传入文件路径和格式类型。
1. 定义目标接口
目标接口是调用方期望使用的接口,这里定义统一的音频播放接口:
// 目标接口:统一的音频播放接口
class AudioPlayer {
public:
virtual ~AudioPlayer() {}
// 播放音频的接口,format为音频格式
virtual void play_audio(const std::string& file_path, const std::string& format) = 0;
};
2. 定义适配者类
适配者是原有的旧接口,这里定义只能播放MP3的旧播放类:
// 适配者:原有的MP3播放类
class OldMp3Player {
public:
// 原有的播放方法,只支持MP3格式
void play_mp3(const std::string& file_path) {
std::cout << "使用旧播放器播放MP3文件:" << file_path << std::endl;
}
};
3. 实现类适配器
类适配器同时继承目标接口和适配者类,重写目标接口的方法,在方法内部调用适配者的原有方法:
// 类适配器:继承目标接口和适配者类
class ClassAudioAdapter : public AudioPlayer, private OldMp3Player {
public:
void play_audio(const std::string& file_path, const std::string& format) override {
if (format == "mp3") {
// 调用适配者的原有方法
play_mp3(file_path);
} else {
std::cout << "类适配器不支持" << format << "格式播放" << std::endl;
}
}
};
4. 类适配器使用示例
#include <iostream>
#include <string>
int main() {
AudioPlayer* player = new ClassAudioAdapter();
// 调用目标接口播放MP3文件
player->play_audio("test.mp3", "mp3");
// 尝试播放其他格式
player->play_audio("test.wav", "wav");
delete player;
return 0;
}
对象适配器实现方式
对象适配器通过组合适配者对象的方式实现接口转换,相比类适配器更加灵活,也是更推荐的实现方式。
1. 定义目标接口和适配者类
目标接口和适配者类和类适配器场景中的定义一致,这里不再重复。
2. 实现对象适配器
对象适配器继承目标接口,内部持有适配者对象的指针或引用,通过调用适配者对象的方法实现接口转换:
// 对象适配器:继承目标接口,组合适配者对象
class ObjectAudioAdapter : public AudioPlayer {
private:
OldMp3Player* mp3_player; // 持有适配者对象
public:
ObjectAudioAdapter(OldMp3Player* player) : mp3_player(player) {}
~ObjectAudioAdapter() {
delete mp3_player;
}
void play_audio(const std::string& file_path, const std::string& format) override {
if (format == "mp3") {
// 调用适配者对象的方法
mp3_player->play_mp3(file_path);
} else {
std::cout << "对象适配器不支持" << format << "格式播放" << std::endl;
}
}
};
3. 对象适配器使用示例
int main() {
OldMp3Player* old_player = new OldMp3Player();
AudioPlayer* player = new ObjectAudioAdapter(old_player);
// 调用目标接口播放MP3文件
player->play_audio("test.mp3", "mp3");
// 尝试播放其他格式
player->play_audio("test.wav", "wav");
delete player;
return 0;
}
两种实现方式的对比
类适配器和对象适配器各有优缺点,适用场景不同,具体对比如下:
| 对比维度 | 类适配器 | 对象适配器 |
|---|---|---|
| 实现方式 | 继承目标接口和适配者类 | 继承目标接口,组合适配者对象 |
| 灵活性 | 较低,适配者类被固定继承,无法适配适配者的子类 | 较高,可以动态传入不同的适配者对象,也可以适配适配者的子类 |
| 耦合度 | 较高,和适配者类强耦合 | 较低,通过组合解耦,符合合成复用原则 |
| 适用场景 | 适配者类数量少,不需要扩展适配者子类的情况 | 适配者类较多,或者需要适配适配者子类的情况,是更通用的实现方式 |
适配器模式的适用场景
在C++开发中,以下场景适合使用适配器模式:
- 需要使用现有的类,但是其接口不符合系统的需求。
- 想要创建一个可以复用的类,该类可以和其他不相关的类或者不可预见的类协同工作。
- 需要对接第三方库,第三方库的接口和自己系统的接口不兼容,又不想修改第三方库的代码。
注意:适配器模式主要用于接口转换,不要过度使用,如果系统中有大量接口不兼容的情况,应该优先考虑重构接口,而不是大量使用适配器,否则会增加系统的复杂度。