职场里总有这么一种人。
看着好像不干核心大活。
但他不可或缺。
老板找技术大牛签字,他挡着:
“大牛在闭关,先放我这。”
外部门来扯皮,他拦着:
“这事儿不归我们管,出门左转。”
甚至只有等到真正的大项目落地时。
他才把大牛请出来按个回车。
在不懂行的人眼里。
他是个“二传手”。
但在懂行的人眼里。
他是系统的“防火墙”和“守门员”。
是他把那些脏活、累活、杂活。
统统挡在了核心业务逻辑之外。
让真正干活的代码。
能安安静静地拿最高绩效。
这就是代理模式 (Proxy)。
一个专门负责“挡事”的艺术。
回到代码里。
这事儿其实一模一样。
有些对象太“重”。
创建一次要耗半天 CPU。
有些对象太“远”。
远在另一台服务器上。
有些对象太“贵”。
不是谁都能随便摸一下。
这时候。
你就不能直接让客户端拿着裸指针往上怼。
你得找个“经纪人”。
这就有了 Proxy。
1. 经典场景:为什么要找个替身?
先说个最古早的例子。
RPC。
远程过程调用。
几十年前大家就在搞这事。
你想调服务器上的一个函数 queryUser(id)。
服务器远在天边。
你不可能真的传一个指针过去。
但在你的代码里。
你希望写起来就像调本地函数一样自然:
User u = userService.queryUser(123);
这背后发生了什么?
那个 userService。
并不是真正的服务器对象。
它是一个“桩” (Stub)。
也就是代理。
它长得和服务器对象一模一样。
都有 queryUser 方法。
但当你调用它时。
它并没有去查数据库。
它只是把参数 123 打包。
塞进网络包。
发给远端。
等远端算完了。
把结果发回来。
它再解包。
假装是自己算出来的。
递给你。
在这个过程里。
你被骗了。
但你被骗得很舒服。
因为你不需要关心网络。
不需要关心序列化。
不需要关心那个对象到底在哪。
这就是远程代理 (Remote Proxy)。
它把“复杂网络调用”伪装成了“简单本地调用”。
还有一种情况。
对象太重。
比如你在做一个文档编辑器。
文档里可能插了一张巨大的 4K 图片。
如果每次打开文档。
都要把这张图读进内存。
那打开速度就崩了。
用户只是想看文字。
还没滚到那一页呢。
你急什么。
这时候你需要一个虚拟代理 (Virtual Proxy)。
在文档模型里。
放一个“图片占位符”。
它实现了 Image 接口。
有宽。
有高。
也能被绘制。
但当你调用 draw() 的时候。
它会先看一眼。
“真正的图加载了吗?”
如果没加载。
现在去磁盘读。
读完了再画。
如果已经加载了。
直接画。
struct ImageProxy : Graphic {
void draw() override {
if (!realImage_) {
realImage_ = loadFromDisk();
}
realImage_->draw();
}
std::unique_ptr<RealImage> realImage_;
};
这就是延迟加载 (Lazy Loading)。
除了真正的干活对象。
没人知道它其实一直在偷懒。
直到最后一刻才动起来。
再比如。
权限控制。
系统里有个 deleteUser 方法。
只有管理员能调。
你不想把权限检查逻辑塞得满地都是。
也不想污染核心业务逻辑。
那就派个保护代理 (Protection Proxy)。
struct UserManagerProxy : UserManager {
void deleteUser(int id) override {
if (!currentUser.isAdmin()) {
throw PermissionDenied();
}
realManager_->deleteUser(id);
}
};
门卫拦在门口。
有证的进。
没证的滚。
里面的老板根本不需要知道刚才发生了什么。
他只管删人。
2. 代码怎么写:长得像,才是关键
Proxy 模式的核心。
在于“伪装”。
要伪装得像。
就得有一张一模一样的脸。
也就是接口。
假设我们有一个“很重”的资源。
struct Subject {
virtual void request() = 0;
virtual ~Subject() = default;
};
这是真正的干活人。
struct RealSubject : Subject {
void request() override {
// 这里可能涉及昂贵的 IO
// 或者复杂的计算
std::cout << "RealSubject: Handling request.\n";
}
};
这是代理人。
struct Proxy : Subject {
explicit Proxy(std::unique_ptr<RealSubject> real)
: real_(std::move(real)) {}
// 如果支持延迟加载,这里可能不用传 real
// 而是自己内部择机创建
void request() override {
if (checkAccess()) {
real_->request();
logAccess();
}
}
private:
bool checkAccess() {
// 假装检查一下权限
return true;
}
void logAccess() {
// 假装记个日志
std::cout << "Proxy: Logged request.\n";
}
std::unique_ptr<RealSubject> real_;
};
客户端根本不关心拿到的是谁。
void clientCode(Subject& s) {
s.request();
}
这结构看着是不是眼熟?
和 Decorator 简直是双胞胎。
都有一个接口。
都持有一个实现类的指针。
都做转发。
那它们的区别到底在哪?
这也是面试官最爱问的钓鱼题。
别背定义。
看意图。
Decorator 的意图是**“增强”**。
它觉得原对象功能不够。
要加点日志。
加点缓存。
加点格式化。
而且通常是动态套娃。
今天套一层。
明天套三层。
Proxy 的意图是**“控制”**。
它不怎么想改变原对象的功能。
它想控制你能不能访问原对象。
或者什么时候访问原对象。
或者访问哪里的原对象。
Decorator 像化妆师。
把你打扮得更漂亮。
Proxy 像经纪人。
挡在前面处理杂事。
让你能专心演戏。
或者在你还没到场的时候。
先替你顶一会儿。
3. C++ 程序员最熟悉的代理:智能指针
别以为 Proxy 只是用来写业务类的。
在 C++ 里。
你每天都在用 Proxy。
那就是智能指针。
std::shared_ptr 和 std::unique_ptr。
它们就是最典型的代理。
你看它们的行为。
重载了 operator-> 和 operator*。
让你用起来觉得它“就是”个指针。
std::shared_ptr<User> u = std::make_shared<User>();
u->getName();
但实际上。
它在背后偷偷干了很多活。
它帮你维护引用计数。
它帮你监控生命周期。
引用归零时。
它帮你删对象。
这就是智能引用代理 (Smart Reference Proxy)。
它接管了访问裸指针的入口。
在这个入口处。
加塞了内存管理的逻辑。
如果你再去看一些高级库。
比如 std::atomic。
或者各种 ORM 库里的“关联对象”。
当你调用 user.getOrders() 时。
它才去数据库查订单。
这也是 Proxy。
C++ 的运算符重载机制。
让写 Proxy 变得特别隐蔽。
也特别优雅。
你完全感觉不到它的存在。
直到它帮你挡掉了一次内存泄漏。
4. 什么时候该用,什么时候别用?
Proxy 很好用。
但也不是万金油。
当你需要延迟初始化时。
一定要用。
特别是那个对象创建成本很高。
但又不一定每次都用得到的时候。
就像单例模式里的 Lazy Initialization。
其实也是一种简单的 Proxy 思想。
当你需要访问控制时。
用它。
把脏活累活拦在业务逻辑之外。
当你需要远程调用时。
没得选。
不管是 gRPC 还是 Thrift。
生成的那个 Client Stub。
本质都是 Proxy。
但是。
如果你的对象很轻。
甚至就是个简单的值对象。
别搞那么多中间商赚差价。
每多一层 Proxy。
就多一次虚函数调用(通常)。
或者多一层间接寻址。
在极端追求性能的场景下。
这都是开销。
而且。
代码复杂度也是成本。
为了保护一个根本没人调用的私有方法。
写一个完整的 Proxy 类。
属于过度设计。
5. 总结:做个负责任的中间人
Proxy 模式说白了。
就是“隔离”。
在调用者和真实对象之间。
插一块玻璃。
有时候是透明玻璃。
为了监控。
有时候是毛玻璃。
为了安全。
有时候是一面镜子。
让你以为真的就在那里。
下次当你觉得。
“直接访问这个对象有点不妥”的时候。
无论是为了性能。
为了安全。
还是为了网络透明。
想想 Proxy。
找个靠谱的经纪人。
让它替对象把那些。
脏的。
累的。
远的。
麻烦的。
事情都扛下来。
让核心对象。
只做它该做的事。