前言外观模式Facade Pattern也译作门面模式要解决的问题很具体一个子系统由许多类组成每个类都有自己的开关、参数和调用顺序客户端如果直接操作它们就会被这些细节绑死。外观模式在子系统之上加一层统一的高层接口客户端只调用一两个方法内部按正确顺序协调各个部件。常见误解有两个。第一以为外观模式要封装子系统把内部类藏起来不让用。实际上外观只是提供一条更简单的路径子系统仍然可以直接访问否则就失去了灵活性。第二把它和适配器Adapter混为一谈适配器是把一个已有接口转换成另一个接口外观是把一组接口简化成一个适配器通常包装一个对象外观协调多个对象。本文用家庭影院这个经典例子讲结构门面如何持有子系统、调用顺序为什么重要、中途失败怎么收尾以及它和中介者、单例的边界。代码以 C17 为基准。一、没有外观时的问题假设看一场电影需要五个步骤调暗灯光、降下幕布、打开投影并切换到宽屏、打开功放并设置音量、打开播放器播放。如果客户端直接写// 适用于 C17lights.dim(10);screen.down();projector.on();projector.wideScreenMode();amplifier.on();amplifier.setVolume(8);player.on();player.play(movie.mp4);这段代码有三个问题调用顺序散落在每个客户端里任何一处漏掉或写反都会出问题子系统类的接口一变所有客户端都要改关闭时还要按相反顺序再来一遍重复度极高。外观模式把这些步骤收进一个类// 适用于 C17class HomeTheaterFacade {public:void watchMovie(const std::string title) {lights_.dim(10);screen_.down();projector_.on();projector_.wideScreenMode();amplifier_.on();amplifier_.setVolume(8);player_.on();player_.play(title);}void endMovie() {player_.stop();player_.off();amplifier_.off();projector_.off();screen_.up();lights_.on();}};客户端只剩两行facade.watchMovie(movie.mp4)与facade.endMovie()。顺序、参数、收尾都封装在门面内部。二、门面与子系统的关系门面通常不拥有子系统只持有引用或指针。原因很简单子系统可能被多个门面或客户端共享生命周期不该由门面决定。// 适用于 C17#include stringclass Amplifier {public:void on() { /* 打开 */ }void off() { /* 关闭 */ }void setVolume(int level) { volume_ level; }private:int volume_ 0;};class Projector {public:void on() { /* 打开 */ }void off() { /* 关闭 */ }void wideScreenMode() { /* 切换宽屏 */ }};class Screen {public:void down() { /* 降下 */ }void up() { /* 升起 */ }};class Lights {public:void dim(int level) { level_ level; }void on() { level_ 100; }private:int level_ 100;};class Player {public:void on() { /* 打开 */ }void off() { /* 关闭 */ }void play(const std::string title) { current_ title; }void stop() { current_.clear(); }private:std::string current_;};门面的构造函数接收这些子系统的引用成员存引用。把构造与成员补全后类的形状如下方法体与前一段合起来看// 适用于 C17class HomeTheaterFacade {public:HomeTheaterFacade(Amplifier amp, Projector proj, Screen screen,Lights lights, Player player): amp_(amp), proj_(proj), screen_(screen),lights_(lights), player_(player) {}void watchMovie(const std::string title);void endMovie();private:Amplifier amp_;Projector proj_;Screen screen_;Lights lights_;Player player_;};用引用而不是std::unique_ptr表达的语义是门面不负责这些对象的生死。如果门面确实要独占子系统才改用unique_ptr并在构造时创建。三、和适配器、中介者、单例的区别模式目的对象数量子系统是否仍可直连外观简化一组接口的调用协调多个可以适配器把已有接口转成期望的接口包装一个不涉及中介者让对象之间不直接通信由中介转发协调多个对等对象通常通过中介单例保证全局唯一实例一个不涉及外观是单向的客户端知道门面门面知道子系统子系统不知道门面。中介者是多向的同事对象彼此不直接引用都通过中介交互中介需要理解各方的消息。单例关注的是实例数量和外观解决的不是同一类问题门面可以被实现成单例但这不是必须的多数情况下反而会带来测试困难。四、顺序与异常安全门面最大的价值是把顺序固定下来但顺序也意味着部分失败的可能如果player_.play()抛异常前面的灯、幕布、投影、功放都已经打开此时对象处于半开状态。客户端并不会自动调用endMovie()资源就悬在那里。处理方式有三种门面内 try/catch捕获异常后按逆序回滚已完成的步骤再重新抛出。实现简单但要小心回滚本身也可能失败。RAII 守卫把每一步的资源状态包成对象析构时自动恢复。适合资源本身有明确获取/释放语义的场景。状态记录门面维护一个已经做到第几步的标志endMovie根据标志决定关到哪一步。适合关闭流程可能被重复调用的场景。无论哪种关闭顺序通常与打开顺序相反后打开的先关闭。这不是形式主义——投影先关再升幕布可以避免幕布上残留画面功放先关再关播放器可以避免突然的爆音。顺序写反不一定崩溃但会产生不符合预期的中间状态。五、什么时候不该用外观不是越多越好。如果子系统只有一个类、或者客户端本来就需要精细控制每个部件再加一层门面只会多一次转发。另一个反模式是上帝门面随着需求增加门面类塞进了几十个方法变成所有业务的集中地任何改动都要动它反而增加了耦合。判断标准可以简化为两条客户端是否重复写同样的调用序列子系统的接口变化是否频繁影响客户端。两条都成立时外观收益明显只有一条时可以先抽一个辅助函数不必上升到模式。常见坑点1. 门面持有子系统的所有权却不释放// ❌ 裸指针成员谁来 delete 不明确Amplifier* amp_;// ✅ 不拥有就用引用或非拥有指针Amplifier amp_;2. 门面析构时顺手关闭子系统// ❌ 子系统可能被其他客户端使用析构门面不该改变它们的状态~HomeTheaterFacade() { amp_.off(); }// ✅ 关闭动作放在显式的 endMovie() 里3. 打开与关闭顺序相同// ❌ 先升幕布再关投影画面还打在幕布上lights_.on(); screen_.up(); projector_.off();amplifier_.off(); player_.off();// ✅ 逆序关闭先停播放再关功放、投影最后升幕布、开灯player_.off(); amplifier_.off(); projector_.off();screen_.up(); lights_.on();具体顺序由子系统语义决定不能机械照抄要点是后打开的先关闭以及依赖方先于被依赖方关闭。4. 中途失败不清理// ❌ play 抛异常后前面的设备一直开着void watchMovie(const std::string t) {lights_.dim(10);player_.play(t);}// ✅ 捕获后逆序回滚再重新抛出try { /* ... */ } catch (...) { rollback(); throw; }5. 门面里写业务逻辑// ❌ 门面开始计算折扣、校验权限职责膨胀// ✅ 门面只负责协调调用顺序6. 门面暴露所有子系统方法// ❌ 变成子系统的转发器没有简化任何东西void setVolume(int v) { amp_.setVolume(v); }void wideScreen() { projector_.wideScreenMode(); }// ✅ 只暴露客户端真正需要的高层操作7. 把门面做成全局单例并在其中保存可变状态// ❌ 多线程共享同一个门面状态互相干扰static HomeTheaterFacade instance();// ✅ 门面本身无状态状态放在子系统里8. 忘记子系统仍可直接访问// ❌ 为了封装把子系统类全部设为 private 嵌套类// ✅ 外观是便捷入口不剥夺直接使用的可能总结要点做法职责只协调调用顺序与参数不写业务所有权持有引用/非拥有指针不管理生命周期接口少量高层方法避免变成转发器失败处理逆序回滚或 RAII 守卫与子系统单向依赖子系统不知道门面适用调用序列重复、子系统接口易变外观模式的本质是把怎么做收进一个地方只把做什么暴露出去。它不改变子系统的能力也不隐藏它们只是给常见的使用路径提供一条更短的路。判断是否值得引入看客户端是否在重复同一段编排代码即可如果只是两三个类、一次调用一个普通函数就够了。