精读C++20设计模式——结构型设计模式:外观模式
精读C++20设计模式——结构型设计模式:外观模式
前言
⚠:笔者的这个设计模式谈不上了解,甚至可以说是现场学习的。所以这个部分会有所混杂GPT的生成内容,因此请您谨慎参考!
外观模式简直就是字如其名:我们一切跟着外观走——倒不如说,我们将接口实现分离的策略重新换了一个更好的名称:外观模式(Facade)
没太看懂?GPT是这样打比方的:想象你走进一家大型酒店,前台接待小姐很少把厨房、保洁、安保、工程维护这些后端细节告诉你。你只要对前台说“我要入住”,她帮你办好一切——房卡、叫客服、通知打扫、开启电梯口令。这就是外观模式的直观比喻:对外提供一个简单、统一的接口,把一堆复杂子系统的交互和时序隐藏在幕后,让客户端只关心“我要做什么”,而不用管“怎么做”。
其实这就是我们自然的一个想法——我们完全可以将更多更加低级的模块有机的组合起来,针对调用方的需求组合,我们就产生了客户端特供的表现形式。
笔者认为处于这个角度,外观模式不是一个设计模式而是一个设计的思路——不是发明新能力,而是把已有的能力按职责做有意义的封装:减少依赖、降低耦合、集中处理通用流程与错误边界,从而让上层代码更专注于业务逻辑。
说一个笔者的例子
笔者之前给IMX6ULL开发板有做过一个多媒体播放器。类名有些忘记了,但是就是这样做的:有 NetworkStream
负责拉流、AudioDecoder
和 VideoDecoder
负责解码、Renderer
负责显示、SubtitleEngine
负责加载字幕。最原始的客户端代码可能像这样:
// client.cpp(无外观,直接调用子系统)
NetworkStream stream;
stream.open(url);
auto rawPacket = stream.readPacket();VideoDecoder vdec;
vdec.init(codecParams);
vdec.sendPacket(rawPacket);
vdec.decodeFrame();AudioDecoder adec;
adec.init(codecParams);
adec.sendPacket(rawPacket);
adec.decodeFrame();Renderer renderer;
renderer.setup(window);
renderer.renderFrame(vdec.getFrame(), adec.getFrame());SubtitleEngine sub;
sub.load(subUrl);
sub.sync(renderer.getTimestamp());
你看到了嘛?如果我们直接将这些代码堆放到MainWindows里,这太麻烦了。完全可以把这一系列Sequence做一个抽象,把资源托管,异常处理和协调工作的部分组合起来。
第一改:引入简单门面(单一职责的入口)
笔者意识到这个问题之后,立马封装了一个新的类,这里咱们就在讲设计模式,就叫做 MediaPlayerFacade
吧!它的职责是:接受一个播放请求,协调子系统按正确顺序工作,并在出错时负责清理与报告。客户端只需要一行代码就能播放视频:player.play(url)
。
// MediaPlayerFacade.h
class MediaPlayerFacade {
public:MediaPlayerFacade();~MediaPlayerFacade();bool play(const std::string& url); // 简单接口:成功/失败void stop();private:std::unique_ptr<NetworkStream> stream_;std::unique_ptr<VideoDecoder> vdec_;std::unique_ptr<AudioDecoder> adec_;std::unique_ptr<Renderer> renderer_;std::unique_ptr<SubtitleEngine> subtitle_;
};
实现中,play()
会负责打开流、初始化解码器、循环读包并驱动渲染。任何子系统细节(如何重试、缓冲策略、错误映射)都封装在门面里。客户端调用变得简单且稳定:
MediaPlayerFacade player;
if (!player.play("https://example.com/stream.m3u8")) {std::cerr << "播放失败\n";
}
现在我们惊喜的发现,我们直接拿到了一个媒体播放器模块了,可以随意的按照我们的想法进行调整,而不用自己手动调整下面的一堆子模块的协作!
第二改:把异常、资源与日志放到门面里
门面最有价值的地方之一是能统一处理错误和资源生命周期。把初始化失败的清理逻辑放在门面中,客户端无需关心。示例实现(伪代码与关键片段):
bool MediaPlayerFacade::play(const std::string& url) {try {stream_ = std::make_unique<NetworkStream>();stream_->open(url);auto vidParams = stream_->getVideoParams();vdec_ = std::make_unique<VideoDecoder>();if (!vdec_->init(vidParams)) throw std::runtime_error("vdec init failed");auto audParams = stream_->getAudioParams();adec_ = std::make_unique<AudioDecoder>();if (!adec_->init(audParams)) throw std::runtime_error("adec init failed");renderer_ = std::make_unique<Renderer>();renderer_->setup(window_);// 主循环(简化)while (running_) {auto pkt = stream_->readPacket();if (pkt.isVideo()) vdec_->sendPacket(pkt);if (pkt.isAudio()) adec_->sendPacket(pkt);auto vfrm = vdec_->getFrameIfReady();auto afrm = adec_->getFrameIfReady();if (vfrm || afrm) renderer_->renderFrame(vfrm, afrm);}return true;} catch (const std::exception& e) {LOG_ERROR("播放失败: {}", e.what());stop(); // 统一清理return false;}
}
现在,我们还让它具备了异常处理的能力了!后面,笔者也做了异步处理和通知处理,但是这就离我们的话题有点远了,感兴趣的朋友可自行研究。
何时使用外观(以及何时别用它)
外观最适合用在“你需要把复杂子系统的协作逻辑封装成单一入口”的场景:例如启动/关闭一组服务、媒体播放、数据库连接池的统一管理、复杂构建流程(编译器的前端构建 API)等。优点在于减少客户端与细节耦合、集中处理错误和生命周期、便于修改底层而不干扰使用者。
但如果你滥用外观,把所有细节都全包进一个“大门面”,门面很容易变成“上帝对象(God Object)”:它积累太多逻辑,把原本应该由子模块负责的职责都吞下,导致单元测试变得困难,代码也更难维护。外观应该是“门面”,不是“重写子系统的替代品”;它应组织调用与抽象流程,而不是重新实现业务逻辑。