
代理模式详解控制对象访问的中间人C完整实现引言你有没有过这样的经历想海淘一件国外的商品自己直接买很麻烦于是找了代购想租房子不想挨个找房东于是找了中介想访问国外的网站直接访问不了于是用了代理。这些生活中的中间人在软件开发中对应着一种非常重要的设计模式——代理模式(Proxy Pattern)。它是一种结构型设计模式允许你为另一个对象提供一个替代品或占位符通过代理对象来控制对真实对象的访问。代理模式就像一个门卫所有对真实对象的请求都必须先经过它。它可以在请求到达真实对象之前做预处理也可以在请求返回之后做后处理甚至可以直接拒绝请求。今天我们就用C语言从基础概念到完整实现全面深入地理解代理模式。一、代理模式的核心概念1.1 解决的痛点在软件开发中我们经常会遇到需要控制对某个对象访问的情况对象创建开销很大比如加载大图片、连接数据库对象位于远程服务器上访问需要网络通信需要给对象的访问添加权限控制需要记录对象的访问日志需要缓存对象的计算结果避免重复计算如果直接让客户端访问真实对象会导致代码耦合度高、性能差、难以维护。代理模式通过引入一个中间代理对象完美解决了这些问题。1.2 核心思想代理模式的核心思想是创建一个代理对象作为真实对象的替身客户端通过代理对象间接访问真实对象。代理对象可以在不修改真实对象代码的情况下添加额外的逻辑如权限检查、日志记录、缓存、延迟加载等。代理模式的关键在于透明性客户端使用代理对象和使用真实对象的方式完全相同不需要知道代理的存在也不需要知道真实对象的实现细节。1.3 三个核心角色代理模式包含三个关键角色抽象主题(Subject)定义了真实主题和代理共同实现的接口声明了客户端可以调用的方法真实主题(RealSubject)被代理的真实对象实现了抽象主题接口包含了实际的业务逻辑代理(Proxy)实现了抽象主题接口持有一个真实主题的引用在客户端调用方法时将请求转发给真实主题并在前后添加额外逻辑二、标准代理模式实现2.1 UML类图2.2 C实现图片延迟加载例子我们用最经典的图片延迟加载例子来实现代理模式。假设我们有一个图片查看器需要显示很多大图片。如果在程序启动时就加载所有图片会非常耗时占用大量内存。使用代理模式我们可以在真正需要显示图片的时候才加载它延迟加载大大提高程序的启动速度和内存利用率。#includeiostream#includestring#includememory// 抽象主题图片接口classImage{public:virtual~Image()default;virtualvoiddisplay()0;// 显示图片};// 真实主题真实图片类负责实际加载和显示图片classRealImage:publicImage{private:std::string filename_;// 模拟加载图片的耗时操作voidloadFromDisk(){std::cout正在从磁盘加载图片: filename_std::endl;// 模拟耗时操作for(inti0;i3;i){std::cout.;}std::coutstd::endl;}public:explicitRealImage(std::string filename):filename_(std::move(filename)){// 构造时就加载图片真实对象的缺点loadFromDisk();}voiddisplay()override{std::cout显示图片: filename_std::endl;}};// 代理图片代理类实现延迟加载classImageProxy:publicImage{private:std::string filename_;mutablestd::unique_ptrRealImagereal_image_;// 延迟创建真实对象public:explicitImageProxy(std::string filename):filename_(std::move(filename)){}voiddisplay()override{// 第一次调用时才创建真实对象if(!real_image_){std::cout第一次显示图片开始加载...std::endl;real_image_std::make_uniqueRealImage(filename_);}// 转发请求给真实对象real_image_-display();}};// 客户端代码intmain(){std::cout 不使用代理的情况 std::endl;// 构造时就加载图片即使不显示也会加载Image*real_imagenewRealImage(风景.jpg);std::cout图片对象已创建但还未显示std::endl;real_image-display();deletereal_image;std::cout\n 使用代理的情况 std::endl;// 构造代理对象不会加载图片Image*proxy_imagenewImageProxy(风景.jpg);std::cout代理对象已创建图片尚未加载std::endl;// 第一次显示时才加载图片std::cout\n第一次调用display():std::endl;proxy_image-display();// 第二次显示时直接使用已加载的图片std::cout\n第二次调用display():std::endl;proxy_image-display();deleteproxy_image;return0;}2.3 运行结果 不使用代理的情况 正在从磁盘加载图片: 风景.jpg ... 图片对象已创建但还未显示 显示图片: 风景.jpg 使用代理的情况 代理对象已创建图片尚未加载 第一次调用display(): 第一次显示图片开始加载... 正在从磁盘加载图片: 风景.jpg ... 显示图片: 风景.jpg 第二次调用display(): 显示图片: 风景.jpg2.4 代码解析抽象主题Image定义了所有图片都必须实现的display()方法真实主题RealImage负责实际加载和显示图片。它的缺点是在构造时就会加载图片即使图片永远不会被显示代理ImageProxy持有真实图片的引用但在构造时不会创建真实对象。只有当客户端第一次调用display()方法时才会创建真实图片对象并加载图片。之后再次调用display()时直接使用已加载的图片通过使用代理我们实现了延迟加载只有在真正需要的时候才创建和加载对象大大提高了程序的性能和资源利用率。三、代理模式的五种常见类型代理模式根据用途的不同可以分为五种常见类型每种类型都有其特定的适用场景。3.1 虚拟代理(Virtual Proxy)用途延迟创建开销大的对象直到真正需要时才创建。例子上面的图片延迟加载例子就是典型的虚拟代理。其他例子包括文档编辑器中大图片的延迟加载数据库连接池在第一次查询时才创建连接大型对象的懒加载3.2 远程代理(Remote Proxy)用途为位于不同地址空间的对象提供本地代表让客户端可以像访问本地对象一样访问远程对象。例子RPC远程过程调用框架分布式系统中的服务调用网络文件系统简单实现示意classRemoteServiceProxy:publicService{private:std::string server_address_;public:explicitRemoteServiceProxy(std::string address):server_address_(std::move(address)){}voidrequest()override{// 1. 建立网络连接connectToServer(server_address_);// 2. 序列化请求数据serializeRequest();// 3. 发送请求到远程服务器sendRequest();// 4. 接收并反序列化响应receiveResponse();// 5. 返回结果给客户端}};3.3 保护代理(Protection Proxy)用途控制对真实对象的访问权限根据不同的用户角色允许或拒绝某些操作。例子系统的权限控制文件的读写权限控制API接口的访问控制C实现// 抽象主题文档接口classDocument{public:virtual~Document()default;virtualvoidread()0;virtualvoidwrite(conststd::stringcontent)0;};// 真实主题真实文档classRealDocument:publicDocument{private:std::string content_;public:voidread()override{std::cout文档内容: content_std::endl;}voidwrite(conststd::stringcontent)override{content_content;std::cout写入内容成功std::endl;}};// 保护代理文档代理控制读写权限classDocumentProxy:publicDocument{private:std::unique_ptrRealDocumentreal_doc_;std::string user_role_;// 用户角色public:DocumentProxy(std::string user_role):real_doc_(std::make_uniqueRealDocument()),user_role_(std::move(user_role)){}voidread()override{// 所有用户都有读权限real_doc_-read();}voidwrite(conststd::stringcontent)override{// 只有管理员才有写权限if(user_role_admin){real_doc_-write(content);}else{std::cout权限不足只有管理员才能写入文档std::endl;}}};// 客户端代码intmain(){std::cout 普通用户访问 std::endl;Document*user_docnewDocumentProxy(user);user_doc-read();// 可以读user_doc-write(新内容);// 不能写std::cout\n 管理员访问 std::endl;Document*admin_docnewDocumentProxy(admin);admin_doc-read();// 可以读admin_doc-write(新内容);// 可以写deleteuser_doc;deleteadmin_doc;return0;}3.4 智能引用代理(Smart Reference Proxy)用途在访问对象时添加额外的操作比如记录对象的引用计数当没有引用时自动释放对象。最经典的例子C STL中的std::shared_ptr就是一个智能引用代理。它在指针被复制时增加引用计数在指针被销毁时减少引用计数当引用计数为0时自动释放指向的对象。简单实现示意templatetypenameTclassSmartPtr{private:T*ptr_;int*ref_count_;public:explicitSmartPtr(T*ptrnullptr):ptr_(ptr),ref_count_(newint(1)){}// 拷贝构造函数SmartPtr(constSmartPtrother){ptr_other.ptr_;ref_count_other.ref_count_;(*ref_count_);}// 析构函数~SmartPtr(){(*ref_count_)--;if(*ref_count_0){deleteptr_;deleteref_count_;}}// 重载-运算符T*operator-(){returnptr_;}// 重载*运算符Toperator*(){return*ptr_;}};3.5 缓存代理(Cache Proxy)用途为开销大的计算结果提供缓存当多次请求相同结果时直接返回缓存中的数据避免重复计算。例子数据库查询结果缓存网络请求结果缓存复杂计算结果缓存C实现示意classCalculatorProxy:publicCalculator{private:std::unique_ptrRealCalculatorreal_calculator_;mutablestd::mapstd::pairint,int,intcache_;// 缓存计算结果public:CalculatorProxy():real_calculator_(std::make_uniqueRealCalculator()){}intadd(inta,intb)override{autokeystd::make_pair(a,b);// 先查缓存if(cache_.find(key)!cache_.end()){std::cout从缓存中获取结果std::endl;returncache_[key];}// 缓存中没有调用真实计算器intresultreal_calculator_-add(a,b);cache_[key]result;// 存入缓存returnresult;}};四、代理模式的优缺点4.1 优点控制对象访问可以在不修改真实对象的情况下添加访问控制逻辑提高性能通过延迟加载、缓存等方式减少不必要的计算和资源消耗降低耦合度客户端只依赖抽象主题接口不需要知道真实对象的存在和实现细节符合开闭原则添加新的代理逻辑时不需要修改现有代码职责单一真实对象只负责实现核心业务逻辑代理对象负责处理辅助功能4.2 缺点增加系统复杂度引入了额外的代理类增加了代码的层数和复杂度可能带来性能开销每次调用都需要经过代理的转发会有一定的性能损失调试难度增加问题可能出在代理层也可能出在真实对象层增加了调试难度五、适用场景代理模式特别适合以下场景需要延迟创建开销大的对象→ 使用虚拟代理需要访问远程对象→ 使用远程代理需要控制对象的访问权限→ 使用保护代理需要管理对象的生命周期→ 使用智能引用代理需要缓存计算结果→ 使用缓存代理需要给对象添加日志、监控等辅助功能六、与其他模式的对比很多人容易把代理模式和其他结构型模式混淆这里做一个清晰的对比模式核心目的与代理的区别代理模式控制对对象的访问不改变接口控制访问客户端不知道真实对象装饰器模式动态给对象添加额外功能不改变接口增强功能支持多层嵌套适配器模式转换接口让不兼容的类一起工作改变接口不改变功能外观模式为复杂子系统提供简单统一接口简化接口而不是控制访问最容易混淆的是代理模式和装饰器模式它们的核心区别在于代理模式关注于控制访问通常只包装一层客户端不知道真实对象的存在装饰器模式关注于增强功能支持多层嵌套客户端知道自己在装饰对象七、现代C改进建议在现代CC11及以后中我们可以对代理模式进行一些改进使用智能指针管理内存如前面的例子所示使用std::unique_ptr和std::shared_ptr来管理真实对象的生命周期避免内存泄漏使用模板实现通用代理可以编写一个模板代理类适用于任何抽象主题接口使用Lambda表达式简化简单代理对于只有一个方法的接口可以使用std::function和Lambda表达式来实现简单的代理八、总结代理模式是一种非常实用的设计模式它的核心思想是**“通过中间人控制对象访问”**。通过引入代理对象我们可以在不修改真实对象代码的情况下添加各种辅助功能如延迟加载、权限控制、日志记录、缓存等。在实际开发中代理模式无处不在。从我们每天使用的智能指针到RPC框架、数据库连接池、缓存系统都能看到代理模式的身影。记住设计模式不是银弹。只有当你确实需要控制对对象的访问并且直接访问会带来问题时才应该使用代理模式。希望这篇文章能帮助你彻底理解代理模式并在实际项目中正确地使用它。