Day 12:代理模式(Proxy Pattern)

给真实对象找一个「替身」:接口一字不改,访问权限、加载时机、网络传输全部由替身代劳。

结构型模式 GoF 23 · 第 12/23 天 2026-08-24

① 今日主题

代理模式(Proxy Pattern)是一种结构型设计模式:它为另一个对象提供一个替身或占位符(surrogate),由这个替身来控制对原对象的访问。关键约束有三条:代理与真实对象实现同一个接口;客户端只面向接口编程,甚至不知道真实对象的存在;代理在把调用转发给真实对象之前或之后,插入额外的控制逻辑——比如权限校验、延迟加载、缓存、日志、网络传输。

一句话:「接口不变,控制先行」——真实对象躲在幕后干活,代理在前台把关。

② 为什么需要这个模式

先看一段没有代理的坏代码:一个图片查看器直接操作「大图」对象,权限检查、加载时机全部由调用方负责。

#include <iostream>
#include <string>

// 坏设计:一张加载代价极高的"大图"
class BigImage {
public:
    explicit BigImage(const std::string& f) : file(f) {
        // 构造即从磁盘加载,占用 200MB 内存
        std::cout << "从磁盘加载 " << file << ",占用 200MB 内存\n";
    }
    void show() const { std::cout << "显示 " << file << "\n"; }
private:
    std::string file;
};

// 调用方:既要做业务,又要做权限检查、又要操心对象是否已加载
void showImage(BigImage* img, bool isAdmin) {
    if (!isAdmin) {                 // 权限逻辑写死在调用方
        std::cout << "无权限\n";
        return;
    }
    if (img) img->show();           // 空指针判断也要调用方操心
}

int main() {
    BigImage img("wallpaper.jpg");  // 程序一启动就加载了 200MB 大图!
    showImage(&img, true);          // 每次调用都要手动传权限标志
    return 0;
}

这段代码的问题非常典型:

  • 启动即加载:大图在 main 里就被构造,哪怕用户根本不会打开它,200MB 内存照样被吃掉——没有「按需加载」的机制。
  • 权限逻辑散落各处:每新增一个调用点,就要复制一遍 if (!isAdmin) 的判断。以后权限规则从「管理员」变成「管理员或 VIP」,所有调用处都要改——违背开闭原则(OCP)。
  • 调用方职责过载:业务展示、权限校验、生命周期管理三件事挤在一个函数里——违背单一职责原则(SRP)。
  • 面向具体类编程:调用方直接依赖 BigImage,想换成缓存版、远程版、日志版都得改调用方代码——违背依赖倒置原则(DIP),应该依赖抽象接口。

代理模式就是来解决这一整类问题的:把「访问控制」从调用方手里夺回来,放进一个与真实对象同接口的替身里,让调用方只跟替身打交道。

③ 核心思想

代理模式的核心思想可以拆成三步:① 让代理和真实对象实现同一个接口(客户端无感知替换);② 代理持有真实对象的引用(转发的基础);③ 在转发前后插入控制逻辑(代理存在的全部价值)。

生活类比——明星经纪人:你(客户端)想请某明星(真实对象)演出,你不会直接找到明星本人,而是联系他的经纪人(代理)。经纪人手里握着明星的档期(引用),但在答应你之前会先审核报价、确认档期、检查合同(控制逻辑)。对你来说,你谈的对象始终是「能演出的人」这个角色(接口),经纪人只是这个角色在台前的执行者。明星本人可以非常忙(昂贵),但经纪人的存在让你可以随时先谈意向,而不必惊动明星。

另一个类比——信用卡:你购物时刷卡(代理),银行系统在后台对真实账户(真实对象)做结算。你手里那张塑料卡片,就是账户的「占位符」:它让「支付」这个行为不必每次直接操作真实账户,额度校验、记账、风控全在卡片背后完成。

再一个技术场景类比——代客泊车:你把车钥匙交给门童(代理),门童替你完成停车(真实操作)。你不需要知道车停在哪、怎么停,只需要「把车交出去」这个接口。

注意代理模式的核心价值在于「控制」而非「增强」:装饰器是往对象上加功能,代理是替对象做访问管理。这个区别在章节⑪会重点辨析。

④ UML / 角色关系

代理模式只有四个角色,结构上非常简洁:

┌───────────────────────────────────────────────┐
│                  Client(客户端)                │
│          只面向 Subject 接口编程,              │
│          不知 RealSubject 的存在               │
└──────────────────────┬────────────────────────┘
                       │ 调用接口
                       ▼
┌───────────────────────────────────────────────┐
│            <<interface>>  Subject              │
│                + request()                     │
└───────────┬───────────────────┬───────────────┘
            │ 实现               │ 实现
┌───────────▼──────────┐  ┌──────▼────────────────┐
│        Proxy         │  │     RealSubject       │
│ - real: RealSubject  │──▶│ 真正干活的对象        │
│ + request()          │  │ + request()           │
│   检查权限/缓存/日志  │  │                       │
│   然后转发给 real    │  │                       │
└──────────────────────┘  └───────────────────────┘
角色职责在本日示例中
Subject(抽象主题)定义真实对象与代理共有的接口,让客户端能无差别对待两者Image 接口
RealSubject(真实主题)真正执行业务逻辑的对象,通常是昂贵、敏感或远程的RealImage
Proxy(代理)持有 RealSubject 的引用;在转发调用前后插入权限校验、懒加载、缓存、日志等控制逻辑;负责 RealSubject 的创建与生命周期ImageProxy
Client(客户端)只依赖 Subject 接口,不直接接触 RealSubjectmain()

按用途,代理可分为五种常见变体:虚拟代理(延迟创建昂贵对象)、保护代理(访问控制)、远程代理(屏蔽网络细节,如 gRPC 的 stub)、缓存代理(缓存结果)、智能引用代理(引用计数、加锁、写时复制)。

⑤ 最小 C++ 示例

用 C++17 实现一个「延迟加载 + 访问控制」的图片代理。完整可编译,无需任何第三方库:

#include <iostream>
#include <memory>
#include <string>

// ── 抽象主题(Subject):图片的统一接口 ──
class Image {
public:
    virtual ~Image() = default;          // 基类必须虚析构,否则删除派生类会泄漏
    virtual void display() = 0;          // 纯虚函数:所有图片都能显示
};

// ── 真实主题(RealSubject):磁盘上的一张"昂贵"大图 ──
class RealImage : public Image {
public:
    explicit RealImage(std::string file) : m_file(std::move(file)) {
        loadFromDisk();                  // 构造即加载 —— 代价高昂
    }
    void display() override {
        std::cout << "[RealImage] 显示 " << m_file << std::endl;
    }
private:
    void loadFromDisk() const {
        std::cout << "[RealImage] 从磁盘加载 " << m_file
                  << "(耗时 3 秒)" << std::endl;
    }
    std::string m_file;
};

// ── 代理(Proxy):真实图片的替身,负责延迟加载 + 访问控制 ──
class ImageProxy : public Image {
public:
    ImageProxy(std::string file, bool hasPermission)
        : m_file(std::move(file)), m_hasPermission(hasPermission) {}

    void display() override {
        // ① 保护代理:无权限直接拦截,真实对象根本不会被创建
        if (!m_hasPermission) {
            std::cout << "[Proxy] 拒绝访问:" << m_file
                      << "(无权限)" << std::endl;
            return;
        }
        // ② 虚拟代理:第一次访问才创建昂贵的真实对象
        if (!m_real) {
            std::cout << "[Proxy] 首次访问,创建真实对象(延迟加载)" << std::endl;
            m_real = std::make_unique<RealImage>(m_file);
        }
        // ③ 转发调用:真实对象干活,代理只做记录
        m_real->display();
        ++m_accessCount;
    }

    int accessCount() const { return m_accessCount; }

private:
    std::string m_file;
    bool m_hasPermission;
    std::unique_ptr<RealImage> m_real;   // 初始为空:真实对象可以"不存在"
    int m_accessCount = 0;               // 顺带演示"统计访问次数"的代理能力
};

int main() {
    // 客户端只持有代理,完全不知道 RealImage 的存在
    auto proxy = std::make_unique<ImageProxy>("wallpaper.jpg", true);
    proxy->display();                    // 第一次:触发懒加载
    proxy->display();                    // 第二次:直接复用,不再加载

    auto denied = std::make_unique<ImageProxy>("secret.jpg", false);
    denied->display();                   // 无权限:被拦截

    std::cout << "wallpaper.jpg 共被访问 " << proxy->accessCount()
              << " 次" << std::endl;
    return 0;
}

程序输出:

[Proxy] 首次访问,创建真实对象(延迟加载)
[RealImage] 从磁盘加载 wallpaper.jpg(耗时 3 秒)
[RealImage] 显示 wallpaper.jpg
[RealImage] 显示 wallpaper.jpg
[Proxy] 拒绝访问:secret.jpg(无权限)
wallpaper.jpg 共被访问 2 次

注意三处细节:① 基类析构函数声明为 virtual,这是多态删除的底线;② 用 std::unique_ptr 管理真实对象,代理析构时自动释放,无内存泄漏;③ 被拒绝访问时真实对象从未创建——代理让「昂贵对象」可以安全地「不存在」。

⑥ Qt 实战示例

Qt 框架本身就是代理模式的最大受益者与最佳示范:Model/View 架构中的 QSortFilterProxyModel 就是一个教科书级的代理——它与源模型实现同一套接口(QAbstractItemModel),视图(QTableView)只认识它,而它在转发请求前插入「排序 + 过滤」逻辑。下面是一个完整可运行的 Qt6 示例:员工名单里只显示「研发部」的行。

// main.cpp —— 用 QSortFilterProxyModel 过滤员工名单
#include <QApplication>
#include <QTableView>
#include <QStandardItemModel>
#include <QSortFilterProxyModel>
#include <QStringList>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    // 1. 源模型:全体员工数据(真实对象)
    auto *source = new QStandardItemModel(0, 2);
    source->setHeaderData(0, Qt::Horizontal, QStringLiteral("姓名"));
    source->setHeaderData(1, Qt::Horizontal, QStringLiteral("部门"));

    auto addRow = [source](const QString& name, const QString& dept) {
        auto *nameItem = new QStandardItem(name);
        auto *deptItem = new QStandardItem(dept);
        source->appendRow({nameItem, deptItem});
    };
    addRow(QStringLiteral("张三"), QStringLiteral("研发部"));
    addRow(QStringLiteral("李四"), QStringLiteral("市场部"));
    addRow(QStringLiteral("王五"), QStringLiteral("研发部"));
    addRow(QStringLiteral("赵六"), QStringLiteral("行政部"));

    // 2. 代理模型:只"透传"研发部的行(保护代理 + 过滤逻辑)
    auto *proxy = new QSortFilterProxyModel;
    proxy->setSourceModel(source);          // 代理持有真实模型
    proxy->setFilterKeyColumn(1);           // 按"部门"列过滤
    proxy->setFilterFixedString(QStringLiteral("研发部"));

    // 3. 视图只认识代理模型,完全不感知源模型(客户端面向接口)
    QTableView view;
    view.setModel(proxy);
    view.resize(320, 220);
    view.show();

    return app.exec();
}
# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(qt-proxy-demo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)               # Qt 元对象编译器

find_package(Qt6 REQUIRED COMPONENTS Widgets)

add_executable(qt-proxy-demo main.cpp)
target_link_libraries(qt-proxy-demo PRIVATE Qt6::Widgets)

编译运行:cmake -B build && cmake --build build && ./build/qt-proxy-demo,窗口中将只显示张三、王五两行。这里 QSortFilterProxyModel 的角色与我们的 ImageProxy 完全同构:接口一致(都是 QAbstractItemModel)、持有真实对象(setSourceModel)、转发前插入逻辑(filterAcceptsRow 决定哪些行可见)。如果把真实模型换成数据库模型 QSqlTableModel,视图代码一行都不用改——代理模式让「数据来源」与「展示层」彻底解耦。

⑦ 代码执行流程

以最小 C++ 示例的两次 display() 调用为主线,完整执行流程如下:

  1. 构造阶段:main 创建 ImageProxy。此刻只保存了文件名和权限标志,m_real 为 nullptr——昂贵的 RealImage 还不存在。
  2. 第一次调用:客户端调用 proxy->display(),控制权进入代理。代理先做权限检查(通过),发现 m_real 为空,于是调用 std::make_unique<RealImage> 触发懒加载:磁盘读取、内存分配在这一刻才发生。
  3. 转发:代理调用 m_real->display(),真实对象执行真正的显示逻辑,随后代理把访问计数 +1。
  4. 第二次调用:代理发现 m_real 已存在,跳过创建直接转发——「加载 3 秒」只发生一次,后续调用零成本,这就是缓存/复用带来的性能收益。
  5. 被拒流程:无权限的代理在第一步检查就直接 return,真实对象从未被创建——「昂贵对象可以不存在的安全保证」在此体现。
  6. 析构阶段:unique_ptr 自动释放 RealImage,无需手工 delete,无泄漏。

Qt 示例的流程同理:视图请求第 N 行 → 代理模型调用 filterAcceptsRow 判断该行是否匹配「研发部」→ 命中才向源模型转发 → 源模型返回数据 → 视图渲染。视图与源模型之间永远隔着一层「把关者」。

⑧ 为什么这样设计

代理模式的设计动机,可以从五个维度理解:

⑨ 不使用会怎样

没有代理,控制逻辑不会消失,只会换一个更糟糕的地方落脚——调用方或真实对象本身:

// 反例:没有代理时,调用方被迫承担一切
void showAnything(bool isBig, bool isAdmin, bool needLog) {
    if (isBig && !loaded) { loadBig(); loaded = true; }  // 加载时机逻辑
    if (!isAdmin) { reject(); return; }                  // 权限逻辑
    if (needLog) { logAccess(); }                        // 审计逻辑
    big->show();                                         // 真正的业务只有一行
}

⑩ 何时使用

适合使用的场景:

不适合的场景:

过度设计提醒:代理是「按需把关」而非「逢类必包」。判断标准只有一条:真实对象是否昂贵、敏感或远程,且控制逻辑是否会被多处复用?两者都不满足时,代理就是负担。另外注意别把代理与装饰器混为一谈——你在「控制访问」还是「叠加功能」,想清楚再动手。

⑪ 与其他模式的区别

对比项代理 Proxy装饰器 Decorator适配器 Adapter
意图控制对真实对象的访问为对象动态叠加功能让不兼容的接口协作
接口与真实对象完全相同与组件完全相同转换为客户端期望的新接口
客户端感知通常不知道真实对象存在透明叠加,可感知功能增强感知到接口转换
组合结构一对一,代理持有一个真实对象可递归嵌套多层一对一适配
生命周期代理常负责真实对象的创建与销毁装饰器不管理组件生命周期适配器不管理被适配者生命周期

最容易混淆的是代理与装饰器:两者类图几乎一模一样(都持有同接口的对象并转发调用),差别全在意图上。装饰器是「加功能」——给咖啡加牛奶、加糖,一层层叠上去,客户端清楚地感受到功能变化;代理是「把关」——替真实对象挡权限、挡延迟、挡网络,客户端甚至不知道替身背后还有人。一个常见的记忆法:装饰器向外输出能力,代理向内保护对象。

外观模式(Facade)也容易混淆:外观为一组子系统提供「简化门面」,它不转发子系统的原始接口,而是提供一个更友好的新接口;代理则原样转发 Subject 的接口。外观管「一群」,代理管「一个」。

⑫ Qt 源码中的体现

Qt 是代理模式思想贯彻得最彻底的框架之一:

⑬ 面试常见问题

Q1:代理模式和装饰器模式有什么区别?

A:结构上几乎相同,区别全在意图。装饰器的目的是增强功能(动态叠加新行为,可递归多层),客户端能感知到功能的累加;代理的目的是控制访问(权限、延迟、远程、缓存),客户端通常不知道真实对象的存在。代理一般一对一,且常负责真实对象的创建与销毁。

Q2:为什么 QSortFilterProxyModel 是代理模式的体现?它的价值是什么?

A:它与源模型实现同一个接口 QAbstractItemModel,内部持有源模型,在转发数据请求前插入过滤/排序逻辑——完全符合「同接口 + 持有真实对象 + 转发前控制」的代理三要素。价值在于:视图与数据源彻底解耦,可无侵入地叠加排序过滤、可串联多个代理(代理套代理)、源模型保持纯净不被视图需求污染。

Q3:代理模式有哪些常见变体?

A:五种——虚拟代理(延迟创建昂贵对象)、保护代理(访问权限控制)、远程代理(屏蔽网络通信细节)、缓存代理(缓存重复查询结果)、智能引用代理(引用计数、加锁、写时复制)。

Q4:std::shared_ptr 是代理模式吗?

A:思想上是「智能引用代理」——它持有裸指针,接口(operator->/operator*)与裸指针一致,在解引用和析构时插入引用计数与自动释放逻辑。但 C++ 语境下通常把它归入 RAII 资源管理而非 GoF 设计模式。面试时答「思想一致、实现上属于 RAII」最为稳妥。

Q5:代理模式会带来哪些代价?

A:一是额外的间接层——每次调用多一次转发和虚函数分派,性能敏感路径上不可忽视;二是接口同步维护成本——代理必须跟随真实对象的接口变化;三是滥用风险——对象不昂贵、无控制需求时,代理就是纯粹的过度设计。

⑭ 今日练习

需求:实现一个带缓存的「天气查询服务」(15-30 分钟)。

① 定义接口 WeatherService,含 std::string fetch(const std::string& city);② 实现 RemoteWeatherService:用 std::this_thread::sleep_for(std::chrono::seconds(2)) 模拟网络延迟,返回形如 "北京:晴 26°C" 的字符串;③ 实现 CachedWeatherServiceProxy:缓存每个城市最近一次查询结果,60 秒内重复查询直接返回缓存(不触发 2 秒延迟);④ 客户端只依赖 WeatherService 接口,对同一城市连续查询两次,对比两次耗时并统计缓存命中次数。

提示 1:缓存容器可用 std::unordered_map<std::string, std::pair<std::string, std::chrono::steady_clock::time_point>>;提示 2:判断过期用「当前时刻 - 缓存时刻 > 60 秒」;提示 3:查询第二次时若命中缓存,耗时应当接近 0 而不是 2 秒——用 std::chrono::duration_cast 打印毫秒耗时验证。

完成后自问:如果需求升级为「只允许 VIP 调用」和「每分钟最多调用 10 次」,你的代理要改几处?——答案应当是:新增/调整一个代理类,真实服务和客户端一行不动。

⑮ 今日总结

一句话记忆:代理是真实对象的替身——接口不变,控制先行。

代码特征信号(看到这些就该想到代理):

明日预告:Day 13 责任链模式(Chain of Responsibility)——让请求沿着一条「处理者链」依次传递,每个处理者决定自己处理、还是抛给下一个。谁都能处理,谁都不必全权负责。