代理模式:模式动机与定义、静态代理与动态代理、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也套上代理,那纯粹是过度设计。记住,模式是工具,不是目的。

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