设计模式 · 第 10 天

Day 10:外观模式(Facade Pattern)

给复杂子系统配一个前台接待员——客户端只点单,不进后厨。

①

今日主题

外观模式(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;
}

这段代码的问题非常明显:

外观模式正是为这个问题而生:把"开机流程"收进一个 ComputerFacade,客户端只调用 facade.start() 一个方法。

③

核心思想

通俗解释:外观模式就是给一个复杂的系统配一个"前台接待员"。你去酒店入住,不需要自己跑到配电房开电、去机房调网络、去洗衣房确认布草,你只需要把身份证递给前台,剩下的事前台替你安排。

生活类比:

三个关键点:

  1. 外观不增加功能:Facade 只是子系统的"简化视图",它调用的还是子系统的原有方法,不包揽业务逻辑。
  2. 单向依赖:客户端依赖 Facade,Facade 依赖子系统;子系统不感知 Facade 的存在,这与中介者模式(Mediator)有本质区别。
  3. 可选择性:外观模式不禁止客户端直接使用子系统——需要细粒度控制的高级用户仍然可以绕过 Facade。
④

UML / 角色关系

用文字图表示依赖方向(注意箭头是单向的):

┌──────────┐  调用   ┌────────────────┐  委派   ┌──────────────┐
│  Client  │ ──────▶ │    Facade      │ ──────▶ │  SubsystemA  │
└──────────┘         │ ComputerFacade │         ├──────────────┤
                     └────────────────┘         │  SubsystemB  │
                                               ├──────────────┤
                                               │  SubsystemC  │
                                               └──────────────┘

角色职责:

依赖方向永远是 Client → Facade → Subsystem,形成一条清晰的分层链,任何一层都看不到更内部的细节。

⑤

最小 C++ 示例

完整可编译的 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 实战示例

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 为例,程序执行分四步:

  1. 构造阶段:main 中构造 ComputerFacade pc;,外观对象作为成员"组合"了 4 个子系统对象(值语义,自动构造/析构,无泄漏)。
  2. 开机编排:pc.start() 依次调用 m_power.on() → 自检 isStable() → m_cpu.powerOn() → m_memory.loadBootloader() → m_hardDrive.readBootSector() → m_cpu.jumpTo(entry),顺序由外观类唯一持有。
  3. 关机编排:pc.shutdown() 按逆序收尾:先 m_cpu.powerOff() 再 m_power.off()——保证"先断电 CPU、后断总电源"的正确顺序,客户端完全不参与判断。
  4. 结束:main 返回 0,pc 析构,所有子系统对象随外观自动释放。

第⑥节的 Qt 示例流程同理:main 构造 NetworkFacade → 调用一次 get() → 外观内部创建 QNetworkRequest/QNetworkReply、挂超时、接 finished 信号 → 事件循环驱动异步完成 → 回调把结果交回客户端 → reply 被 deleteLater() 回收。客户端全程只做"发请求、收结果"两件事。

⑧

为什么这样设计

外观模式的价值全部来自"中间多了一层",逐一拆解:

⑨

不使用会怎样

没有门面,代价会随着调用方数量线性放大:

// 反例:三个客户端各自重复编排,顺序改了要改三处
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 个):

  1. 子系统接口多、步骤有固定顺序,而大多数客户端只关心"最终结果"——例如支付流程(校验→扣款→回调通知)。
  2. 需要为子系统划分层次、提供统一入口——典型如 Service 层对 Repository/基础设施层的封装。
  3. 重构遗留系统:先用门面包住老接口,客户端全部切到门面,再在门面背后逐步替换内部实现,实现"平滑迁移"。
  4. 多个客户端需要完全相同的编排逻辑(DRY 的集中体现)。
  5. 希望隔离易变的第三方依赖(网络栈、数据库驱动、硬件 SDK),日后替换依赖只动门面内部。

不适合的场景(2-4 个):

  1. 子系统只有一两个类、接口本身很简单——门面纯属多余层。
  2. 客户端需要细粒度控制(协议调试、逐包处理、底层参数微调)——门面反而挡路,应让高级用户直连子系统。
  3. 整个系统只有一个客户端且永远不会变——YAGNI,别为不存在的第二个调用方设计。
  4. 门面开始承载业务逻辑、膨胀成"上帝对象"——门面应该是"薄"的,业务逻辑应留在子系统或领域层。

过度设计提醒:门面不是"万能入口"。如果门面只是把子系统的每个方法透传一遍(透传门面=空壳),或者方法超过十几个且各做各的事,说明子系统接口设计有问题,或者应该按业务场景拆成多个门面(如 OrderFacade、UserFacade),而不是一个巨型门面。

⑪

与其他模式区别

vs 适配器模式(Adapter):适配器解决"接口不匹配"——让原本不兼容的类一起工作,通常一对一地转换单个类的接口,目的是"让不能用的能用";外观解决"接口太复杂"——为整个子系统提供简化新接口,子系统之间本来就配合良好,目的是"让能用的更好用"。适配器是翻译官,外观是前台接待员。

vs 中介者模式(Mediator):区别在依赖方向。中介者协调同事对象之间的双向通信,同事对象互相引用中介者(多对多);外观是单向委派,子系统完全不感知门面,门面也不负责子系统之间的互相通信。中介者是交通警察(协调关系),外观是接待员(隐藏细节)。

vs 代理模式(Proxy):代理与原对象接口一致,只是控制访问(延迟加载、权限校验、日志、远程转发);外观提供的是全新简化接口,且不控制对子系统的访问。代理是替身,外观是简化入口。

模式目的接口依赖方向
Facade简化复杂子系统全新高层接口单向,子系统不感知
Adapter接口转换目标接口(转换后)适配器持有被适配者
Mediator协调同事通信同事间通信中枢双向,同事引用中介者
Proxy控制访问与原对象相同代理持有原对象
⑫

Qt 源码中的体现

规律:Qt 中凡是叫 Manager / Engine / Application 的类(QNetworkAccessManager、QFileSystemWatcher、QMediaPlayer……)大多有门面气质——对外一个类,对内协调一堆私有类。这也是 Qt 源码里 xxxPrivate 指针(Pimpl + 门面)如此普遍的原因。

⑬

面试常见问题

Q1:外观模式和适配器模式有什么区别?

目的不同。适配器解决"接口不匹配"——把不兼容的接口转换成客户端期望的接口,通常一对一,目标是让原本不能协作的类协作起来;外观解决"接口太复杂"——为整个子系统提供统一简化入口,一对多,目标是隐藏内部细节。适配器让不能用的能用,外观让能用的更好用。

Q2:外观模式违背开闭原则吗?

不违背。新增子系统或新增高层操作时,只需修改/扩展门面本身,客户端与子系统都不需要改动;子系统只要保持原有接口,门面也无需改。关键约束是门面要"薄"——只做编排、不含业务逻辑,这样门面自身的变动频率才会最低。

Q3:外观模式和中介者模式的区别是什么?

依赖方向相反。中介者协调同事对象之间的双向通信,同事对象互相持有中介者的引用(多对多网状关系);外观是单向委派,子系统完全不感知门面存在。中介者重在"协调",外观重在"隐藏"。如果同事对象之间本来不需要互相通信,就用外观而不是中介者。

Q4:门面会影响性能吗?如何避免?

门面只是多了一层间接调用,开销通常可忽略;但要避免门面在每次调用中重复创建子系统对象(应持有成员引用),避免在热路径中做无谓的转发或复制。如果门面成为性能瓶颈,说明调用频率和层次设计有问题,需要重新审视粒度。

Q5:如何避免门面变成"上帝对象"?

三个手段:① 门面只编排、不写业务逻辑;② 按业务场景拆分多个门面(OrderFacade / UserFacade / PaymentFacade),而不是一个系统一个门面;③ 如果门面需要透传大量方法,说明子系统接口本身设计不合理——先重构子系统,而不是继续加门面。

⑭

今日练习(15-30 分钟)

需求:为"智能家居"系统编写外观类 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——隔离变化,这正是门面最核心的价值。)

⑮

今日总结

一句话记忆:外观模式 = 给复杂子系统配一个前台接待员,客户端只点单,不进后厨。

代码特征信号(看到这些就是门面的味道):

  1. 客户端只调用一个对象的 1-2 个方法就完成整套复杂流程;
  2. 该类内部组合(持有)多个子系统对象,方法体是"编排调用"而非"功能实现";
  3. 子系统类完全不知道门面存在(没有反向依赖、没有门面指针);
  4. 多个客户端共享同一个入口,编排逻辑在代码库里只出现一份;
  5. 新增流程环节只改门面,客户端与子系统零改动。

明日预告:Day 11 享元模式(Flyweight)——当程序需要创建成千上万个相似对象时,如何通过"共享"让一万个对象只占一份内存?它和单例、对象池又有什么区别?