① 今日主题
代理模式(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 接口,不直接接触 RealSubject | main() |
按用途,代理可分为五种常见变体:虚拟代理(延迟创建昂贵对象)、保护代理(访问控制)、远程代理(屏蔽网络细节,如 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() 调用为主线,完整执行流程如下:
- 构造阶段:
main创建ImageProxy。此刻只保存了文件名和权限标志,m_real为nullptr——昂贵的RealImage还不存在。 - 第一次调用:客户端调用
proxy->display(),控制权进入代理。代理先做权限检查(通过),发现m_real为空,于是调用std::make_unique<RealImage>触发懒加载:磁盘读取、内存分配在这一刻才发生。 - 转发:代理调用
m_real->display(),真实对象执行真正的显示逻辑,随后代理把访问计数 +1。 - 第二次调用:代理发现
m_real已存在,跳过创建直接转发——「加载 3 秒」只发生一次,后续调用零成本,这就是缓存/复用带来的性能收益。 - 被拒流程:无权限的代理在第一步检查就直接 return,真实对象从未被创建——「昂贵对象可以不存在的安全保证」在此体现。
- 析构阶段:
unique_ptr自动释放RealImage,无需手工 delete,无泄漏。
Qt 示例的流程同理:视图请求第 N 行 → 代理模型调用 filterAcceptsRow 判断该行是否匹配「研发部」→ 命中才向源模型转发 → 源模型返回数据 → 视图渲染。视图与源模型之间永远隔着一层「把关者」。