设计模式学习——适配器模式

这篇笔记介绍了适配器模式的概念、优缺点及其在 C++ 中的实现,并通过多媒体播放器和 STL 容器适配器的示例展示了其应用。

引入

现实生活中,“电源适配器”可以将220V转换成12V等所需要的电压。编程中的适配器同样起到了桥梁的作用,使原本不能一起工作的类能够协同工作。

举个真实的例子,读卡器是作为内存卡和笔记本之间的适配器。您将内存卡插入读卡器,再将读卡器插入笔记本,这样就可以通过笔记本来读取内存卡。——菜鸟教程

优缺点

  • 首先,使用适配器模式意味着可以在源代码的基础上通过简单添加代码来适配新的功能,而不需要真的去实现,这可以提供良好的灵活性并减小工作量及代码复用。
  • 然而过度使用适配器可能导致系统结构混乱,难以理解和维护。因此要谨慎使用。

多媒体播放器

假设有一个音频播放器,它只能播放音频文件。现在,我们需要播放 VLC 和 MP4 文件,可以通过创建一个适配器来实现:

  • 目标接口:定义一个可以播放多种格式文件的音频播放器接口。
  • 适配者类:现有的音频播放器,只能播放 MP3 文件。
  • 适配器类:创建一个新的类,实现目标接口,并在内部使用适配者类来播放 MP3 文件,同时添加对 VLC 和 MP4 文件的支持。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class MediaPlayer {
public:
    MediaPlayer() = default;
    virtual ~MediaPlayer() = 0;
    virtual void play(const std::string &filetype, const std::string &filename) = 0;
};

class AudioPlayer : public MediaPlayer{
public:
    ~AudioPlayer() override = default;
    void play(const std::string &filetype, const std::string &filename) override;
};
class VideoPlayer{
public:
    void playMP4(const std::string &filename);
    void playMKV(const std::string &filename);
};

继承自MediaPlayerAudioPlayer类能够正常播放音频文件,而VideoPlayer类能够播放视频类文件。在开发过程中,若要保留MediaPlayer的接口,则可以使用适配器模式。

我们定义类MediaAdapter作为适配器类,在其中加入连接对象的成员指针变量。同时,由于使用适配器会增加项目维护难度,所以我们还可以在适配器中使用[[deprecated]]标签来指出推荐的使用方式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class MediaAdaptor : public MediaPlayer {
public:
    MediaAdaptor();
    ~MediaAdaptor() override;
    void play(const std::string &filetype, const std::string &filename) override;

private:
    VideoPlayer *_player;
    [[deprecated("Use VideoPlayer::playMP4() instead")]]void playMP4(const std::string &filename);
    [[deprecated("Use VideoPlayer::playMKV() instead")]]void playMKV(const std::string &filename);
};

AudioPlayerplay()操作中,如果遇到了音频以外的格式,就使用适配器进行播放。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void AudioPlayer::play(const std::string &filetype, const std::string &filename) {
    if(filetype == "mp3" || filetype == "m4a")
        std::cout << "Playing: " << filename << std::endl;
    else{
        MediaAdaptor adaptor;
        adaptor.play(filetype,filename);
    }
}
void MediaAdaptor::play(const std::string &filetype, const std::string &filename) {
    if(filetype == "mp4")
        playMP4(filename);
    else if(filetype == "mkv")
        playMKV(filename);
    else
        std::cout << "Invalid File Format \"" << filetype << "\"" << std::endl;
}

最后,我们在主程序中使用AudioPlayer来播放各种格式的媒体。

1
2
3
4
5
6
7
8
int main() {
    AudioPlayer player;
    player.play("mp3","Synthesis.");
    player.play("m4a", "Record120");
    player.play("mp4", "MaimaiDX [X] 101.00!!!!");
    player.play("av1", "HelloWorld C++");
    return 0;
}

输出结果:

1
2
3
4
Playing: Synthesis.
Playing: Record120
Playing: MaimaiDX [X] 101.00!!!!
Invalid File Format "av1"

STL中的适配器

C++的STL中就有所谓的“容器适配器”,例如std::stackstd::queue。由于他们的工作原理除了数据顺序问题外基本相同,因此他们都被设计成std::deque(双端队列)的适配器。

以下是std::stack的部分源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
template <class T, class Sequence = deque<T> >
class stack {
  //以下__STL_NULL_TMPL_ARGS会展开为 <>
  friend bool operator== __STL_NULL_TMPL_ARGS (const stack&, const stack&);
  friend bool operator< __STL_NULL_TMPL_ARGS (const stack&, const stack&);
public:
  typedef typename Sequence::value_type value_type;
  typedef typename Sequence::size_type size_type;
  typedef typename Sequence::reference reference;
  typedef typename Sequence::const_reference const_reference;
protected:
  Sequence c;   //底层容器
public:
  //以下完全利用Sequence c的操作,完成stack的操作
  bool empty() const { return c.empty(); }
  size_type size() const { return c.size(); }
  reference top() { return c.back(); }
  const_reference top() const { return c.back(); }
  //deque是两头可进出,stack是后进后出
  void push(const value_type& x) { c.push_back(x); }
  void pop() { c.pop_back(); }
};

std::stack中,Sequence c被作为一个适配器,承载了栈的全部功能。这种做法大大减轻了代码复用情况,也避免了不必要的复杂性。

总结

适配器模式(Adapter Pattern)充当两个不兼容接口之间的桥梁,属于结构型设计模式。它通过一个中间件(适配器)将一个类的接口转换成客户期望的另一个接口,使原本不能一起工作的类能够协同工作。

在实际场景中,适配器可以用来“亡羊补牢”,提供临时快速的代码解决方案;也可在多种数据结构相似时使用,消除代码复用的同时大大减少不必要的复杂性。