STL与代理模式:代理控制、延迟加载、访问控制
代理模式,说白了就是「找个中间人」。你想想看,有时候直接操作对象太麻烦、太危险,或者时机不对。这时候找个代理对象来替你干活,就舒服多了。
我在项目中遇到过不少场景,比如大对象的懒加载、权限校验、远程调用。这些用代理模式都能优雅解决。而STL里的一些设计思想,其实跟代理模式不谋而合。今天我们就来聊聊,怎么用STL的思维去实现代理模式。
代理模式的核心思想
代理模式的结构其实很简单:一个接口,两个实现。一个是真的干活的对象,一个是代理对象。代理对象持有真实对象的引用,然后在调用前后加点料。
我习惯把代理模式分成三类:
- 虚拟代理:延迟加载,用到时才创建真实对象
- 保护代理:访问控制,校验权限
- 远程代理:隐藏网络通信细节
今天重点讲前两种,因为跟STL结合得更紧密。
用STL实现虚拟代理
虚拟代理最常见的场景就是大对象的懒加载。比如一个图片加载器,你不想在程序启动时就加载所有图片,而是等到真正显示时才加载。
这里有个关键点:代理对象和真实对象必须实现相同的接口。这样调用方根本不知道自己在跟代理打交道。
看个例子:
#include <iostream>
#include <memory>
#include <string>
// 抽象接口
class Image {
public:
virtual ~Image() = default;
virtual void display() = 0;
};
// 真实对象:大图片
class RealImage : public Image {
std::string filename;
public:
explicit RealImage(const std::string& f) : filename(f) {
loadFromDisk();
}
void display() override {
std::cout << "显示图片: " << filename << std::endl;
}
private:
void loadFromDisk() {
std::cout << "从磁盘加载: " << filename << std::endl;
}
};
// 代理对象
class ProxyImage : public Image {
std::string filename;
std::unique_ptr<RealImage> realImage;
public:
explicit ProxyImage(const std::string& f) : filename(f) {}
void display() override {
// 延迟加载:第一次调用时才创建真实对象
if (!realImage) {
realImage = std::make_unique<RealImage>(filename);
}
realImage->display();
}
};
int main() {
ProxyImage img("photo.jpg");
// 此时还没有加载图片
std::cout << "图片对象已创建,但未加载" << std::endl;
// 第一次调用display时才加载
img.display();
// 第二次调用不再加载
img.display();
return 0;
}
关键点:这里用了 std::unique_ptr 来管理真实对象的生命周期。这是STL给我们的礼物——智能指针让资源管理变得简单又安全。
输出结果:
图片对象已创建,但未加载
从磁盘加载: photo.jpg
显示图片: photo.jpg
显示图片: photo.jpg
看到了吗?第一次调用 display() 时才触发加载,第二次就直接显示了。这就是延迟加载的精髓。
用STL实现保护代理
保护代理的核心是访问控制。比如一个文档系统,普通用户只能读,管理员才能写。
我曾经在一个项目中做过权限系统,当时用了类似的方式。嗯,这里要注意:不要把权限逻辑散落在业务代码里,用代理统一管理才是正道。
#include <iostream>
#include <memory>
#include <string>
class Document {
public:
virtual ~Document() = default;
virtual void read() = 0;
virtual void write(const std::string& content) = 0;
};
class RealDocument : public Document {
std::string content;
public:
void read() override {
std::cout << "文档内容: " << content << std::endl;
}
void write(const std::string& c) override {
content = c;
std::cout << "文档已更新" << std::endl;
}
};
class ProtectedDocument : public Document {
std::unique_ptr<RealDocument> doc;
bool isAdmin;
public:
ProtectedDocument(bool admin) :
doc(std::make_unique<RealDocument>()),
isAdmin(admin) {}
void read() override {
doc->read(); // 读操作所有人都可以
}
void write(const std::string& content) override {
if (!isAdmin) {
std::cout << "权限不足:只有管理员才能写入" << std::endl;
return;
}
doc->write(content);
}
};
int main() {
ProtectedDocument user(false);
ProtectedDocument admin(true);
user.read(); // 可以读
user.write("新内容"); // 权限不足
admin.write("管理员内容"); // 可以写
admin.read(); // 可以读
return 0;
}
注意:保护代理不是银弹。如果权限规则特别复杂,建议结合策略模式或访问者模式。我曾经在一个项目中把权限逻辑全塞进代理里,结果代理类膨胀到上千行……后来重构时才拆出来。
STL迭代器:天然的代理模式
说到代理模式,我不得不提STL里的迭代器。你想想看,迭代器是不是一个代理?它代理了对容器的遍历操作,隐藏了底层数据结构。
比如 std::vector 的迭代器,你根本不用关心它内部是怎么存储的。你只需要用 ++ 和 * 就能遍历所有元素。这就是代理模式的思想——提供一个统一的接口,屏蔽实现细节。
更妙的是,STL的迭代器还支持「适配器」,比如 std::reverse_iterator。它代理了正向迭代器,把 ++ 变成了 --。这不就是代理模式的变体吗?
智能指针:另一种代理
std::shared_ptr 和 std::unique_ptr 本质上也是代理。它们代理了原始指针,增加了自动内存管理的功能。
我习惯把智能指针叫做「带保姆的指针」。你只管用,不用管释放。保姆(智能指针)会在合适的时候帮你收拾干净。
| 代理类型 | STL对应 | 核心功能 |
|---|---|---|
| 虚拟代理 | 智能指针 + 工厂模式 | 延迟创建、按需加载 |
| 保护代理 | 自定义包装类 | 权限校验、访问控制 |
| 远程代理 | 网络库 + 序列化 | 隐藏网络通信 |
| 迭代器代理 | STL迭代器 | 统一遍历接口 |
| 智能指针代理 | shared_ptr/unique_ptr | 自动资源管理 |
代理模式与STL结合的最佳实践
我个人总结了几个经验:
- 优先用组合,少用继承。代理对象持有真实对象的引用,而不是继承它。这样更灵活。
- 用智能指针管理生命周期。代理模式里经常涉及对象的创建和销毁,
std::shared_ptr能帮你避免内存泄漏。 - 接口要稳定。代理和真实对象共享的接口一旦确定,尽量不要改。否则两边都要改,很麻烦。
- 不要过度代理。如果一个类不需要延迟加载,也不需要权限控制,就别硬套代理模式。我见过有人给每个类都配一个代理,结果代码量翻了三倍……
小技巧:如果你不确定是否需要代理模式,先写一个简单的版本。等确实需要加控制逻辑时,再重构出代理类。别一开始就过度设计。
代理模式的核心逻辑图
下面这张图展示了代理模式的核心结构,以及STL组件在其中的位置:
从这张图可以看出,代理模式的核心就是「中间层」。STL里的迭代器、智能指针、适配器,本质上都在做同一件事——在调用者和实际对象之间加一层,然后在这一层里做文章。
总结
代理模式不是什么高深的东西。说白了,就是找个中间人帮你干活。STL里到处都是这种思想:迭代器代理了遍历,智能指针代理了内存管理,适配器代理了接口转换。
我个人觉得,理解代理模式的关键不在于记住UML图,而在于想清楚一个问题:谁该做什么事? 真实对象只关心业务逻辑,控制逻辑交给代理。这样代码才干净、好维护。
嗯,今天就聊到这里。记住一点:下次你写代码时,如果发现某个类既要干活又要做权限检查,或者既要加载数据又要管理生命周期,那就是该引入代理模式的时候了。
公众号:蓝海资料掘金营,微信deep3321