82、STL与外观模式:统一接口、子系统封装、简化调用
外观模式,说白了就是给一堆复杂的子系统套个「壳」。
你想想看,平时我们写代码,经常要跟好几个模块打交道。比如初始化一个视频播放器,你得先初始化音频引擎、视频解码器、字幕渲染器、网络流加载器……每个子系统都有自己的接口,调用顺序还不能乱。我早期做项目时,这种代码散落在各个业务逻辑里,改一个地方就得翻遍整个工程,别提多痛苦了。
外观模式就是来解决这个问题的。它提供一个高层接口,把子系统的调用细节全藏起来。调用方只需要跟这个外观类交互,剩下的脏活累活,外观类替你干了。
外观模式的核心思想
说白了就三个字:简化。
- 统一接口:把多个子系统的接口合并成一个简洁的接口。
- 子系统封装:调用方不需要知道子系统内部怎么运作。
- 降低耦合:业务代码只依赖外观类,不依赖具体子系统。
我在项目中遇到过好几次这样的场景:一个功能涉及三四个模块,每个模块的初始化、配置、清理步骤都不一样。如果不做封装,业务代码里全是if-else和try-catch,看着就头疼。外观模式一上,瞬间清爽。
STL里有没有外观模式?
严格来说,STL并没有直接提供「外观模式」这个设计模式的类。但STL的很多容器和算法,其实就扮演了外观的角色。
举个例子:std::vector。它内部管理着动态数组、内存分配器、迭代器、异常安全机制……这些子系统你一个都不用管。你只需要调用push_back、size、at这些简单接口。这就是外观模式的思想——把复杂性藏起来。
再比如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;
}
你看,业务代码里完全看不到AudioDecoder、VideoDecoder这些类。调用方只跟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::vector和std::unique_ptr管理子系统的生命周期。外观类负责遍历、调用。你想想看,如果没有这个外观类,业务代码里得写多少遍for循环?
外观模式的优缺点
| 优点 | 缺点 |
|---|---|
| 简化调用,降低学习成本 | 外观类可能成为单点故障 |
| 降低子系统与调用方的耦合 | 不符合开闭原则(修改外观类可能影响所有调用方) |
| 方便统一管理生命周期和调用顺序 | 如果外观类设计得不好,容易变成「上帝类」 |
我曾经踩过的坑:有一次我把所有子系统的配置参数都塞进了外观类的构造函数里,结果外观类有十几个参数,调用方每次创建都得传一堆默认值。后来我改用建造者模式来组装外观类,才把这个问题解决。记住:外观类要「薄」,不要「厚」。
外观模式的核心结构
下面这张图展示了外观模式的核心逻辑:
什么时候该用外观模式?
- 你有一组复杂的子系统,调用方只需要其中一小部分功能。
- 你想给子系统提供一个简单的入口,降低学习成本。
- 子系统之间存在依赖关系,调用顺序不能乱。
- 你想把子系统与业务代码解耦,方便后续替换子系统。
我个人习惯是:只要一个功能涉及三个以上的子系统,我就会考虑用外观模式。哪怕一开始子系统很少,我也先封装一层。因为项目迭代起来,子系统只会越来越多,不会越来越少。
嗯,外观模式其实很简单,但用好它需要一点经验。记住一句话:外观类不是万能胶,它是看门人。它只负责把复杂的东西挡在门外,让调用方看到最简单的一面。