1. 项目概述:为什么我们需要一个“替身”?
在C++的世界里,尤其是当你开始构建大型、复杂的系统时,代码的耦合度、性能瓶颈和安全控制往往会成为让你头疼的“三座大山”。想象一下,你有一个负责加载高清图片的类,每次调用都耗时数秒;或者你有一个核心数据库访问对象,你希望所有操作都必须经过权限校验;又或者,你正在使用一个网络服务,但它的接口庞大而笨重,你只想按需加载部分功能。直接修改这些核心类?风险太高,可能牵一发而动全身。这时候,你就需要一个“替身”——这就是代理模式(Proxy Pattern)要解决的问题。
代理模式是一种结构型设计模式,其核心思想是为另一个对象提供一个替身或占位符,以控制对这个对象的访问。这个“替身”与原始对象(我们称之为“真实主题”或“RealSubject”)实现相同的接口,因此客户端可以无感知地使用代理来代替真实对象。代理在客户端和真实对象之间插入了一层,这层“中间人”可以做很多事情:延迟加载昂贵的资源(虚拟代理)、控制访问权限(保护代理)、记录日志(日志代理)、缓存结果(缓存代理)或者为远程对象提供一个本地代表(远程代理)。
对于C++开发者而言,理解代理模式不仅仅是多学一个设计模式,更是掌握了一种解耦核心业务逻辑与横切关注点(如日志、缓存、安全)的利器。它能让你在不污染核心业务代码的前提下,优雅地添加各种控制逻辑,极大地提升了代码的可维护性和灵活性。接下来,我们将深入拆解这个模式的方方面面,从原理到实践,从基础代码到高级应用,让你彻底掌握如何在C++项目中用好这个“替身演员”。
2. 代理模式的核心结构与原理拆解
2.1 模式中的四大角色
要理解代理模式,首先得搞清楚这场戏里的几个关键角色。它们共同协作,完成一次对真实对象的间接访问。
- 抽象主题(Subject):这是一个接口或抽象基类,声明了真实主题和代理的共同操作。它是客户端依赖的契约,保证了代理和真实主题的可互换性。在C++中,我们通常用一个包含纯虚函数的类来实现它。
- 真实主题(RealSubject):这是真正执行业务逻辑的对象,包含了系统核心功能。它实现了
Subject接口。客户端最终想要的就是这个对象的服务,但代理模式让客户端不直接接触它。 - 代理(Proxy):这是模式的核心。它同样实现了
Subject接口,并且内部持有一个对RealSubject对象的引用(或指针)。代理负责在调用真实主题的方法前后,执行一些额外的操作。客户端与代理交互,而非直接与真实主题交互。 - 客户端(Client):使用
Subject接口与对象进行交互的代码。由于代理和真实主题都遵循同一接口,客户端无需关心自己操作的是代理还是真实对象。
它们之间的关系,可以用一个简单的依赖图来理解:Client依赖于Subject接口。RealSubject和Proxy都实现(继承自)Subject。Proxy内部组合(拥有)一个RealSubject的实例。
2.2 工作流程与UML思想
虽然我们不画UML图,但理解其思想至关重要。整个工作流程就像一个精心设计的接力赛:
- 客户端发起请求:客户端代码调用
Subject接口的某个方法(例如Request()),它此时持有的可能是一个Proxy对象。 - 代理拦截并预处理:
Proxy对象的方法被调用。在将请求转发给真实的RealSubject之前,代理会执行其附加职责。这可能是:- 访问控制:检查客户端是否有权限执行该操作。
- 延迟初始化:如果
RealSubject还未创建(比如因为它构造成本很高),则在此刻创建它。 - 日志记录:记录方法调用的时间、参数等信息。
- 缓存:检查请求的结果是否已经存在缓存中,若有则直接返回,避免调用真实对象。
- 委托真实主题:代理通过其内部持有的
RealSubject引用,调用真实对象的对应方法。 - 代理后处理:在从
RealSubject获得结果后,代理可能还会进行一些后处理,比如更新缓存、格式化返回结果等。 - 结果返回客户端:最终,代理将结果(可能是原始结果,也可能是处理过的结果)返回给客户端。对客户端来说,它只是调用了一个接口方法并得到了结果,完全不知道背后发生了这么多故事。
这种结构的精妙之处在于“对扩展开放,对修改关闭”。如果你想增加新的横切逻辑(比如新的安全检查规则),你只需要创建一个新的代理类,或者修改现有代理,而完全不需要触碰核心的RealSubject业务代码。这完美符合了开闭原则(OCP)。
2.3 与其他模式的关键区别
新手常常混淆代理模式和几个外观相似的模式,这里必须厘清:
- 与装饰器模式(Decorator)的区别:这是最容易混淆的一点。两者都基于组合,并且实现了相同的接口。但意图截然不同。
- 代理的核心是控制访问。它通常管理其真实主题的生命周期(如延迟创建),并且代理和真实主题的关系在编译时或初始化时就相对确定了。代理的目标是成为真实主题的“门卫”或“代表”。
- 装饰器的核心是动态添加职责。它旨在为对象增加新的行为,并且装饰器可以层层嵌套,不断添加新功能。装饰器通常不控制其组件的生命周期,而是透明地包装它。简单说,代理是“经纪人”(控制你见不见明星),装饰器是“造型师”(给明星不断换衣服加配饰)。
- 与适配器模式(Adapter)的区别:适配器解决的是接口不兼容的问题,它会把一个类的接口转换成客户端期望的另一个接口。而代理和真实主题的接口是完全相同的,不存在转换。
- 与外观模式(Facade)的区别:外观是为复杂的子系统提供一个简化的统一接口。它可能封装多个对象。而代理通常只代表一个对象,并且接口与该对象一致,并非简化。
理解这些区别,能帮助你在实际设计中做出最合适的选择。
3. C++中代理模式的多种实现变体与代码精讲
理论说再多,不如一行代码。在C++中实现代理模式,我们需要特别注意资源管理(尤其是内存)、接口设计以及各种变体的实现技巧。
3.1 基础保护代理实现:权限控制的典范
让我们从一个最经典的“保护代理”开始。假设我们有一个敏感文档接口IDocument和一个真实文档SecretDocument,我们希望所有访问都必须经过权限验证。
#include <iostream> #include <string> // 1. 抽象主题 (Subject) class IDocument { public: virtual ~IDocument() = default; virtual void displayContent(const std::string& viewer) const = 0; }; // 2. 真实主题 (RealSubject) class SecretDocument : public IDocument { std::string content_; public: SecretDocument(const std::string& content) : content_(content) {} void displayContent(const std::string& viewer) const override { // 核心业务逻辑:展示内容 std::cout << "[SecretDocument] Displaying to \"" << viewer << "\": " << content_ << std::endl; } }; // 3. 代理 (Proxy) - 保护代理 class DocumentAccessProxy : public IDocument { SecretDocument* real_document_; // 持有真实对象的指针 std::string authorized_user_; bool checkAccess(const std::string& viewer) const { // 访问控制逻辑 std::cout << "[Proxy] Access check for \"" << viewer << "\". "; if (viewer == authorized_user_) { std::cout << "Granted." << std::endl; return true; } else { std::cout << "Denied!" << std::endl; return false; } } public: // 代理负责创建或接收真实对象 DocumentAccessProxy(const std::string& doc_content, const std::string& authorized_user) : authorized_user_(authorized_user) { real_document_ = new SecretDocument(doc_content); // 这里可以是懒加载 } ~DocumentAccessProxy() { delete real_document_; // 代理管理真实对象的生命周期 } // 实现相同的接口,但加入了控制逻辑 void displayContent(const std::string& viewer) const override { if (checkAccess(viewer)) { real_document_->displayContent(viewer); // 委托调用 } else { std::cout << "[Proxy] Access denied. Content hidden." << std::endl; } } }; // 4. 客户端 (Client) int main() { std::cout << "=== 保护代理演示 ===\n"; // 客户端通过代理访问文档,不知道后面是SecretDocument IDocument* doc_for_alice = new DocumentAccessProxy("Top Secret: Project Aurora", "Alice"); IDocument* doc_for_bob = new DocumentAccessProxy("Confidential: Budget Q4", "Bob"); std::cout << "\nAlice tries to view her document:" << std::endl; doc_for_alice->displayContent("Alice"); // 成功 std::cout << "\nBob tries to view Alice's document:" << std::endl; doc_for_alice->displayContent("Bob"); // 失败 std::cout << "\nBob views his own document:" << std::endl; doc_for_bob->displayContent("Bob"); // 成功 delete doc_for_alice; delete doc_for_bob; return 0; }代码解析与心得:
- 接口一致性:
DocumentAccessProxy和SecretDocument都继承自IDocument,这是模式能工作的基石。 - 生命周期管理:在这个示例中,代理在构造函数中创建了真实对象,并在析构函数中销毁它。这体现了代理对真实对象生命周期的控制。这是一种常见做法,但并非唯一。
- 控制点:所有的访问控制逻辑都集中在代理的
checkAccess和displayContent方法中。如果未来权限规则变化(比如增加角色校验),只需修改代理类,SecretDocument毫发无损。 const正确性:注意displayContent被声明为const,这意味着它不应修改对象状态。代理的checkAccess也应是const,因为它只做检查。这是C++实现中保证语义正确性的重要细节。
3.2 虚拟代理与智能指针:优雅的延迟加载
虚拟代理用于延迟创建开销大的对象。在C++中,这常常与智能指针和惰性初始化结合,实现得既安全又高效。我们以加载一个大图像为例。
#include <iostream> #include <memory> #include <string> #include <thread> #include <chrono> // 抽象主题 class IImage { public: virtual ~IImage() = default; virtual void display() const = 0; virtual int getWidth() const = 0; virtual int getHeight() const = 0; }; // 真实主题 - 构造和加载“代价高昂” class HighResImage : public IImage { std::string filename_; int width_; int height_; // 假设这里有一个很大的像素数据数组... void loadFromDisk() const { // 模拟一个耗时的I/O操作 std::cout << "[HighResImage] Loading \"" << filename_ << "\" from disk... (Heavy operation)" << std::endl; std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟2秒加载时间 std::cout << "[HighResImage] Loading complete." << std::endl; } public: HighResImage(const std::string& filename, int width, int height) : filename_(filename), width_(width), height_(height) { // 注意:构造函数里不进行加载!这是关键。 std::cout << "[HighResImage] Object for \"" << filename_ << "\" constructed (data not loaded yet)." << std::endl; } void display() const override { loadFromDisk(); // 第一次真正需要显示时才加载 std::cout << "[HighResImage] Displaying image: " << filename_ << " (" << width_ << "x" << height_ << ")" << std::endl; } int getWidth() const override { return width_; } int getHeight() const override { return height_; } }; // 虚拟代理 class ImageProxy : public IImage { mutable std::unique_ptr<HighResImage> real_image_; // 使用智能指针,mutable用于延迟加载 std::string filename_; int width_; int height_; // 确保真实对象存在的辅助方法 void ensureImageLoaded() const { if (!real_image_) { std::cout << "[ImageProxy] Creating real HighResImage object now." << std::endl; real_image_ = std::make_unique<HighResImage>(filename_, width_, height_); } } public: ImageProxy(const std::string& filename, int width, int height) : filename_(filename), width_(width), height_(height) { // 代理构造非常快,只存储元数据 std::cout << "[ImageProxy] Proxy created for \"" << filename_ << "\". Real image not loaded." << std::endl; } void display() const override { std::cout << "[ImageProxy] display() called." << std::endl; ensureImageLoaded(); // 懒加载! real_image_->display(); } // 对于getWidth/getHeight,我们可能不需要加载真实图像 int getWidth() const override { std::cout << "[ImageProxy] getWidth() called (metadata only)." << std::endl; return width_; // 直接返回代理存储的元数据 } int getHeight() const override { std::cout << "[ImageProxy] getHeight() called (metadata only)." << std::endl; return height_; } }; // 客户端 int main() { std::cout << "=== 虚拟代理(延迟加载)演示 ===\n"; // 创建代理很快,系统启动时毫无压力 std::unique_ptr<IImage> image1 = std::make_unique<ImageProxy>("photo1.jpg", 1920, 1080); std::unique_ptr<IImage> image2 = std::make_unique<ImageProxy>("photo2.png", 3840, 2160); std::cout << "\n--- 操作1:获取图片尺寸(无需加载)---" << std::endl; std::cout << "Image1 width: " << image1->getWidth() << std::endl; std::cout << "\n--- 操作2:显示图片1(触发加载)---" << std::endl; image1->display(); std::cout << "\n--- 操作3:再次显示图片1(已加载,无延迟)---" << std::endl; image1->display(); std::cout << "\n--- 操作4:显示图片2(触发第二次加载)---" << std::endl; image2->display(); return 0; }关键技巧与避坑指南:
mutable关键字:由于display()是const方法,但我们需要在内部修改real_image_(从nullptr变为有效指针)。mutable允许在const成员函数中修改这个成员,这在这里是语义正确的——延迟加载并不改变对象的逻辑状态。- 智能指针管理:使用
std::unique_ptr自动管理真实对象的生命周期,避免了手动new/delete的内存泄漏风险。这是现代C++的推荐做法。 - 元数据与真实数据分离:代理可以存储一些无需加载真实对象就能提供的信息(如
width_,height_,filename_)。这进一步优化了性能。 - 线程安全考虑:上面的
ensureImageLoaded不是线程安全的。如果在多线程环境下,多个线程同时首次调用display(),可能会导致real_image_被多次创建(尽管unique_ptr的赋值本身是原子的,但make_unique和检查!real_image_不是)。在生产环境中,你需要使用互斥锁(std::mutex)或原子操作配合双重检查锁定模式来确保安全。这是一个非常重要的注意事项!
// 线程安全的 ensureImageLoaded 示例(简化版) #include <mutex> class ThreadSafeImageProxy : public IImage { // ... mutable std::once_flag load_flag_; // 使用 std::once_flag void ensureImageLoaded() const { std::call_once(load_flag_, [this] { real_image_ = std::make_unique<HighResImage>(filename_, width_, height_); }); } };3.3 缓存代理实战:为昂贵计算加速
缓存代理存储之前请求的结果,对于计算密集型或远程服务调用特别有效。我们以一个模拟的昂贵计算器为例。
#include <iostream> #include <unordered_map> #include <string> #include <cmath> // 抽象主题 class ICalculator { public: virtual ~ICalculator() = default; virtual double computeExpensiveValue(int input) = 0; }; // 真实主题 - 模拟昂贵计算 class ExpensiveCalculator : public ICalculator { double heavyComputation(int x) const { // 模拟复杂计算:例如计算斐波那契、素数判断、物理模拟等 std::cout << "[ExpensiveCalculator] Performing heavy computation for input " << x << "..." << std::endl; // 这里用一个耗时循环模拟 volatile double result = 0.0; // volatile 防止被优化掉 for (long i = 0; i < 100000000L; ++i) { result += std::sin(i) * std::cos(i); } return static_cast<double>(x) * 2.5 + result * 0.0; // 简单公式,重点是循环耗时 } public: double computeExpensiveValue(int input) override { return heavyComputation(input); } }; // 缓存代理 class CachingCalculatorProxy : public ICalculator { ICalculator* real_calculator_; // 缓存表:输入值 -> 计算结果 std::unordered_map<int, double> cache_; public: // 可以传入外部创建的真实计算器,更灵活 CachingCalculatorProxy(ICalculator* calculator) : real_calculator_(calculator) { std::cout << "[CachingProxy] Proxy initialized with cache." << std::endl; } ~CachingCalculatorProxy() { delete real_calculator_; // 代理负责清理 } double computeExpensiveValue(int input) override { std::cout << "[CachingProxy] Request for input: " << input << std::endl; // 1. 检查缓存 auto it = cache_.find(input); if (it != cache_.end()) { std::cout << " -> Cache HIT! Returning cached value: " << it->second << std::endl; return it->second; } // 2. 缓存未命中,委托真实对象计算 std::cout << " -> Cache MISS. Delegating to real calculator..." << std::endl; double result = real_calculator_->computeExpensiveValue(input); // 3. 存入缓存 std::cout << " -> Caching result: " << result << std::endl; cache_[input] = result; return result; } // 辅助方法:清空缓存(在某些场景下很有用,如数据更新后) void clearCache() { std::cout << "[CachingProxy] Cache cleared." << std::endl; cache_.clear(); } }; int main() { std::cout << "=== 缓存代理演示 ===\n"; // 创建代理,并传入真实对象 ICalculator* calculator = new CachingCalculatorProxy(new ExpensiveCalculator()); std::cout << "\n--- 第一次计算 input=10 (慢) ---" << std::endl; double r1 = calculator->computeExpensiveValue(10); std::cout << "Result: " << r1 << std::endl; std::cout << "\n--- 第二次计算 input=10 (快,从缓存取) ---" << std::endl; double r2 = calculator->computeExpensiveValue(10); std::cout << "Result: " << r2 << std::endl; std::cout << "\n--- 计算新值 input=20 (慢) ---" << std::endl; double r3 = calculator->computeExpensiveValue(20); std::cout << "Result: " << r3 << std::endl; std::cout << "\n--- 第三次计算 input=10 (依然快) ---" << std::endl; double r4 = calculator->computeExpensiveValue(10); std::cout << "Result: " << r4 << std::endl; delete calculator; // 删除代理,代理会删除真实计算器 return 0; }设计要点与扩展思考:
- 缓存策略:这里使用了最简单的永久缓存(
unordered_map)。实际应用中,你可能需要更复杂的策略:- LRU(最近最少使用)缓存:当缓存满时,淘汰最久未使用的条目。
- TTL(生存时间):为缓存项设置过期时间。
- 缓存失效:当底层数据发生变化时,如何清空或更新相关缓存?这通常是一个难点,需要根据业务逻辑设计(如通过事件通知、定时刷新或手动调用
clearCache)。
- 线程安全:和虚拟代理一样,对
cache_的并发访问需要加锁保护,否则会导致数据竞争。可以使用std::shared_mutex(C++17)实现读写锁,允许多个读并发。 - 内存管理:示例中代理接管了真实对象的生命周期(在析构函数中
delete)。你也可以使用shared_ptr来共享所有权,或者由外部管理生命周期,代理只持有引用或弱引用,这取决于具体场景。
4. 高级主题:现代C++下的代理模式演进与性能考量
4.1 使用模板实现通用代理
上面的例子都是针对特定接口的。如果我们想写一个能包装任何类型的通用代理(比如一个通用的日志代理),可以使用模板。这在C++标准库的智能指针(如shared_ptr)和某些测试框架的Mock对象中很常见。
#include <iostream> #include <memory> #include <string> // 一个简单的日志包装器(模板代理) template <typename Subject> class LoggingProxy { std::unique_ptr<Subject> real_subject_; std::string name_; void log(const std::string& msg) const { std::cout << "[LoggingProxy<" << typeid(Subject).name() << "> \"" << name_ << "\"] " << msg << std::endl; } public: // 完美转发构造函数参数给真实对象 template<typename... Args> LoggingProxy(const std::string& name, Args&&... args) : name_(name), real_subject_(std::make_unique<Subject>(std::forward<Args>(args)...)) { log("Created."); } // 重载箭头运算符,使代理用起来像指针 Subject* operator->() { log("Method accessed via ->"); return real_subject_.get(); } const Subject* operator->() const { log("Method accessed via -> (const)"); return real_subject_.get(); } // 也可以重载解引用运算符 Subject& operator*() { log("Dereferenced via *"); return *real_subject_; } const Subject& operator*() const { log("Dereferenced via * (const)"); return *real_subject_; } }; // 一个简单的服务类 class DatabaseService { public: void connect() { std::cout << "DatabaseService: Connected.\n"; } void query(const std::string& sql) { std::cout << "DatabaseService: Executing: " << sql << "\n"; } void disconnect() { std::cout << "DatabaseService: Disconnected.\n"; } }; int main() { std::cout << "=== 模板化通用日志代理演示 ===\n"; // 创建带日志的数据库服务代理 LoggingProxy<DatabaseService> db_proxy("MainDB", /* 可以传参数给DatabaseService构造函数 */); // 使用起来就像使用DatabaseService本身一样 db_proxy->connect(); db_proxy->query("SELECT * FROM users"); (*db_proxy).disconnect(); // 也可以使用解引用方式 return 0; }这个LoggingProxy可以包装任何类型Subject,自动记录所有通过它进行的访问。它通过重载operator->和operator*来实现透明代理,这是C++中实现“智能引用”或“代理指针”的经典手法。
4.2 代理模式的性能开销与优化
代理模式引入了间接层,这必然会带来一些开销:
- 额外的函数调用:客户端调用代理方法,代理再调用真实方法。在性能极其敏感的循环中,这可能成为瓶颈。
- 动态分配:大多数代理实现需要在堆上分配真实对象(尤其是虚拟代理),这比栈上分配或直接成员变量更慢。
- 缓存不友好:多一层指针跳转可能对CPU缓存不友好。
优化策略:
- 权衡使用:不要为了模式而模式。如果控制逻辑非常简单(比如只是一行日志),并且性能是关键,可以考虑使用编译时技术(如策略模式、CRTP)或直接内联代码。
- 静态多态(CRTP):对于在编译时已知的类型,可以使用奇异递归模板模式(CRTP)来实现零开销的代理抽象。但这会牺牲一些运行时灵活性。
- 内存池:如果频繁创建/销毁代理对象,可以考虑使用对象池来管理真实对象的内存分配。
- 选择性代理:并非所有方法都需要代理。可以为那些真正需要控制的方法提供代理,其他方法直接转发(或甚至让代理公开继承自真实类,但这会加强耦合)。
4.3 在真实框架与库中的应用窥探
代理模式在大型C++项目和库中无处不在:
- STL智能指针:
std::shared_ptr,std::unique_ptr在某种程度上可以看作是一种代理,它们管理着原始指针的生命周期,并可以定制删除器(一种行为扩展)。 - ORM(对象关系映射)库:常用虚拟代理来实现关系的延迟加载。例如,一个
User对象有一个Profile属性,只有在第一次访问user.getProfile()时,ORM才从数据库加载Profile数据。 - 远程过程调用(RPC)框架:客户端持有的本地对象实际上是一个“存根”(Stub)或代理,它负责将方法调用序列化,通过网络发送给远程服务器,并反序列化结果返回给客户端。这是远程代理的典型应用。
- 测试Mock框架:如Google Mock,它通过创建接口的代理(Mock对象)来记录调用、验证行为并返回预设的响应,用于单元测试。
理解这些实际应用,能让你在设计系统时,更自然地想到并运用代理模式。
5. 常见问题、陷阱与最佳实践实录
在实际项目中应用代理模式,我踩过不少坑,也总结了一些经验。
5.1 典型问题排查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 客户端代码编译错误,提示找不到某个方法 | 代理类没有实现抽象主题接口的所有纯虚函数。 | 检查代理类是否完整覆盖了Subject接口中的所有方法。即使是简单的转发,也要实现。 |
| 程序运行时崩溃,访问了非法内存 | 1. 代理内部持有的真实对象指针为nullptr(虚拟代理中未初始化就使用)。2. 真实对象已被销毁(生命周期管理错误)。 | 1. 在代理的每个方法中,确保真实对象已初始化(懒加载检查)。 2. 明确所有权。使用智能指针( std::unique_ptr或std::shared_ptr)自动管理生命周期。确保代理的生命周期覆盖真实对象的使用期。 |
| 多线程环境下,缓存代理返回了错误或陈旧的数据 | 对缓存容器的读写没有同步(数据竞争)。 | 使用互斥锁(std::mutex)或读写锁(std::shared_mutex)保护共享的缓存数据结构。对于高性能场景,考虑无锁数据结构或线程局部存储。 |
| 性能分析显示代理成为热点 | 代理的控制逻辑本身过于复杂(如复杂的权限检查、日志序列化),或代理层数过多(过度设计)。 | 优化代理逻辑。考虑是否所有方法都需要代理?能否将一些检查移到客户端或其它地方?避免不必要的代理嵌套。 |
| 无法通过代理访问真实对象的非虚函数或保护/私有成员 | 代理只继承了公共接口。如果客户端需要访问真实对象特有的非虚函数,则破坏了代理的透明性。 | 这通常是一个设计问题。重新考虑:这个功能是否应该属于Subject接口?如果必须暴露,可以考虑在代理中提供特化的方法(但这会降低通用性),或者使用friend声明(增加耦合),需谨慎权衡。 |
5.2 必须牢记的避坑指南
- 接口设计是第一要务:代理模式强依赖于一个良好的、稳定的抽象接口(
Subject)。如果Subject接口经常变动,那么代理类和所有真实主题类都需要同步修改,维护成本剧增。在设计初期就要仔细考虑接口的职责。 - 小心循环引用:如果代理和真实对象相互持有对方的
shared_ptr,会导致循环引用,内存永远无法释放。使用weak_ptr来打破循环。 - 区分“继承”与“组合”:代理通过组合持有真实对象,并通过继承实现相同接口。不要错误地让代理公开继承自真实对象的具体类(而不是接口),这会导致代理与具体实现紧密耦合,失去了模式的灵活性。
- 考虑代理的粒度:你是为整个对象做一个大代理,还是为对象的每个方法做细粒度代理?通常,代理整个对象更常见。但在某些场景下(如某个方法特别耗时,其他方法不需要控制),也可以只为特定方法创建代理,但这会让设计更复杂。
- 测试策略:由于代理插在中间,测试时需要同时测试:a) 客户端通过代理工作的正确性;b) 代理自身的控制逻辑(如权限检查、缓存);c) 真实对象的逻辑。Mock对象在测试代理时非常有用,你可以用Mock对象作为真实对象注入代理,来验证代理的行为。
5.3 何时用,何时不用?
适合使用代理模式的场景:
- 延迟初始化(虚拟代理):对象创建开销大,希望推迟到真正需要时。
- 访问控制(保护代理):需要根据不同权限控制对原始对象的访问。
- 本地执行远程服务(远程代理):例如RPC、CORBA、Web Service客户端存根。
- 缓存请求结果(缓存代理):特别是对于计算复杂或网络获取的数据。
- 记录日志或审计:需要在不修改业务代码的情况下记录方法的调用和参数。
- 智能引用:在引用真实对象前执行额外操作,如引用计数、线程安全检查、惰性加载等。
可能不需要代理模式的场景:
- 如果与真实对象的接口不同:请考虑适配器模式。
- 如果目的是动态地、透明地给对象添加职责:请考虑装饰器模式。
- 如果逻辑非常简单,且性能要求极高:直接修改代码或使用编译时技术可能更合适。
- 如果对象本身非常轻量,且没有横切关注点的需求:引入代理只会增加不必要的复杂性。
代理模式是C++开发者工具箱中一件强大而灵活的工具。它通过引入一个间接层,优雅地解决了访问控制、功能增强与核心逻辑分离的问题。掌握它,不仅能让你写出更清晰、更易维护的代码,更能让你在面对复杂系统设计时多一种从容的解决方案。记住,所有的设计模式都是“术”,而真正的“道”在于深刻理解问题本身,并选择最合适的那把“钥匙”。