设计模式入门:5. 代理模式详解 C++实现

代理模式详解:控制对象访问的"中间人",C++完整实现

引言

你有没有过这样的经历:想海淘一件国外的商品,自己直接买很麻烦,于是找了代购;想租房子,不想挨个找房东,于是找了中介;想访问国外的网站,直接访问不了,于是用了代理。

这些生活中的"中间人",在软件开发中对应着一种非常重要的设计模式------代理模式(Proxy Pattern)。它是一种结构型设计模式,允许你为另一个对象提供一个替代品或占位符,通过代理对象来控制对真实对象的访问。

代理模式就像一个"门卫",所有对真实对象的请求都必须先经过它。它可以在请求到达真实对象之前做预处理,也可以在请求返回之后做后处理,甚至可以直接拒绝请求。

今天我们就用C++语言,从基础概念到完整实现,全面深入地理解代理模式。


一、代理模式的核心概念

1.1 解决的痛点

在软件开发中,我们经常会遇到需要控制对某个对象访问的情况:

  • 对象创建开销很大(比如加载大图片、连接数据库)
  • 对象位于远程服务器上,访问需要网络通信
  • 需要给对象的访问添加权限控制
  • 需要记录对象的访问日志
  • 需要缓存对象的计算结果,避免重复计算

如果直接让客户端访问真实对象,会导致代码耦合度高、性能差、难以维护。代理模式通过引入一个中间代理对象,完美解决了这些问题。

1.2 核心思想

代理模式的核心思想是:创建一个代理对象,作为真实对象的替身,客户端通过代理对象间接访问真实对象。代理对象可以在不修改真实对象代码的情况下,添加额外的逻辑(如权限检查、日志记录、缓存、延迟加载等)。

代理模式的关键在于透明性:客户端使用代理对象和使用真实对象的方式完全相同,不需要知道代理的存在,也不需要知道真实对象的实现细节。

1.3 三个核心角色

代理模式包含三个关键角色:

  1. 抽象主题(Subject):定义了真实主题和代理共同实现的接口,声明了客户端可以调用的方法
  2. 真实主题(RealSubject):被代理的真实对象,实现了抽象主题接口,包含了实际的业务逻辑
  3. 代理(Proxy):实现了抽象主题接口,持有一个真实主题的引用,在客户端调用方法时,将请求转发给真实主题,并在前后添加额外逻辑

二、标准代理模式实现

2.1 UML类图

2.2 C++实现(图片延迟加载例子)

我们用最经典的"图片延迟加载"例子来实现代理模式。假设我们有一个图片查看器,需要显示很多大图片。如果在程序启动时就加载所有图片,会非常耗时,占用大量内存。

使用代理模式,我们可以在真正需要显示图片的时候才加载它(延迟加载),大大提高程序的启动速度和内存利用率。

cpp 复制代码
#include <iostream>
#include <string>
#include <memory>

// 抽象主题:图片接口
class Image {
public:
    virtual ~Image() = default;
    virtual void display() = 0; // 显示图片
};

// 真实主题:真实图片类,负责实际加载和显示图片
class RealImage : public Image {
private:
    std::string filename_;

    // 模拟加载图片的耗时操作
    void loadFromDisk() {
        std::cout << "正在从磁盘加载图片: " << filename_ << std::endl;
        // 模拟耗时操作
        for (int i = 0; i < 3; ++i) {
            std::cout << ".";
        }
        std::cout << std::endl;
    }

public:
    explicit RealImage(std::string filename) 
        : filename_(std::move(filename)) {
        // 构造时就加载图片(真实对象的缺点)
        loadFromDisk();
    }

    void display() override {
        std::cout << "显示图片: " << filename_ << std::endl;
    }
};

// 代理:图片代理类,实现延迟加载
class ImageProxy : public Image {
private:
    std::string filename_;
    mutable std::unique_ptr<RealImage> real_image_; // 延迟创建真实对象

public:
    explicit ImageProxy(std::string filename) 
        : filename_(std::move(filename)) {}

    void display() override {
        // 第一次调用时才创建真实对象
        if (!real_image_) {
            std::cout << "第一次显示图片,开始加载..." << std::endl;
            real_image_ = std::make_unique<RealImage>(filename_);
        }
        // 转发请求给真实对象
        real_image_->display();
    }
};

// 客户端代码
int main() {
    std::cout << "=== 不使用代理的情况 ===" << std::endl;
    // 构造时就加载图片,即使不显示也会加载
    Image* real_image = new RealImage("风景.jpg");
    std::cout << "图片对象已创建,但还未显示" << std::endl;
    real_image->display();
    delete real_image;

    std::cout << "\n=== 使用代理的情况 ===" << std::endl;
    // 构造代理对象,不会加载图片
    Image* proxy_image = new ImageProxy("风景.jpg");
    std::cout << "代理对象已创建,图片尚未加载" << std::endl;
    
    // 第一次显示时才加载图片
    std::cout << "\n第一次调用display():" << std::endl;
    proxy_image->display();
    
    // 第二次显示时直接使用已加载的图片
    std::cout << "\n第二次调用display():" << std::endl;
    proxy_image->display();
    
    delete proxy_image;

    return 0;
}

2.3 运行结果

复制代码
=== 不使用代理的情况 ===
正在从磁盘加载图片: 风景.jpg
...
图片对象已创建,但还未显示
显示图片: 风景.jpg

=== 使用代理的情况 ===
代理对象已创建,图片尚未加载

第一次调用display():
第一次显示图片,开始加载...
正在从磁盘加载图片: 风景.jpg
...
显示图片: 风景.jpg

第二次调用display():
显示图片: 风景.jpg

2.4 代码解析

  • 抽象主题Image :定义了所有图片都必须实现的display()方法
  • 真实主题RealImage:负责实际加载和显示图片。它的缺点是在构造时就会加载图片,即使图片永远不会被显示
  • 代理ImageProxy :持有真实图片的引用,但在构造时不会创建真实对象。只有当客户端第一次调用display()方法时,才会创建真实图片对象并加载图片。之后再次调用display()时,直接使用已加载的图片

通过使用代理,我们实现了延迟加载,只有在真正需要的时候才创建和加载对象,大大提高了程序的性能和资源利用率。


三、代理模式的五种常见类型

代理模式根据用途的不同,可以分为五种常见类型,每种类型都有其特定的适用场景。

3.1 虚拟代理(Virtual Proxy)

用途:延迟创建开销大的对象,直到真正需要时才创建。

例子:上面的图片延迟加载例子就是典型的虚拟代理。其他例子包括:

  • 文档编辑器中,大图片的延迟加载
  • 数据库连接池,在第一次查询时才创建连接
  • 大型对象的懒加载

3.2 远程代理(Remote Proxy)

用途:为位于不同地址空间的对象提供本地代表,让客户端可以像访问本地对象一样访问远程对象。

例子

  • RPC(远程过程调用)框架
  • 分布式系统中的服务调用
  • 网络文件系统

简单实现示意

cpp 复制代码
class RemoteServiceProxy : public Service {
private:
    std::string server_address_;

public:
    explicit RemoteServiceProxy(std::string address) 
        : server_address_(std::move(address)) {}

    void request() override {
        // 1. 建立网络连接
        connectToServer(server_address_);
        // 2. 序列化请求数据
        serializeRequest();
        // 3. 发送请求到远程服务器
        sendRequest();
        // 4. 接收并反序列化响应
        receiveResponse();
        // 5. 返回结果给客户端
    }
};

3.3 保护代理(Protection Proxy)

用途:控制对真实对象的访问权限,根据不同的用户角色允许或拒绝某些操作。

例子

  • 系统的权限控制
  • 文件的读写权限控制
  • API接口的访问控制

C++实现

cpp 复制代码
// 抽象主题:文档接口
class Document {
public:
    virtual ~Document() = default;
    virtual void read() = 0;
    virtual void write(const std::string& content) = 0;
};

// 真实主题:真实文档
class RealDocument : public Document {
private:
    std::string content_;

public:
    void read() override {
        std::cout << "文档内容: " << content_ << std::endl;
    }

    void write(const std::string& content) override {
        content_ = content;
        std::cout << "写入内容成功" << std::endl;
    }
};

// 保护代理:文档代理,控制读写权限
class DocumentProxy : public Document {
private:
    std::unique_ptr<RealDocument> real_doc_;
    std::string user_role_; // 用户角色

public:
    DocumentProxy(std::string user_role) 
        : real_doc_(std::make_unique<RealDocument>()), 
          user_role_(std::move(user_role)) {}

    void read() override {
        // 所有用户都有读权限
        real_doc_->read();
    }

    void write(const std::string& content) override {
        // 只有管理员才有写权限
        if (user_role_ == "admin") {
            real_doc_->write(content);
        } else {
            std::cout << "权限不足:只有管理员才能写入文档" << std::endl;
        }
    }
};

// 客户端代码
int main() {
    std::cout << "=== 普通用户访问 ===" << std::endl;
    Document* user_doc = new DocumentProxy("user");
    user_doc->read(); // 可以读
    user_doc->write("新内容"); // 不能写

    std::cout << "\n=== 管理员访问 ===" << std::endl;
    Document* admin_doc = new DocumentProxy("admin");
    admin_doc->read(); // 可以读
    admin_doc->write("新内容"); // 可以写

    delete user_doc;
    delete admin_doc;
    return 0;
}

3.4 智能引用代理(Smart Reference Proxy)

用途:在访问对象时添加额外的操作,比如记录对象的引用计数,当没有引用时自动释放对象。

最经典的例子 :C++ STL中的std::shared_ptr就是一个智能引用代理。它在指针被复制时增加引用计数,在指针被销毁时减少引用计数,当引用计数为0时自动释放指向的对象。

简单实现示意

cpp 复制代码
template <typename T>
class SmartPtr {
private:
    T* ptr_;
    int* ref_count_;

public:
    explicit SmartPtr(T* ptr = nullptr) 
        : ptr_(ptr), ref_count_(new int(1)) {}

    // 拷贝构造函数
    SmartPtr(const SmartPtr& other) {
        ptr_ = other.ptr_;
        ref_count_ = other.ref_count_;
        (*ref_count_)++;
    }

    // 析构函数
    ~SmartPtr() {
        (*ref_count_)--;
        if (*ref_count_ == 0) {
            delete ptr_;
            delete ref_count_;
        }
    }

    // 重载->运算符
    T* operator->() {
        return ptr_;
    }

    // 重载*运算符
    T& operator*() {
        return *ptr_;
    }
};

3.5 缓存代理(Cache Proxy)

用途:为开销大的计算结果提供缓存,当多次请求相同结果时,直接返回缓存中的数据,避免重复计算。

例子

  • 数据库查询结果缓存
  • 网络请求结果缓存
  • 复杂计算结果缓存

C++实现示意

cpp 复制代码
class CalculatorProxy : public Calculator {
private:
    std::unique_ptr<RealCalculator> real_calculator_;
    mutable std::map<std::pair<int, int>, int> cache_; // 缓存计算结果

public:
    CalculatorProxy() 
        : real_calculator_(std::make_unique<RealCalculator>()) {}

    int add(int a, int b) override {
        auto key = std::make_pair(a, b);
        // 先查缓存
        if (cache_.find(key) != cache_.end()) {
            std::cout << "从缓存中获取结果" << std::endl;
            return cache_[key];
        }
        // 缓存中没有,调用真实计算器
        int result = real_calculator_->add(a, b);
        cache_[key] = result; // 存入缓存
        return result;
    }
};

四、代理模式的优缺点

4.1 优点

  1. 控制对象访问:可以在不修改真实对象的情况下,添加访问控制逻辑
  2. 提高性能:通过延迟加载、缓存等方式,减少不必要的计算和资源消耗
  3. 降低耦合度:客户端只依赖抽象主题接口,不需要知道真实对象的存在和实现细节
  4. 符合开闭原则:添加新的代理逻辑时,不需要修改现有代码
  5. 职责单一:真实对象只负责实现核心业务逻辑,代理对象负责处理辅助功能

4.2 缺点

  1. 增加系统复杂度:引入了额外的代理类,增加了代码的层数和复杂度
  2. 可能带来性能开销:每次调用都需要经过代理的转发,会有一定的性能损失
  3. 调试难度增加:问题可能出在代理层,也可能出在真实对象层,增加了调试难度

五、适用场景

代理模式特别适合以下场景:

  1. 需要延迟创建开销大的对象 → 使用虚拟代理
  2. 需要访问远程对象 → 使用远程代理
  3. 需要控制对象的访问权限 → 使用保护代理
  4. 需要管理对象的生命周期 → 使用智能引用代理
  5. 需要缓存计算结果 → 使用缓存代理
  6. 需要给对象添加日志、监控等辅助功能

六、与其他模式的对比

很多人容易把代理模式和其他结构型模式混淆,这里做一个清晰的对比:

模式 核心目的 与代理的区别
代理模式 控制对对象的访问 不改变接口,控制访问,客户端不知道真实对象
装饰器模式 动态给对象添加额外功能 不改变接口,增强功能,支持多层嵌套
适配器模式 转换接口,让不兼容的类一起工作 改变接口,不改变功能
外观模式 为复杂子系统提供简单统一接口 简化接口,而不是控制访问

最容易混淆的是代理模式和装饰器模式,它们的核心区别在于:

  • 代理模式 :关注于控制访问,通常只包装一层,客户端不知道真实对象的存在
  • 装饰器模式 :关注于增强功能,支持多层嵌套,客户端知道自己在装饰对象

七、现代C++改进建议

在现代C++(C++11及以后)中,我们可以对代理模式进行一些改进:

  1. 使用智能指针管理内存 :如前面的例子所示,使用std::unique_ptrstd::shared_ptr来管理真实对象的生命周期,避免内存泄漏
  2. 使用模板实现通用代理:可以编写一个模板代理类,适用于任何抽象主题接口
  3. 使用Lambda表达式简化简单代理 :对于只有一个方法的接口,可以使用std::function和Lambda表达式来实现简单的代理

八、总结

代理模式是一种非常实用的设计模式,它的核心思想是**"通过中间人控制对象访问"**。通过引入代理对象,我们可以在不修改真实对象代码的情况下,添加各种辅助功能,如延迟加载、权限控制、日志记录、缓存等。

在实际开发中,代理模式无处不在。从我们每天使用的智能指针,到RPC框架、数据库连接池、缓存系统,都能看到代理模式的身影。

记住,设计模式不是银弹。只有当你确实需要控制对对象的访问,并且直接访问会带来问题时,才应该使用代理模式。

希望这篇文章能帮助你彻底理解代理模式,并在实际项目中正确地使用它。

相关推荐
哈泽尔都1 小时前
运动控制教学——5分钟学会力控算法(阻抗/导纳/力位混合)
c++·python·算法·决策树·贪心算法·机器人·gpu算力
ZK_H1 小时前
MFC程序开发自学笔记其一——windows应用程序与c++基础
c++·笔记·mfc
cpp_25011 小时前
P10722 [GESP202406 六级] 二叉树
数据结构·c++·算法·题解·洛谷·树形结构·gesp六级
不负岁月无痕2 小时前
STL-- C++ stack_queue _priority_queue类 模拟实现
开发语言·c++
selt7912 小时前
Redisson 源码深度分析
java·c++·redis·lua
周末也要写八哥2 小时前
浅谈:C++中cpp 14 ~ cpp 17
开发语言·c++·算法
不会C语言的男孩2 小时前
C++ Primer 第13章:拷贝控制
开发语言·c++
c238562 小时前
map和set
数据结构·c++
basketball6162 小时前
C++进阶:3. unique_ptr 现代C++内存管理的基石
java·jvm·c++