给复杂子系统配一个前台接待员——客户端只点单,不进后厨。
外观模式(Facade Pattern):为子系统中的一组接口提供一个统一的、简化的高层接口,使得客户端只需要面对一个"门面"对象,就能完成原本需要与多个子系统类逐个交互的复杂操作。它属于结构型模式,核心是"简化"与"隔离"——不增加任何新功能,只负责把复杂编排藏起来。
先看一段没有外观模式的坏代码。假设我们要写一个"电脑开机"程序,电脑由电源(PowerSupply)、CPU、内存(Memory)、硬盘(HardDrive)四个部件组成,开机需要严格的先后顺序:先供电、再 CPU 上电、加载引导程序、读引导扇区、跳转执行。客户端(main)被迫直接操作所有部件:
// 坏味道:客户端直接操作子系统,编排逻辑散落在调用方
#include <iostream>
#include <string>
class PowerSupply {
public:
void on() const { std::cout << "[电源] 供电\n"; }
bool isStable() const { return true; }
};
class Cpu {
public:
void powerOn() const { std::cout << "[CPU] 上电\n"; }
};
class Memory {
public:
void loadBootloader() const { std::cout << "[内存] 加载引导\n"; }
};
class HardDrive {
public:
unsigned long readBootSector() const { return 0x7C00; }
};
int main() {
PowerSupply ps; Cpu cpu; Memory mem; HardDrive hd;
// 客户端必须知道全部细节和顺序:供电 → 自检 → CPU → 引导 → 跳转
ps.on();
if (!ps.isStable()) { std::cout << "供电异常\n"; return -1; }
cpu.powerOn();
mem.loadBootloader();
unsigned long entry = hd.readBootSector();
cpu.jumpTo(entry); // 假设 Cpu 有 jumpTo
std::cout << "系统启动\n";
return 0;
}
这段代码的问题非常明显:
readBootSector 改返回结构体),所有客户端都要跟着改。外观模式正是为这个问题而生:把"开机流程"收进一个 ComputerFacade,客户端只调用 facade.start() 一个方法。
通俗解释:外观模式就是给一个复杂的系统配一个"前台接待员"。你去酒店入住,不需要自己跑到配电房开电、去机房调网络、去洗衣房确认布草,你只需要把身份证递给前台,剩下的事前台替你安排。
生活类比:
三个关键点:
用文字图表示依赖方向(注意箭头是单向的):
┌──────────┐ 调用 ┌────────────────┐ 委派 ┌──────────────┐
│ Client │ ──────▶ │ Facade │ ──────▶ │ SubsystemA │
└──────────┘ │ ComputerFacade │ ├──────────────┤
└────────────────┘ │ SubsystemB │
├──────────────┤
│ SubsystemC │
└──────────────┘
角色职责:
依赖方向永远是 Client → Facade → Subsystem,形成一条清晰的分层链,任何一层都看不到更内部的细节。
完整可编译的 C++17 程序(保存为 facade_demo.cpp,g++ -std=c++17 facade_demo.cpp -o facade_demo):
// facade_demo.cpp —— 外观模式最小示例:一键开机
#include <iostream>
// ========== 子系统 1:电源 ==========
class PowerSupply {
public:
void on() const { std::cout << "[电源] 市电接入,输出稳定电压\n"; }
bool isStable() const { return true; } // 供电自检
void off() const { std::cout << "[电源] 切断电源\n"; }
};
// ========== 子系统 2:CPU ==========
class Cpu {
public:
void powerOn() const { std::cout << "[CPU] 上电复位,初始化寄存器\n"; }
void powerOff() const { std::cout << "[CPU] 停机,保存现场\n"; }
void jumpTo(unsigned long addr) const {
std::cout << "[CPU] 跳转到引导地址 0x" << std::hex << addr << std::dec << "\n";
}
};
// ========== 子系统 3:内存 ==========
class Memory {
public:
void loadBootloader() const { std::cout << "[内存] 加载引导程序\n"; }
};
// ========== 子系统 4:硬盘 ==========
class HardDrive {
public:
unsigned long readBootSector() const {
std::cout << "[硬盘] 读取主引导扇区\n";
return 0x7C00; // 引导代码入口地址
}
};
// ========== 外观(Facade):统一的高层开机/关机接口 ==========
class ComputerFacade {
public:
void start() const { // 一键开机:编排复杂流程
m_power.on();
if (!m_power.isStable()) {
std::cout << ">> 供电异常,中止开机\n";
return;
}
m_cpu.powerOn();
m_memory.loadBootloader();
const unsigned long entry = m_hardDrive.readBootSector();
m_cpu.jumpTo(entry);
std::cout << ">> 系统启动完成,进入操作系统\n";
}
void shutdown() const { // 一键关机:逆序收尾
std::cout << ">> 系统保存状态,执行关机\n";
m_cpu.powerOff();
m_power.off();
}
private:
// 外观"组合"子系统对象(组合优于继承)
PowerSupply m_power;
Cpu m_cpu;
Memory m_memory;
HardDrive m_hardDrive;
};
int main() {
ComputerFacade pc; // 客户端只认识这一个"前台"
pc.start(); // 一个调用,背后四步
pc.shutdown();
return 0;
}
程序输出:
[电源] 市电接入,输出稳定电压
[CPU] 上电复位,初始化寄存器
[内存] 加载引导程序
[硬盘] 读取主引导扇区
[CPU] 跳转到引导地址 0x7c00
>> 系统启动完成,进入操作系统
>> 系统保存状态,执行关机
[CPU] 停机,保存现场
[电源] 切断电源
对比第②节的坏代码:客户端从"知道 4 个类 + 5 个步骤"变成"只知道 1 个类 + 1 个方法",后续加装独立显卡、指纹验证等新环节,只需要改 ComputerFacade::start(),main 一行不动。
Qt 自身就是外观模式的重度使用者:QNetworkAccessManager 是 HTTP 协议栈、SSL 握手、代理、重定向、Cookie、缓存、连接池的"门面"——你只需要 get()/post(),协议细节全被藏起。下面我们在它之上再包一层 NetworkFacade,把"超时保护 + 错误归一化 + 异步回调"也藏进门面,让调用方一行代码完成 HTTP GET。
network_facade.h:
// network_facade.h —— 外观类:向客户端提供"发一个 GET 请求"的简单接口
#pragma once
#include <QObject>
#include <QUrl>
#include <functional>
class QNetworkAccessManager;
class NetworkFacade : public QObject {
Q_OBJECT
public:
explicit NetworkFacade(QObject* parent = nullptr);
// 唯一的高层接口:GET 一个 URL,成功/失败通过回调返回
// 客户端完全不接触 QNetworkRequest / QNetworkReply / 超时处理
void get(const QUrl& url,
std::function<void(const QByteArray&)> onSuccess,
std::function<void(const QString&)> onError);
private:
QNetworkAccessManager* m_manager = nullptr; // 内部子系统,对客户端不可见
};
network_facade.cpp:
// network_facade.cpp
#include "network_facade.h"
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QTimer>
NetworkFacade::NetworkFacade(QObject* parent)
: QObject(parent)
, m_manager(new QNetworkAccessManager(this)) { // RAII:随外观析构释放
}
void NetworkFacade::get(const QUrl& url,
std::function<void(const QByteArray&)> onSuccess,
std::function<void(const QString&)> onError) {
QNetworkRequest request(url);
QNetworkReply* reply = m_manager->get(request);
// 10 秒超时保护:超时直接 abort,随后会触发 finished 信号
QTimer::singleShot(10000, reply, &QNetworkReply::abort);
connect(reply, &QNetworkReply::finished, this,
[=, onSuccess = std::move(onSuccess), onError = std::move(onError)]() {
reply->deleteLater(); // 安全回收 reply
if (reply->error() != QNetworkReply::NoError) {
onError(reply->errorString()); // 错误统一归一化
} else {
onSuccess(reply->readAll()); // 成功返回响应体
}
});
}
main.cpp:
// main.cpp —— 客户端只面对 NetworkFacade 一个门面
#include <QCoreApplication>
#include <QDebug>
#include "network_facade.h"
int main(int argc, char* argv[]) {
QCoreApplication app(argc, argv);
NetworkFacade net;
net.get(QUrl("https://example.com"),
[](const QByteArray& body) { // 成功回调
qDebug() << "成功,收到" << body.size() << "字节";
QCoreApplication::quit();
},
[](const QString& err) { // 失败回调
qDebug() << "失败:" << err;
QCoreApplication::exit(1);
});
return app.exec();
}
CMakeLists.txt:
cmake_minimum_required(VERSION 3.16)
project(network_facade_demo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON) # Q_OBJECT 宏需要 moc
find_package(Qt6 REQUIRED COMPONENTS Core Network)
qt_add_executable(network_facade_demo
main.cpp
network_facade.h
network_facade.cpp
)
target_link_libraries(network_facade_demo PRIVATE Qt6::Core Qt6::Network)
构建运行:cmake -B build && cmake --build build && ./build/network_facade_demo。今后调用方想加超时重试、加 HTTP 头、加日志,都只改 NetworkFacade 内部——这就是门面在真实项目里的价值。
以第⑤节 ComputerFacade 为例,程序执行分四步:
main 中构造 ComputerFacade pc;,外观对象作为成员"组合"了 4 个子系统对象(值语义,自动构造/析构,无泄漏)。pc.start() 依次调用 m_power.on() → 自检 isStable() → m_cpu.powerOn() → m_memory.loadBootloader() → m_hardDrive.readBootSector() → m_cpu.jumpTo(entry),顺序由外观类唯一持有。pc.shutdown() 按逆序收尾:先 m_cpu.powerOff() 再 m_power.off()——保证"先断电 CPU、后断总电源"的正确顺序,客户端完全不参与判断。main 返回 0,pc 析构,所有子系统对象随外观自动释放。第⑥节的 Qt 示例流程同理:main 构造 NetworkFacade → 调用一次 get() → 外观内部创建 QNetworkRequest/QNetworkReply、挂超时、接 finished 信号 → 事件循环驱动异步完成 → 回调把结果交回客户端 → reply 被 deleteLater() 回收。客户端全程只做"发请求、收结果"两件事。
外观模式的价值全部来自"中间多了一层",逐一拆解:
ComputerFacade::start();新增高层操作(如 sleep() 休眠)只需要给 Facade 加一个方法。客户端与子系统双双"对修改关闭,对扩展开放"。IPowerController,客户端持有接口指针,测试时注入 Mock。没有门面,代价会随着调用方数量线性放大:
readBootSector() 改返回结构体),编译错误会出现在所有客户端文件里——高耦合的直接证据。// 反例:三个客户端各自重复编排,顺序改了要改三处
void wakeByTimer() { ps.on(); if (!ps.isStable()) return; cpu.powerOn(); mem.loadBootloader(); ... }
void wakeByRemote() { ps.on(); if (!ps.isStable()) return; cpu.powerOn(); mem.loadBootloader(); ... }
void wakeByManual() { ps.on(); if (!ps.isStable()) return; cpu.powerOn(); mem.loadBootloader(); ... }
// 新增"内存自检"步骤 → 三处全要改,漏一处 = bug
一句话:没有门面,每个客户端都变成了半个系统架构师——它们不得不了解不属于自己的内部细节。
适合的场景(3-5 个):
不适合的场景(2-4 个):
过度设计提醒:门面不是"万能入口"。如果门面只是把子系统的每个方法透传一遍(透传门面=空壳),或者方法超过十几个且各做各的事,说明子系统接口设计有问题,或者应该按业务场景拆成多个门面(如 OrderFacade、UserFacade),而不是一个巨型门面。
vs 适配器模式(Adapter):适配器解决"接口不匹配"——让原本不兼容的类一起工作,通常一对一地转换单个类的接口,目的是"让不能用的能用";外观解决"接口太复杂"——为整个子系统提供简化新接口,子系统之间本来就配合良好,目的是"让能用的更好用"。适配器是翻译官,外观是前台接待员。
vs 中介者模式(Mediator):区别在依赖方向。中介者协调同事对象之间的双向通信,同事对象互相引用中介者(多对多);外观是单向委派,子系统完全不感知门面,门面也不负责子系统之间的互相通信。中介者是交通警察(协调关系),外观是接待员(隐藏细节)。
vs 代理模式(Proxy):代理与原对象接口一致,只是控制访问(延迟加载、权限校验、日志、远程转发);外观提供的是全新简化接口,且不控制对子系统的访问。代理是替身,外观是简化入口。
| 模式 | 目的 | 接口 | 依赖方向 |
|---|---|---|---|
| Facade | 简化复杂子系统 | 全新高层接口 | 单向,子系统不感知 |
| Adapter | 接口转换 | 目标接口(转换后) | 适配器持有被适配者 |
| Mediator | 协调同事通信 | 同事间通信中枢 | 双向,同事引用中介者 |
| Proxy | 控制访问 | 与原对象相同 | 代理持有原对象 |
get()/post(),背后是 HTTP 协议栈、SSL/TLS 握手、代理、重定向、Cookie、缓存、连接池一整组子系统。它内部协调大量私有类(QNetworkAccessManagerPrivate、QNetworkReply 等),对外只暴露"发请求、收信号"。app.exec() 和 app.quit(),平台细节(X11/Wayland/Windows)全部藏在背后。QSqlDatabase/QSqlQuery 接口,换数据库只改连接字符串,业务代码零改动。start() 隐藏了 fork/exec 或 CreateProcess 的全部细节。规律:Qt 中凡是叫 Manager / Engine / Application 的类(QNetworkAccessManager、QFileSystemWatcher、QMediaPlayer……)大多有门面气质——对外一个类,对内协调一堆私有类。这也是 Qt 源码里 xxxPrivate 指针(Pimpl + 门面)如此普遍的原因。
Q1:外观模式和适配器模式有什么区别?
目的不同。适配器解决"接口不匹配"——把不兼容的接口转换成客户端期望的接口,通常一对一,目标是让原本不能协作的类协作起来;外观解决"接口太复杂"——为整个子系统提供统一简化入口,一对多,目标是隐藏内部细节。适配器让不能用的能用,外观让能用的更好用。
Q2:外观模式违背开闭原则吗?
不违背。新增子系统或新增高层操作时,只需修改/扩展门面本身,客户端与子系统都不需要改动;子系统只要保持原有接口,门面也无需改。关键约束是门面要"薄"——只做编排、不含业务逻辑,这样门面自身的变动频率才会最低。
Q3:外观模式和中介者模式的区别是什么?
依赖方向相反。中介者协调同事对象之间的双向通信,同事对象互相持有中介者的引用(多对多网状关系);外观是单向委派,子系统完全不感知门面存在。中介者重在"协调",外观重在"隐藏"。如果同事对象之间本来不需要互相通信,就用外观而不是中介者。
Q4:门面会影响性能吗?如何避免?
门面只是多了一层间接调用,开销通常可忽略;但要避免门面在每次调用中重复创建子系统对象(应持有成员引用),避免在热路径中做无谓的转发或复制。如果门面成为性能瓶颈,说明调用频率和层次设计有问题,需要重新审视粒度。
Q5:如何避免门面变成"上帝对象"?
三个手段:① 门面只编排、不写业务逻辑;② 按业务场景拆分多个门面(OrderFacade / UserFacade / PaymentFacade),而不是一个系统一个门面;③ 如果门面需要透传大量方法,说明子系统接口本身设计不合理——先重构子系统,而不是继续加门面。
需求:为"智能家居"系统编写外观类 HomeFacade。子系统:Light(灯光)、AirConditioner(空调)、Curtain(窗帘)。要求提供两个高层接口:leaveHome()(离家:关灯、关空调、拉上窗帘)和 arriveHome()(回家:开灯、空调设为 26℃、拉开窗帘)。用 C++17 写一个可编译的小程序,main 中只调用这两个方法,并打印每个子系统的动作。
提示 1:子系统各自有独立接口,例如 Light::on()/off()、AirConditioner::setTemperature(int)/off()、Curtain::open()/close()。先写子系统,再写门面,最后写 main——注意 main 里不要出现任何子系统的名字。
提示 2(思考题):如果新增子系统"安防摄像头 Camera",要求离/回家时联动布防/撤防——客户端代码需要改动吗?这验证了外观模式的哪个价值?(参考答案:客户端零改动,只需改 HomeFacade——隔离变化,这正是门面最核心的价值。)
代码特征信号(看到这些就是门面的味道):
明日预告:Day 11 享元模式(Flyweight)——当程序需要创建成千上万个相似对象时,如何通过"共享"让一万个对象只占一份内存?它和单例、对象池又有什么区别?