代理模式:模式动机与定义

代理模式,说白了就是「找人替你办事」。你想想看,生活中我们经常这么做——找中介租房、找律师打官司、找秘书接电话。这些代理人帮我们处理了那些我们不想亲自做、或者不方便做的事情。

在软件开发里,道理一模一样。有时候我们不想直接操作某个对象,或者直接操作它成本太高、不安全、太麻烦。这时候,我们就创建一个代理对象,让它站在客户端和真实对象之间,替我们完成那些「脏活累活」。

我记得有一次接手一个老项目,里面有个图片加载模块。每次打开页面,所有图片都一股脑儿加载,页面卡得不行。我当时就想:能不能让图片「懒加载」?用户看到哪张,再加载哪张?嗯,代理模式正好解决这个问题。

代理模式的定义:为其他对象提供一种代理,以控制对这个对象的访问。

说白了,代理对象和被代理对象实现相同的接口,客户端通过代理来间接访问真实对象。代理可以在调用前后做一些额外的事情——比如权限检查、延迟加载、日志记录等。

静态代理与动态代理

静态代理:编译期就定好的代理

静态代理是最直观的实现方式。你在编译之前就把代理类和真实类的关系写死。代理类和真实类实现同一个接口,代理类里持有真实类的引用。

我在项目中用过静态代理做权限控制。当时有个支付系统,不同角色能调用的接口不一样。我写了个代理类,在调用真实支付接口之前先检查用户权限。代码结构清晰,但问题也很明显——每增加一个真实类,就得写一个对应的代理类。项目大了以后,代理类比业务类还多,维护起来挺头疼的。

// 接口
class ISubject {
public:
    virtual void request() = 0;
    virtual ~ISubject() = default;
};

// 真实对象
class RealSubject : public ISubject {
public:
    void request() override {
        std::cout << "真实对象处理请求" << std::endl;
    }
};

// 静态代理
class StaticProxy : public ISubject {
private:
    RealSubject* realSubject;
public:
    StaticProxy() : realSubject(new RealSubject()) {}
    ~StaticProxy() { delete realSubject; }
    
    void request() override {
        std::cout << "代理:权限检查..." << std::endl;
        realSubject->request();
        std::cout << "代理:日志记录..." << std::endl;
    }
};

动态代理:运行时才生成的代理

动态代理就灵活多了。你不需要为每个类手写代理,而是在运行时动态生成代理对象。C++ 里没有 Java 那种内置的动态代理机制,但我们可以用模板、lambda、或者 std::function 来实现类似的效果。

我曾经用模板实现过一个通用的动态代理。核心思路是:代理类模板接受一个真实对象和一个「拦截器」函数,在调用真实方法前后执行拦截逻辑。这样一套代码就能代理任意类型的对象。

// 动态代理(基于模板)
template<typename T>
class DynamicProxy {
private:
    T* realObject;
    std::function<void()> beforeHook;
    std::function<void()> afterHook;
public:
    DynamicProxy(T* obj, std::function<void()> before, std::function<void()> after)
        : realObject(obj), beforeHook(before), afterHook(after) {}
    
    template<typename... Args>
    auto call(void (T::*method)(Args...), Args... args) {
        if (beforeHook) beforeHook();
        (realObject->*method)(args...);
        if (afterHook) afterHook();
    }
};

// 使用示例
RealSubject subject;
DynamicProxy<RealSubject> proxy(&subject, 
    [](){ std::cout << "前置处理" << std::endl; },
    [](){ std::cout << "后置处理" << std::endl; }
);
proxy.call(&RealSubject::request);

我的建议:如果代理逻辑简单且稳定,用静态代理就够了。如果代理逻辑经常变化,或者要代理很多不同类型的对象,动态代理更合适。别为了「炫技」而用动态代理,代码可读性更重要。

C++代码实现:一个完整的代理模式示例

下面是一个更完整的例子。假设我们有一个文件下载系统,真实对象负责从远程服务器下载文件。代理对象在下载前检查缓存,下载后记录日志。

#include <iostream>
#include <string>
#include <map>

// 抽象接口
class FileDownloader {
public:
    virtual void download(const std::string& url) = 0;
    virtual ~FileDownloader() = default;
};

// 真实下载器
class RealFileDownloader : public FileDownloader {
public:
    void download(const std::string& url) override {
        std::cout << "正在从 " << url << " 下载文件..." << std::endl;
        // 模拟网络延迟
        // 实际项目中这里会有真正的下载逻辑
    }
};

// 代理下载器
class ProxyFileDownloader : public FileDownloader {
private:
    RealFileDownloader* realDownloader;
    std::map<std::string, bool> cache;  // 简单缓存
    int downloadCount;
public:
    ProxyFileDownloader() : realDownloader(new RealFileDownloader()), downloadCount(0) {}
    ~ProxyFileDownloader() { delete realDownloader; }
    
    void download(const std::string& url) override {
        // 1. 缓存检查
        if (cache.find(url) != cache.end()) {
            std::cout << "缓存命中,直接返回" << std::endl;
            return;
        }
        
        // 2. 权限检查(模拟)
        std::cout << "检查下载权限..." << std::endl;
        
        // 3. 调用真实对象
        realDownloader->download(url);
        
        // 4. 更新缓存
        cache[url] = true;
        
        // 5. 日志记录
        downloadCount++;
        std::cout << "下载完成,累计下载次数: " << downloadCount << std::endl;
    }
};

// 客户端代码
int main() {
    ProxyFileDownloader proxy;
    proxy.download("http://example.com/file1.zip");
    proxy.download("http://example.com/file1.zip");  // 第二次会命中缓存
    proxy.download("http://example.com/file2.zip");
    return 0;
}

避坑指南:我曾经在代理模式里犯过一个错误——代理对象和真实对象的生命周期管理没做好。代理里持有真实对象的指针,但真实对象被提前销毁了,导致野指针崩溃。后来我改用 shared_ptr 管理生命周期,问题才解决。记住:谁创建,谁销毁;或者用智能指针。

远程代理与虚拟代理

远程代理:让远程调用像本地调用一样

远程代理是代理模式最经典的应用之一。它隐藏了网络通信的细节,让客户端感觉自己在调用一个本地对象,实际上请求被发送到了远程服务器。

我做过一个分布式计算项目,计算节点分布在不同的机器上。我们用远程代理封装了网络通信——客户端调用代理对象的方法,代理内部把参数序列化、通过网络发送、接收结果、反序列化,然后返回给客户端。整个过程对客户端完全透明。

远程代理的核心职责:

  • 网络通信的封装(序列化/反序列化)
  • 连接管理(建立、维护、断开连接)
  • 错误处理(网络超时、重试机制)
  • 负载均衡(如果有多个远程服务节点)

虚拟代理:延迟加载,按需创建

虚拟代理的核心思想是「用到的时候再创建」。它适用于创建开销很大的对象——比如大图片、复杂文档、数据库连接等。

我记得有个报表系统,每次打开都要加载几十个图表。用户其实只关心前两个图表,后面的根本来不及看。我们用虚拟代理改造后,只有图表进入可视区域时才真正加载数据。页面打开速度从 8 秒降到了 1.5 秒。

虚拟代理的实现要点:

  • 代理对象一开始只保存创建真实对象所需的信息(比如文件路径、URL)
  • 第一次调用真实方法时,才真正创建真实对象
  • 后续调用直接复用已创建的真实对象
// 虚拟代理示例:延迟加载大图片
class Image {
public:
    virtual void display() = 0;
};

class HighResolutionImage : public Image {
private:
    std::string filePath;
public:
    HighResolutionImage(const std::string& path) : filePath(path) {
        loadFromDisk();  // 构造函数里加载,开销大
    }
    void display() override {
        std::cout << "显示图片: " << filePath << std::endl;
    }
private:
    void loadFromDisk() {
        std::cout << "从磁盘加载图片(耗时操作)..." << std::endl;
    }
};

class ImageProxy : public Image {
private:
    std::string filePath;
    HighResolutionImage* realImage;
public:
    ImageProxy(const std::string& path) : filePath(path), realImage(nullptr) {}
    
    void display() override {
        if (realImage == nullptr) {
            realImage = new HighResolutionImage(filePath);  // 第一次调用时才加载
        }
        realImage->display();
    }
    
    ~ImageProxy() { delete realImage; }
};

核心区别:远程代理解决的是「对象不在同一个地址空间」的问题,虚拟代理解决的是「对象创建成本太高」的问题。两者都是代理模式,但动机不同。

知识体系结构图

下面这张图展示了代理模式的核心知识脉络,从模式动机到具体实现,再到不同变体之间的关系。

代理模式知识体系 代理模式 模式动机:控制对象访问 ▼ 两种实现方式 静态代理 编译期确定,代码写死 动态代理 运行时生成,灵活通用 ▼ 常见变体 远程代理 隐藏网络通信细节 虚拟代理 延迟加载,按需创建 其他变体:保护代理、缓存代理、日志代理等

代理模式的核心就这些。静态代理简单直接,动态代理灵活通用。远程代理让你感觉不到网络的存在,虚拟代理让你感觉不到加载的延迟。选哪种,取决于你的具体场景。

我个人更倾向于:能用静态代理解决的问题,就别上动态代理。代码少、好调试、容易理解。但如果你要代理的对象类型不确定,或者代理逻辑经常变化,动态代理才是正确的选择。

最后说一句:代理模式不是银弹。别为了用模式而用模式。如果你只是想在调用前后加点逻辑,一个简单的函数包装器可能就够了。模式是工具,不是目的。

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