代理模式:模式动机与定义、静态代理与动态代理、C++代码实现、远程代理与虚拟代理
代理模式,说白了就是“找人替你办事”。你想想看,有时候你不想直接面对某个对象,或者直接调用它成本太高、太危险,那你就找个代理。代理和你实现同样的接口,外面的人以为在跟你打交道,其实背后是代理在跟真正的对象沟通。
我在项目中遇到过好几次这样的场景:比如一个图片加载器,如果直接加载大图,界面会卡死。这时候我就搞了个虚拟代理,先显示一个占位图,等后台线程把大图加载完了,再替换上去。用户感觉挺流畅,其实背后是代理在“搞鬼”。
模式动机与定义
为什么需要代理?核心动机就三个:
- 控制访问:不想让客户端直接碰真实对象,比如权限检查、访问计数。
- 延迟开销:真实对象的创建或加载很重,等真正要用的时候再动手。
- 增强功能:在不修改真实对象的前提下,加一些额外逻辑,比如日志、缓存。
代理模式的定义其实很简单:为另一个对象提供一个替身或占位符,以控制对这个对象的访问。嗯,这里要注意,代理和真实对象必须实现同一个接口,这样客户端才能无感切换。
核心要点:代理模式的关键是“接口一致”。如果代理和真实对象的接口不一样,那就不是代理,是适配器了。我见过有人把这两者搞混,结果代码越写越拧巴。
静态代理与动态代理
静态代理,就是你在编译期就写死了代理类。每个真实对象都得配一个代理类,代码量会膨胀。我早期做项目时,给每个业务类都手动写一个代理,后来发现维护起来真想哭。
动态代理就不一样了。它在运行时动态生成代理类,你只需要写一个处理器(handler),所有代理逻辑都集中在这里。C++里没有Java那种内置的动态代理机制,但我们可以用模板、lambda、或者更高级的CRTP(奇异递归模板模式)来模拟。
| 对比项 | 静态代理 | 动态代理 |
|---|---|---|
| 代码生成时机 | 编译期 | 运行期 |
| 灵活性 | 低,每增加一个真实类就得写一个新代理 | 高,一个处理器可以处理多种真实类 |
| 性能开销 | 几乎没有额外运行时开销 | 有少量动态分发开销 |
| 适用场景 | 代理逻辑固定,类数量少 | 代理逻辑通用,类数量多或不确定 |
我的建议:如果项目里代理逻辑很简单,比如就加个日志,用静态代理就够了。但如果代理逻辑复杂,或者你希望以后能灵活切换,那动态代理更合适。我曾经在一个中间件项目里用动态代理统一处理了所有远程调用,省了不少事。
C++代码实现
先看一个静态代理的例子。假设我们有一个图像接口,真实对象是RealImage,代理是ProxyImage。
// 抽象主题
class Image {
public:
virtual ~Image() = default;
virtual void display() = 0;
};
// 真实主题
class RealImage : public Image {
public:
explicit RealImage(const std::string& filename) : filename_(filename) {
loadFromDisk(); // 构造时就加载,很重
}
void display() override {
std::cout << "Displaying " << filename_ << std::endl;
}
private:
void loadFromDisk() {
std::cout << "Loading " << filename_ << " from disk..." << std::endl;
}
std::string filename_;
};
// 代理
class ProxyImage : public Image {
public:
explicit ProxyImage(const std::string& filename) : filename_(filename), realImage_(nullptr) {}
void display() override {
if (!realImage_) {
realImage_ = std::make_unique<RealImage>(filename_); // 延迟加载
}
realImage_->display();
}
private:
std::string filename_;
std::unique_ptr<RealImage> realImage_;
};
// 客户端
int main() {
ProxyImage proxy("photo.jpg");
// 第一次调用时才真正加载
proxy.display();
// 第二次调用直接显示,不再加载
proxy.display();
return 0;
}
动态代理在C++里可以用模板和函数包装器来实现。下面是一个简单的动态代理示例,它通过std::function和可变参数模板,把调用转发给真实对象,同时插入日志。
#include <iostream>
#include <functional>
#include <memory>
// 动态代理处理器基类
class InvocationHandler {
public:
virtual ~InvocationHandler() = default;
virtual void invoke(const std::string& methodName) = 0;
};
// 日志处理器
class LoggingHandler : public InvocationHandler {
public:
void invoke(const std::string& methodName) override {
std::cout << "[LOG] Calling method: " << methodName << std::endl;
}
};
// 动态代理模板
template<typename Interface>
class DynamicProxy : public Interface {
public:
template<typename... Args>
explicit DynamicProxy(std::shared_ptr<Interface> real, std::shared_ptr<InvocationHandler> handler, Args&&... args)
: real_(std::move(real)), handler_(std::move(handler)) {}
// 假设Interface有一个方法 void doSomething()
void doSomething() override {
handler_->invoke("doSomething");
real_->doSomething();
}
private:
std::shared_ptr<Interface> real_;
std::shared_ptr<InvocationHandler> handler_;
};
// 真实对象
class RealService : public Interface {
public:
void doSomething() override {
std::cout << "RealService is doing something." << std::endl;
}
};
// 接口定义
class Interface {
public:
virtual ~Interface() = default;
virtual void doSomething() = 0;
};
int main() {
auto real = std::make_shared<RealService>();
auto handler = std::make_shared<LoggingHandler>();
DynamicProxy<Interface> proxy(real, handler);
proxy.doSomething(); // 先日志,再真实调用
return 0;
}
注意:C++的动态代理不像Java那样原生支持,上面的模板方案只能代理已知接口。如果你需要完全动态的代理(比如代理任意类),那就得用更底层的技术,比如运行时生成机器码或者使用dlopen。我曾经在一个插件系统里用过后者,水很深,不建议轻易尝试。
远程代理与虚拟代理
远程代理,就是代理一个在不同进程或不同机器上的对象。客户端调用代理的方法,代理把调用序列化后通过网络发过去,远程对象执行完再把结果返回。说白了,就是RPC(远程过程调用)的雏形。
虚拟代理,就是延迟创建开销大的对象。前面图片加载的例子就是虚拟代理。它在你真正需要对象的时候才去创建,而不是在代理构造时就创建。
这两种代理在实际项目中非常常见。我做过一个分布式监控系统,每个节点都暴露一个远程代理,中心服务器通过代理来收集数据,根本不用关心数据到底在哪台机器上。虚拟代理就更不用说了,几乎每个UI系统里都有它的影子。
避坑指南:远程代理一定要注意网络异常。我曾经因为没处理超时,导致代理线程卡死,整个界面都冻住了。后来加了超时和重试机制,才稳定下来。虚拟代理则要注意线程安全,如果多个线程同时触发加载,可能会重复创建真实对象。
知识体系结构图
这张图把代理模式的核心脉络理清了。你看,动机和定义是根基,静态代理和动态代理是两种实现方式,远程代理和虚拟代理是两种典型应用。嗯,其实还有保护代理、缓存代理等变体,但万变不离其宗。
我个人觉得,代理模式是那种“用起来简单,想用好却不容易”的模式。关键是要搞清楚:你到底想控制什么?是访问权限,还是创建时机,还是调用方式?想清楚了,代理模式就能帮你写出更干净、更健壮的代码。
最后说一句:别为了用代理而用代理。如果你的真实对象本身就很轻量,加一层代理反而多余。我见过有人给一个简单的getter也套上代理,那纯粹是过度设计。记住,模式是工具,不是目的。