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_ptrstd::unique_ptr 本质上也是代理。它们代理了原始指针,增加了自动内存管理的功能。

我习惯把智能指针叫做「带保姆的指针」。你只管用,不用管释放。保姆(智能指针)会在合适的时候帮你收拾干净。

代理类型 STL对应 核心功能
虚拟代理 智能指针 + 工厂模式 延迟创建、按需加载
保护代理 自定义包装类 权限校验、访问控制
远程代理 网络库 + 序列化 隐藏网络通信
迭代器代理 STL迭代器 统一遍历接口
智能指针代理 shared_ptr/unique_ptr 自动资源管理

代理模式与STL结合的最佳实践

我个人总结了几个经验:

  1. 优先用组合,少用继承。代理对象持有真实对象的引用,而不是继承它。这样更灵活。
  2. 用智能指针管理生命周期。代理模式里经常涉及对象的创建和销毁,std::shared_ptr 能帮你避免内存泄漏。
  3. 接口要稳定。代理和真实对象共享的接口一旦确定,尽量不要改。否则两边都要改,很麻烦。
  4. 不要过度代理。如果一个类不需要延迟加载,也不需要权限控制,就别硬套代理模式。我见过有人给每个类都配一个代理,结果代码量翻了三倍……

小技巧:如果你不确定是否需要代理模式,先写一个简单的版本。等确实需要加控制逻辑时,再重构出代理类。别一开始就过度设计。

代理模式的核心逻辑图

下面这张图展示了代理模式的核心结构,以及STL组件在其中的位置:

代理模式核心结构(STL视角) 抽象接口 代理对象 持有真实对象引用 添加控制逻辑 真实对象 实际业务逻辑 资源密集型操作 委托调用 STL 中的代理模式体现 迭代器(遍历代理) 智能指针(资源代理) 适配器(接口代理)

从这张图可以看出,代理模式的核心就是「中间层」。STL里的迭代器、智能指针、适配器,本质上都在做同一件事——在调用者和实际对象之间加一层,然后在这一层里做文章。

总结

代理模式不是什么高深的东西。说白了,就是找个中间人帮你干活。STL里到处都是这种思想:迭代器代理了遍历,智能指针代理了内存管理,适配器代理了接口转换。

我个人觉得,理解代理模式的关键不在于记住UML图,而在于想清楚一个问题:谁该做什么事? 真实对象只关心业务逻辑,控制逻辑交给代理。这样代码才干净、好维护。

嗯,今天就聊到这里。记住一点:下次你写代码时,如果发现某个类既要干活又要做权限检查,或者既要加载数据又要管理生命周期,那就是该引入代理模式的时候了。


公众号:蓝海资料掘金营,微信deep3321