82、STL与外观模式:统一接口、子系统封装、简化调用

外观模式,说白了就是给一堆复杂的子系统套个「壳」。

你想想看,平时我们写代码,经常要跟好几个模块打交道。比如初始化一个视频播放器,你得先初始化音频引擎、视频解码器、字幕渲染器、网络流加载器……每个子系统都有自己的接口,调用顺序还不能乱。我早期做项目时,这种代码散落在各个业务逻辑里,改一个地方就得翻遍整个工程,别提多痛苦了。

外观模式就是来解决这个问题的。它提供一个高层接口,把子系统的调用细节全藏起来。调用方只需要跟这个外观类交互,剩下的脏活累活,外观类替你干了。

外观模式的核心思想

说白了就三个字:简化

  • 统一接口:把多个子系统的接口合并成一个简洁的接口。
  • 子系统封装:调用方不需要知道子系统内部怎么运作。
  • 降低耦合:业务代码只依赖外观类,不依赖具体子系统。

我在项目中遇到过好几次这样的场景:一个功能涉及三四个模块,每个模块的初始化、配置、清理步骤都不一样。如果不做封装,业务代码里全是if-else和try-catch,看着就头疼。外观模式一上,瞬间清爽。

STL里有没有外观模式?

严格来说,STL并没有直接提供「外观模式」这个设计模式的类。但STL的很多容器和算法,其实就扮演了外观的角色。

举个例子:std::vector。它内部管理着动态数组、内存分配器、迭代器、异常安全机制……这些子系统你一个都不用管。你只需要调用push_backsizeat这些简单接口。这就是外观模式的思想——把复杂性藏起来

再比如std::sort。它内部可能用了快速排序、堆排序、插入排序的混合策略,甚至还有并行版本。但你调用时只需要传两个迭代器。STL帮你选了最优算法,你只管用。

核心观点:STL本身就是一套「外观」——它把底层数据结构和算法的复杂性封装成统一的接口,让你用最少的代码完成最多的事。

实战:用STL实现一个外观模式

假设我们有一个多媒体播放系统,包含三个子系统:音频解码器、视频解码器、字幕渲染器。每个子系统都有自己的初始化、播放、停止接口。

我们先定义子系统:

// 子系统1:音频解码器
class AudioDecoder {
public:
    void init(const std::string& file) {
        std::cout << "音频解码器初始化: " << file << std::endl;
    }
    void play() { std::cout << "音频播放中..." << std::endl; }
    void stop() { std::cout << "音频停止" << std::endl; }
};

// 子系统2:视频解码器
class VideoDecoder {
public:
    void init(const std::string& file) {
        std::cout << "视频解码器初始化: " << file << std::endl;
    }
    void play() { std::cout << "视频播放中..." << std::endl; }
    void stop() { std::cout << "视频停止" << std::endl; }
};

// 子系统3:字幕渲染器
class SubtitleRenderer {
public:
    void load(const std::string& srtFile) {
        std::cout << "字幕加载: " << srtFile << std::endl;
    }
    void show() { std::cout << "字幕显示中..." << std::endl; }
    void hide() { std::cout << "字幕隐藏" << std::endl; }
};

现在,我们创建一个外观类,把这些子系统统一管理起来:

// 外观类:媒体播放器
class MediaPlayer {
private:
    AudioDecoder audio;
    VideoDecoder video;
    SubtitleRenderer subtitle;
    std::string mediaFile;
    std::string subtitleFile;
public:
    MediaPlayer(const std::string& media, const std::string& srt = "")
        : mediaFile(media), subtitleFile(srt) {}

    void play() {
        // 统一初始化顺序
        audio.init(mediaFile);
        video.init(mediaFile);
        if (!subtitleFile.empty()) {
            subtitle.load(subtitleFile);
        }
        // 开始播放
        audio.play();
        video.play();
        if (!subtitleFile.empty()) {
            subtitle.show();
        }
        std::cout << "播放器全速运行!" << std::endl;
    }

    void stop() {
        audio.stop();
        video.stop();
        subtitle.hide();
        std::cout << "播放器已停止" << std::endl;
    }
};

调用方只需要这样:

int main() {
    MediaPlayer player("movie.mp4", "subtitle.srt");
    player.play();
    // ... 一段时间后
    player.stop();
    return 0;
}

你看,业务代码里完全看不到AudioDecoderVideoDecoder这些类。调用方只跟MediaPlayer打交道。这就是外观模式的价值。

我的习惯:外观类里不要放太多业务逻辑。它只负责编排子系统的调用顺序,不要在里面写if-else判断播放器状态之类的代码。否则外观类会越来越臃肿,最后变成「上帝类」。

外观模式与STL容器的结合

有时候,我们需要管理多个子系统实例。比如一个游戏引擎里可能有多个音频源、多个视频流。这时候可以用STL容器来存储子系统对象,外观类负责统一管理。

class MultiMediaManager {
private:
    std::vector<std::unique_ptr<AudioDecoder>> audioDecoders;
    std::vector<std::unique_ptr<VideoDecoder>> videoDecoders;
public:
    void addAudio(std::unique_ptr<AudioDecoder> audio) {
        audioDecoders.push_back(std::move(audio));
    }
    void playAll() {
        for (auto& a : audioDecoders) a->play();
        for (auto& v : videoDecoders) v->play();
    }
    void stopAll() {
        for (auto& a : audioDecoders) a->stop();
        for (auto& v : videoDecoders) v->stop();
    }
};

这里用std::vectorstd::unique_ptr管理子系统的生命周期。外观类负责遍历、调用。你想想看,如果没有这个外观类,业务代码里得写多少遍for循环?

外观模式的优缺点

优点 缺点
简化调用,降低学习成本 外观类可能成为单点故障
降低子系统与调用方的耦合 不符合开闭原则(修改外观类可能影响所有调用方)
方便统一管理生命周期和调用顺序 如果外观类设计得不好,容易变成「上帝类」

我曾经踩过的坑:有一次我把所有子系统的配置参数都塞进了外观类的构造函数里,结果外观类有十几个参数,调用方每次创建都得传一堆默认值。后来我改用建造者模式来组装外观类,才把这个问题解决。记住:外观类要「薄」,不要「厚」。

外观模式的核心结构

下面这张图展示了外观模式的核心逻辑:

调用方(Client) 外观类(Facade) play() / stop() / init() 子系统A AudioDecoder 子系统B VideoDecoder 子系统C SubtitleRenderer 外观类封装了子系统的复杂调用顺序和接口 调用方只需与外观类交互,无需了解子系统细节

什么时候该用外观模式?

  • 你有一组复杂的子系统,调用方只需要其中一小部分功能。
  • 你想给子系统提供一个简单的入口,降低学习成本。
  • 子系统之间存在依赖关系,调用顺序不能乱。
  • 你想把子系统与业务代码解耦,方便后续替换子系统。

我个人习惯是:只要一个功能涉及三个以上的子系统,我就会考虑用外观模式。哪怕一开始子系统很少,我也先封装一层。因为项目迭代起来,子系统只会越来越多,不会越来越少。

嗯,外观模式其实很简单,但用好它需要一点经验。记住一句话:外观类不是万能胶,它是看门人。它只负责把复杂的东西挡在门外,让调用方看到最简单的一面。

公众号:蓝海资料掘金营,微信 deep3321