14. 代理模式(Proxy):虚代理、保护代理、远程代理,C++智能指针中的代理思想
代理模式,说白了就是「找人替你办事」。你想想看,生活中我们经常这么干——找中介租房、让秘书安排会议、请律师打官司。自己不出面,但事情照样能办成。
在C++里,代理模式也是这个道理。一个对象不想直接暴露给客户端,或者直接操作成本太高,那就找个「替身」来挡在前面。这个替身就是Proxy,它和原对象实现同一个接口,客户端感知不到区别。
我个人习惯把代理模式分成三类:虚代理、保护代理、远程代理。咱们一个一个聊。
14.1 虚代理(Virtual Proxy)
虚代理解决的是「延迟加载」的问题。对象创建成本高,但又不一定马上用得到,那就先弄个轻量级的代理占个位,真正需要的时候再创建真实对象。
我在项目中遇到过一个大图像查看器。用户打开一个文件夹,里面几百张高清图,如果全部加载到内存,程序直接崩。解决方案就是虚代理——先显示缩略图,用户双击某张图时,才加载原图。
class Image {
public:
virtual void display() = 0;
virtual ~Image() = default;
};
class HighResImage : public Image {
string filename;
public:
HighResImage(const string& f) : filename(f) {
loadFromDisk(); // 开销大
}
void display() override {
cout << "Displaying " << filename << endl;
}
void loadFromDisk() {
cout << "Loading " << filename << " from disk..." << endl;
}
};
class ImageProxy : public Image {
string filename;
HighResImage* realImage = nullptr;
public:
ImageProxy(const string& f) : filename(f) {}
void display() override {
if (!realImage)
realImage = new HighResImage(filename);
realImage->display();
}
~ImageProxy() { delete realImage; }
};
你看,客户端始终操作的是Image*接口,根本不知道背后是代理还是真实对象。这就是代理模式的精髓——透明替换。
14.2 保护代理(Protection Proxy)
保护代理管的是「谁可以碰」。说白了就是权限控制。真实对象不想暴露给所有人,代理在中间做一道安检。
我曾经给一个内部管理系统写过权限模块。不同角色能看到的数据不一样,但底层数据接口是同一个。我不想在每个业务方法里都写if-else判断权限,太恶心了。于是用保护代理,在调用前统一拦截。
class Document {
public:
virtual void view() = 0;
virtual void edit() = 0;
virtual ~Document() = default;
};
class RealDocument : public Document {
string content;
public:
RealDocument(const string& c) : content(c) {}
void view() override {
cout << "Content: " << content << endl;
}
void edit() override {
cout << "Editing document..." << endl;
}
};
class ProtectedDocument : public Document {
RealDocument* doc;
string userRole;
public:
ProtectedDocument(const string& c, const string& role)
: doc(new RealDocument(c)), userRole(role) {}
void view() override {
doc->view(); // 所有人都能看
}
void edit() override {
if (userRole == "admin" || userRole == "editor") {
doc->edit();
} else {
cout << "Access denied: you cannot edit." << endl;
}
}
~ProtectedDocument() { delete doc; }
};
14.3 远程代理(Remote Proxy)
远程代理处理的是「不在同一个地址空间」的问题。对象在另一台机器上,或者另一个进程里,客户端想调用它就像调用本地对象一样。这其实就是RPC(远程过程调用)的雏形。
我记得早年做分布式系统时,用过CORBA和Java RMI,底层就是远程代理。C++里虽然没有内置的远程代理框架,但自己实现一个也不复杂——代理负责序列化请求、网络传输、反序列化响应。
class Calculator {
public:
virtual int add(int a, int b) = 0;
virtual ~Calculator() = default;
};
class RemoteCalcProxy : public Calculator {
string serverAddress;
public:
RemoteCalcProxy(const string& addr) : serverAddress(addr) {}
int add(int a, int b) override {
// 1. 序列化请求
string request = serialize(a, b);
// 2. 发送到 serverAddress
string response = sendRequest(request);
// 3. 反序列化结果
return deserialize(response);
}
};
当然,生产环境不建议自己造轮子。gRPC、Thrift这些框架已经做得很成熟了。但理解远程代理的原理,能帮你更好地理解这些框架的设计思路。
14.4 C++智能指针中的代理思想
聊到这里,你可能会觉得代理模式有点「重」。其实C++里天天用的智能指针,就是代理模式的经典应用。
std::shared_ptr 和 std::unique_ptr 本质上就是原始指针的代理。它们接管了内存管理的职责,让你不用手动delete。这不就是代理模式吗?
- shared_ptr:引用计数代理。多个代理共享同一个真实对象,最后一个代理销毁时释放资源。
- unique_ptr:独占代理。代理销毁时自动释放真实对象,不允许拷贝,只允许移动。
- weak_ptr:弱引用代理。不增加引用计数,解决循环引用问题。
void process() {
// 代理模式:shared_ptr 代理了原始指针
std::shared_ptr<Image> proxy = std::make_shared<HighResImage>("photo.jpg");
proxy->display(); // 客户端完全不知道内部有引用计数
// 离开作用域,自动释放
}
14.5 代理模式的核心结构
下面这张图展示了代理模式的通用结构。你可以看到,Proxy 和 RealSubject 都实现了同一个接口 Subject,客户端只跟 Subject 打交道。
14.6 代理模式 vs 装饰器模式
很多初学者会把代理模式和装饰器模式搞混。它们结构确实很像,但意图完全不同。
| 维度 | 代理模式 | 装饰器模式 |
|---|---|---|
| 核心目的 | 控制访问、延迟加载、权限校验 | 动态扩展功能 |
| 对象创建 | 代理通常自己创建或持有真实对象 | 装饰器接收外部传入的被装饰对象 |
| 接口透明性 | 完全透明,客户端无感知 | 透明,但可以叠加多层 |
| 典型场景 | 智能指针、远程调用、懒加载 | 流处理、日志、缓存 |
说白了,代理模式是「替身」,装饰器模式是「增强」。代理不想让你直接碰真实对象,装饰器则是在真实对象的基础上加料。
14.7 避坑指南
我曾经在一个项目里过度使用代理模式。每个接口都套一层代理,结果代码变得极其难调试。你想想看,调用链上多了好几层间接跳转,出问题根本不知道是代理层的问题还是真实对象的问题。
所以我的经验是:代理模式虽好,但别滥用。只有当你确实需要「控制访问」或「延迟加载」时,才引入代理。否则,保持简单直接。
- 虚代理:延迟创建,按需加载
- 保护代理:权限控制,统一拦截
- 远程代理:跨地址空间调用
- C++智能指针:内存管理的代理思想
代理模式的核心价值,就是让你在不修改真实对象的前提下,增加一层控制。这层控制可以是延迟、权限、网络,也可以是内存管理。理解了这个本质,你就能在合适的场景下灵活运用。