# C++设计模式:代理模式——控制对象访问的智慧之道 在软件开发中,有时我们不希望客户端直接访问某个对象,而是希望通过一个"中间人"来间接访问。这个"中间人"就是**代理**。 **场景 1:大图片加载问题** 假设你在开发一个图片浏览器,需要加载大量高清图片: ```cpp class HighResImage { public:     HighResImage(const std::string& path) {         // 从磁盘读取高清图片,耗时 2-5 秒         loadFromDisk(path);  // 阻塞操作!     }     void display() { /* 显示图片 */ } private:     void loadFromDisk(const std::string& path) {         std::cout << "正在加载高清图片: " << path                   << " (耗时 3 秒...)" << std::endl;         // 模拟耗时操作     }     std::vector data_;  // 可能几十MB }; // 问题:一次创建 100 张图片对象 void openAlbum() {     std::vector images;     for (int i = 0; i < 100; ++i) {         images.emplace_back("photo_" + std::to_string(i) + ".jpg");         // 每张图片加载 3 秒,100 张 = 300 秒!     }     // 用户只是想浏览,可能只会看其中几张... } ``` **问题**:100 张图片全部加载需要 300 秒,但用户可能只看其中 5 张! **场景 2:权限控制问题** ```cpp class Database { public:     void query(const std::string& sql) { /* 执行查询 */ }     void update(const std::string& sql) { /* 执行更新 */ }     void dropTable(const std::string& table) { /* 删除表 */ } }; // 问题:实习生也能调用 dropTable! Database db; db.dropTable("users");  // 灾难!没有任何权限检查 ``` **场景 3:远程服务调用** ```cpp // 你以为在调用本地对象 calculator.add(1, 2); // 实际上对象在另一台服务器上 // 需要:序列化参数 → 网络传输 → 反序列化 → 执行 → 返回结果 // 但客户端不应该关心这些细节 ``` 这三个场景有一个共同的解决思路:**在客户端和真实对象之间加一层代理**。 代理模式的思想在生活中随处可见: - **房产中介**就是房东的代理——你不需要直接联系房东,中介帮你筛选、谈价、签约。房东(真实对象)专注提供房源,中介(代理)负责控制访问流程。 - **信用卡**是现金的代理——你不需要随身携带大量现金,刷卡时银行会替你检查额度(权限控制)、记录消费(日志),最终完成支付(转发请求)。 - **律师**是当事人的代理——当事人不直接出庭,律师替你过滤问题、保护权益,再将必要信息转达给法庭。 这些例子的共同特点是:**代理与真实对象做"同样的事",但在过程中加入了额外的控制逻辑**。 ```bash 没有代理: [客户端] ────直接访问────→ [真实对象] 有代理: [客户端] ──→ [代理] ──控制访问──→ [真实对象]                 ↑            延迟加载 / 权限检查 / 缓存 / 日志 ``` ## 代理模式详解 ### 模式定义 > **代理模式(Proxy Pattern)**:为其他对象提供一种代理以控制对这个对象的访问。代理对象在客户端和目标对象之间起到中介作用,可以在不改变目标对象的前提下,添加额外的功能。 ### 核心思想 代理模式的核心可以用一句话概括:**通过与真实对象相同的接口,在访问前后做文章**。 理解"相同的接口"非常关键。代理之所以能替代真实对象,是因为它和真实对象实现了**同一个抽象接口**。对客户端来说,调用代理和直接调用真实对象没有任何区别——客户端甚至不知道自己在跟代理打交道。这种**对客户端的透明性**是代理模式的根本前提。 在此基础上,代理可以在三个时机"做文章": - • **访问前(Pre-processing)**:在将请求转发给真实对象之前,代理可以做检查或准备工作。例如检查调用者是否有权限、检查缓存中是否已有结果、或者在这个时刻才真正创建真实对象(延迟加载)。 - • **转发请求(Delegation)**:代理调用真实对象上对应的方法,完成实际的业务逻辑。这一步是代理和真实对象的"桥梁"。 - • **访问后(Post-processing)**:请求完成后,代理可以做收尾工作。例如记录日志、将结果存入缓存、统计调用次数等。 ``` 代理做什么? ├── 访问前:检查权限、延迟创建、查缓存 ├── 转发请求:调用真实对象的方法 └── 访问后:记录日志、更新缓存、释放资源 ``` ### 模式角色 ``` ┌─────────┐ │ Subject │ ← 抽象接口 │(抽象主题)│ └────┬────┘ │ ┌──────┴──────┐ │ │ ┌────┴────┐ ┌─────┴─────┐ │ Proxy │ │RealSubject│ ← 真实对象 │ (代理) │──→│ (真实主题) │ └─────────┘ └───────────┘ ``` 1. 1. **Subject(抽象主题)**:定义代理和真实对象的公共接口,客户端通过此接口操作 2. 2. **RealSubject(真实主题)**:实现具体业务逻辑的真实对象 3. 3. **Proxy(代理)**:持有真实对象的引用,实现与真实对象相同的接口,在转发请求前后添加额外逻辑 这三个角色的关系可以这样理解:**Subject 是合同模板**,规定了必须提供哪些服务(接口方法);**RealSubject 是真正干活的人**,提供了所有服务的具体实现;**Proxy 是中间人**,对外声称自己能提供同样的服务,但在真正干活之前或之后,会做一些额外的事情。 为什么需要 Subject 这个抽象接口?因为它让客户端可以**面向接口编程**——客户端持有的是 `Subject*` 或 `Subject&`,不关心背后是代理还是真实对象。这也是"里氏替换原则"的体现:任何使用真实对象的地方,都可以无缝替换为代理。 ### 基本实现结构 下面用一个图片加载的例子来演示代理模式的基本结构。`Image` 是抽象接口,`RealImage` 是真实的图片对象(构造时就会加载文件),`ImageProxy` 是代理(构造时只记录文件名,真正需要时才创建 `RealImage`)。 ```cpp #include  #include  #include  // 抽象主题:定义公共接口 class Image { public:     virtual ~Image() = default;     virtual void display() = 0;     virtual int getWidth() const = 0;     virtual int getHeight() const = 0; }; // 真实主题:加载和显示高清图片 class RealImage : public Image { public:     explicit RealImage(const std::string& filename)         : filename_(filename) {         loadFromDisk();  // 构造时立即加载(耗时操作)     }     void display() override {         std::cout << "显示图片: " << filename_ << std::endl;     }     int getWidth() const override { return width_; }     int getHeight() const override { return height_; } private:     void loadFromDisk() {         std::cout << "[耗时操作] 从磁盘加载: " << filename_ << std::endl;         width_ = 1920;         height_ = 1080;     }     std::string filename_;     int width_ = 0;     int height_ = 0; }; // 代理:控制对真实图片的访问 class ImageProxy : public Image { public:     explicit ImageProxy(const std::string& filename)         : filename_(filename) {         // 注意:这里不创建 RealImage,只记录文件名         std::cout << "[代理] 创建图片代理: " << filename_ << std::endl;     }     void display() override {         ensureLoaded();  // 真正需要时才加载         realImage_->display();     }     int getWidth() const override {         ensureLoaded();         return realImage_->getWidth();     }     int getHeight() const override {         ensureLoaded();         return realImage_->getHeight();     } private:     void ensureLoaded() const {         if (!realImage_) {             std::cout << "[代理] 首次访问,开始加载真实图片..." << std::endl;             realImage_ = std::make_unique(filename_);         }     }     std::string filename_;     mutable std::unique_ptr realImage_;  // 延迟创建 }; ``` 这段代码中有一个重要的 C++ 关键字需要理解: > **`mutable` 关键字**:正常情况下,一个对象的 `const`方法不能修改任何成员变量。但`mutable`打破了这个限制——被`mutable`修饰的成员变量,即使在`const`方法中也可以被修改。在代理模式中,`getWidth()`和`getHeight()`从语义上看是"只读"操作(所以是`const`方法),但延迟加载要求我们在第一次调用时创建真实对象,这就需要修改`realImage_`成员。`mutable` 让这种"逻辑上只读、实现上需要写入"的场景成为可能。 **使用示例**: ```cpp int main() {     // 创建 100 个图片代理(瞬间完成,不会加载任何图片)     std::vector> album;     for (int i = 0; i < 100; ++i) {         album.push_back(             std::make_unique("photo_" + std::to_string(i) + ".jpg")         );     }     std::cout << "相册准备完毕!(没有加载任何图片)\n" << std::endl;     // 只有用户查看的图片才会真正加载     album[0]->display();   // 第一次访问:加载 + 显示     std::cout << std::endl;     album[0]->display();   // 第二次访问:直接显示(已缓存)     std::cout << std::endl;     album[42]->display();  // 只加载用户要看的图片 } ``` 注意观察输出:创建 100 个代理几乎瞬间完成,因为代理的构造函数只是保存了文件名,没有做任何耗时操作。只有当 `display()` 被调用时,`ensureLoaded()` 才会检测到真实对象尚未创建,从而触发加载。第二次对同一张图片调用 `display()` 时,`realImage_` 已经存在,直接跳过加载步骤。这就是代理"延迟加载"的核心价值。 ------ ## 代理模式的分类 代理模式根据不同的用途,分为多种类型。下面逐一讲解最常用的四种。 ### 一、虚拟代理(Virtual Proxy)——延迟加载 虚拟代理是最常用的代理类型,核心思想是**延迟创建开销大的对象,直到真正需要时才创建**。 "虚拟"二字的含义是:在真实对象被创建出来之前,代理对象充当了一个"虚拟的替身"。客户端以为自己拿到了真实对象,但实际上拿到的只是一个轻量级的占位符。只有当客户端真正需要使用对象的功能时,代理才会在幕后创建真实对象。这种"按需创建"的策略在以下场景特别有用: - • **对象创建非常耗时**:例如加载大文件、建立网络连接、初始化复杂的数据结构 - • **对象可能根本不会被使用**:例如列表中的 100 个对象,用户可能只访问其中几个 - • **对象占用大量内存**:延迟创建可以避免一次性占用过多内存 上面基本实现中的 `ImageProxy` 就是一个虚拟代理。我们来看一个 3D 模型加载的示例,展示虚拟代理的一个进阶设计:代理可以对**部分方法不触发加载**(如获取文件名),只有**真正需要数据的方法才触发加载**(如渲染、获取顶点数)。 ```cpp #include  #include  #include  #include  #include  // 抽象接口:3D 模型 class Model3D { public:     virtual ~Model3D() = default;     virtual void render() = 0;     virtual std::string getName() const = 0;     virtual size_t getVertexCount() const = 0; }; // 真实对象:加载完整的 3D 模型(非常耗时) class RealModel3D : public Model3D { public:     explicit RealModel3D(const std::string& path) : path_(path) {         load();     }     void render() override {         std::cout << "  渲染3D模型 [" << name_                   << "] 顶点数: " << vertexCount_ << std::endl;     }     std::string getName() const override { return name_; }     size_t getVertexCount() const override { return vertexCount_; } private:     void load() {         std::cout << "  [加载中] 读取模型文件: " << path_ << std::endl;         std::this_thread::sleep_for(std::chrono::seconds(1));  // 模拟耗时         name_ = path_.substr(path_.find_last_of('/') + 1);         vertexCount_ = 50000;         std::cout << "  [加载完成] " << name_ << std::endl;     }     std::string path_;     std::string name_;     size_t vertexCount_ = 0; }; // 虚拟代理:延迟加载 3D 模型 class Model3DProxy : public Model3D { public:     explicit Model3DProxy(const std::string& path)         : path_(path),           name_(path.substr(path.find_last_of('/') + 1)) {}     void render() override {         ensureLoaded();         realModel_->render();     }     // 不需要加载就能返回的信息     std::string getName() const override { return name_; }     // 需要加载才能返回的信息     size_t getVertexCount() const override {         ensureLoaded();         return realModel_->getVertexCount();     } private:     void ensureLoaded() const {         if (!realModel_) {             realModel_ = std::make_unique(path_);         }     }     std::string path_;     std::string name_;     mutable std::unique_ptr realModel_; }; ``` **关键设计点**: - • `getName()` 不需要加载模型就能返回(代理本身缓存了文件名)。这是一个重要的优化思路:**能在代理层回答的问题,就不要麻烦真实对象**。文件名在创建代理时就已知,完全没必要为了获取名字而加载整个模型。 - • `render()` 和 `getVertexCount()` 需要真实的模型数据,才会触发加载。代理通过 `ensureLoaded()` 方法实现"首次使用时加载"的逻辑。 - • 使用 `mutable` 修饰 `realModel_`,允许在 `const` 方法(如 `getVertexCount() const`)中完成延迟初始化。这是因为延迟加载是一种**实现细节**,不应该改变方法的语义——从调用者角度看,"获取顶点数"显然是一个只读操作。 ### 二、保护代理(Protection Proxy)——访问控制 保护代理用于**控制对真实对象的访问权限**,根据调用者的身份或权限决定是否放行。 与虚拟代理关注"何时创建"不同,保护代理关注的是"**谁有权使用**"。真实对象本身通常不包含权限检查逻辑(符合单一职责原则——业务逻辑和权限逻辑分离),保护代理作为"守门人",在请求到达真实对象之前进行拦截和校验。 保护代理的典型工作流程是: 1. 接收客户端的请求 2. 检查调用者的身份/角色是否有权执行该操作 3. 如果有权限,将请求转发给真实对象;如果没有权限,拒绝请求(抛出异常或返回错误) 4. 可选地记录审计日志(谁在什么时间做了什么操作) 下面的示例展示了一个数据库保护代理:真实数据库对象 `RealDatabase` 负责执行 SQL,代理 `DatabaseProxy` 负责根据用户角色检查权限,同时记录审计日志。 ```cpp #include  #include  #include  #include  #include  // 用户角色 enum class Role { Guest, Developer, DBA, Admin }; std::string roleToString(Role role) {     switch (role) {         case Role::Guest:     return"访客";         case Role::Developer: return"开发者";         case Role::DBA:       return"DBA";         case Role::Admin:     return"管理员";     }     return"未知"; } // 抽象接口:数据库操作 class DatabaseOperations { public:     virtual ~DatabaseOperations() = default;     virtual std::string query(const std::string& sql) = 0;     virtual void insert(const std::string& sql) = 0;     virtual void update(const std::string& sql) = 0;     virtual void deleteRecord(const std::string& sql) = 0;     virtual void dropTable(const std::string& table) = 0; }; // 真实数据库:执行实际操作(无权限检查) class RealDatabase : public DatabaseOperations { public:     std::string query(const std::string& sql) override {         std::cout << "  [DB] 执行查询: " << sql << std::endl;         return"查询结果...";     }     void insert(const std::string& sql) override {         std::cout << "  [DB] 执行插入: " << sql << std::endl;     }     void update(const std::string& sql) override {         std::cout << "  [DB] 执行更新: " << sql << std::endl;     }     void deleteRecord(const std::string& sql) override {         std::cout << "  [DB] 执行删除: " << sql << std::endl;     }     void dropTable(const std::string& table) override {         std::cout << "  [DB] 删除表: " << table << std::endl;     } }; // 保护代理:根据角色控制数据库操作权限 class DatabaseProxy : public DatabaseOperations { public:     DatabaseProxy(std::unique_ptr db,                   const std::string& user, Role role)         : db_(std::move(db)), user_(user), role_(role) {         std::cout << "数据库代理已创建 - 用户: " << user_                   << ", 角色: " << roleToString(role_) << std::endl;     }     std::string query(const std::string& sql) override {         checkPermission("SELECT");         logAccess("SELECT", sql);         return db_->query(sql);     }     void insert(const std::string& sql) override {         checkPermission("INSERT");         logAccess("INSERT", sql);         db_->insert(sql);     }     void update(const std::string& sql) override {         checkPermission("UPDATE");         logAccess("UPDATE", sql);         db_->update(sql);     }     void deleteRecord(const std::string& sql) override {         checkPermission("DELETE");         logAccess("DELETE", sql);         db_->deleteRecord(sql);     }     void dropTable(const std::string& table) override {         checkPermission("DROP");         logAccess("DROP", table);         db_->dropTable(table);     } private:     void checkPermission(const std::string& operation) const {         staticconst std::unordered_map>             permissions = {                 {Role::Guest,     {"SELECT"}},                 {Role::Developer, {"SELECT", "INSERT", "UPDATE"}},                 {Role::DBA,       {"SELECT", "INSERT", "UPDATE", "DELETE"}},                 {Role::Admin,     {"SELECT", "INSERT", "UPDATE", "DELETE", "DROP"}}             };         auto it = permissions.find(role_);         if (it == permissions.end() ||             it->second.find(operation) == it->second.end()) {             throw std::runtime_error(                 "权限拒绝: 用户 [" + user_ + "](" +                 roleToString(role_) + ") 无权执行 " + operation + " 操作"             );         }     }     void logAccess(const std::string& operation,                    const std::string& detail) const {         std::cout << "  [审计日志] 用户=" << user_                   << " 操作=" << operation                   << " 详情=" << detail << std::endl;     }     std::unique_ptr db_;     std::string user_;     Role role_; }; ``` 代码中 `checkPermission()` 使用了一个静态的权限映射表,将每种角色与其允许的操作集合关联起来。这种**基于角色的访问控制(RBAC)**是企业级系统中最常见的权限模型。注意 `RealDatabase` 类完全没有权限逻辑——它只管"执行",权限检查的职责完全由代理承担,这是单一职责原则的体现。 **使用示例**: ```cpp int main() {     auto guestDb = std::make_unique(         std::make_unique(), "张三", Role::Guest     );     auto adminDb = std::make_unique(         std::make_unique(), "李四", Role::Admin     );     // 访客:只能查询     guestDb->query("SELECT * FROM users");    // ✅ 允许     try {         guestDb->insert("INSERT INTO users VALUES(...)");  // ❌ 拒绝     } catch (const std::runtime_error& e) {         std::cout << "  [错误] " << e.what() << std::endl;     }     // 管理员:拥有全部权限     adminDb->query("SELECT * FROM users");      // ✅ 允许     adminDb->dropTable("temp_table");            // ✅ 允许 } ``` 输出中可以看到,同样是调用 `insert()` 方法,访客角色被代理拦截并抛出异常,管理员角色则顺利通过。**客户端代码完全相同,差异完全由代理内部的权限逻辑决定**。 ### 三、缓存代理(Caching Proxy)——避免重复计算 缓存代理用于**缓存耗时操作的结果,避免重复计算或重复请求**。 在实际开发中,很多操作的结果在短时间内不会变化:天气数据几分钟内不会更新、数据库查询结果在数据没修改时保持不变、远程 API 的响应在一定周期内是固定的。如果每次都重新执行这些耗时操作,就是在浪费时间和资源。 缓存代理的工作原理是: 1. 1. 客户端发起请求时,代理先检查缓存中是否已有**未过期**的结果 2. 2. 如果缓存命中(Cache Hit),直接返回缓存的结果,跳过耗时操作 3. 3. 如果缓存未命中(Cache Miss)或已过期,才调用真实对象执行操作,并将结果存入缓存 这个思路和 CPU 的多级缓存、浏览器的 HTTP 缓存如出一辙——**用空间换时间**,在存储开销可接受的前提下,大幅减少重复计算。 下面实现了一个天气服务的缓存代理,带有过期时间和命中率统计: ```cpp #include  #include  #include  #include  #include  #include  // 抽象接口:天气服务 class WeatherService { public:     virtual ~WeatherService() = default;     virtual std::string getWeather(const std::string& city) = 0; }; // 真实对象:从远程 API 获取天气(耗时) class RemoteWeatherService : public WeatherService { public:     std::string getWeather(const std::string& city) override {         std::cout << "  [远程请求] 正在查询 " << city << " 的天气..." << std::endl;         std::this_thread::sleep_for(std::chrono::milliseconds(500));         return city + ": 晴天, 25°C, 湿度 60%";     } }; // 缓存代理:缓存查询结果,设定过期时间 class CachingWeatherProxy : public WeatherService { public:     explicit CachingWeatherProxy(std::unique_ptr service,                                   int cacheSeconds = 60)         : service_(std::move(service)),           cacheDuration_(cacheSeconds) {}     std::string getWeather(const std::string& city) override {         auto now = std::chrono::steady_clock::now();         auto it = cache_.find(city);         if (it != cache_.end()) {             auto elapsed = std::chrono::duration_cast(                 now - it->second.timestamp             ).count();             if (elapsed < cacheDuration_) {                 std::cout << "  [缓存命中] " << city                           << " (缓存剩余 " << (cacheDuration_ - elapsed)                           << " 秒)" << std::endl;                 ++cacheHits_;                 return it->second.data;             }             std::cout << "  [缓存过期] " << city << std::endl;         }         // 缓存未命中,调用真实服务         ++cacheMisses_;         std::string result = service_->getWeather(city);         cache_[city] = {result, now};         return result;     }     void printStats() const {         int total = cacheHits_ + cacheMisses_;         double hitRate = total > 0             ? (static_cast(cacheHits_) / total * 100) : 0;         std::cout << "\n缓存统计: 命中=" << cacheHits_                   << " 未命中=" << cacheMisses_                   << " 命中率=" << hitRate << "%" << std::endl;     } private:     struct CacheEntry {         std::string data;         std::chrono::steady_clock::time_point timestamp;     };     std::unique_ptr service_;     std::unordered_map cache_;     int cacheDuration_;     int cacheHits_ = 0;     int cacheMisses_ = 0; }; ``` 代码中 `CacheEntry` 结构体将查询结果和时间戳绑定在一起。每次缓存命中时,代理会计算缓存条目的"年龄",如果超过 `cacheDuration_`(过期时间),就视为缓存失效,重新从真实服务获取。这种**带过期时间的缓存策略(TTL, Time-To-Live)**是最常见的缓存失效机制。 **使用示例**: ```cpp int main() {     auto weatherService = std::make_unique(         std::make_unique(), 5// 缓存 5 秒     );     // 第一次查询:缓存未命中,访问远程服务     std::cout << weatherService->getWeather("北京") << std::endl;     std::cout << weatherService->getWeather("上海") << std::endl;     // 第二次查询同一城市:缓存命中,不访问远程服务     std::cout << weatherService->getWeather("北京") << std::endl;     std::cout << weatherService->getWeather("上海") << std::endl;     weatherService->printStats();     // 输出: 缓存统计: 命中=2 未命中=2 命中率=50% } ``` 第一次查询北京和上海的天气时各花费 500ms(远程请求),第二次查询直接从缓存返回,耗时接近 0。如果一个页面需要多次显示天气信息,缓存代理可以将 4 次 500ms 的请求缩减为 2 次,节省 50% 的时间。在高并发场景下,这种优化效果更加显著。 ### 四、智能引用代理(Smart Reference Proxy) 智能引用代理的核心思想是:**在对象被引用(访问)时,自动执行一些额外的管理操作**。最典型的例子就是 C++ 的智能指针——它在对象被引用时管理引用计数,在最后一个引用消失时自动释放资源。 与前面三种代理不同,智能引用代理关注的不是"何时创建"、"谁能访问"或"是否有缓存",而是**对对象引用本身的管理**:统计被访问了多少次、有多少个引用者、什么时候应该释放等。 在 C++ 中,这种代理通常通过**重载 `operator->` 和 `operator\*`** 来实现——让代理对象在语法上表现得和指针一样,客户端用 `proxy->method()` 的写法就像在用普通指针,但每次 `->` 的调用都会被代理拦截,执行额外逻辑后再转发到真实对象。 下面我们实现一个自定义的智能引用代理,它在标准 `shared_ptr` 的基础上增加了访问计数功能: ```cpp #include  #include  #include  // 业务对象 class Document { public:     explicit Document(const std::string& name) : name_(name) {         std::cout << "  文档 [" << name_ << "] 已创建" << std::endl;     }     ~Document() {         std::cout << "  文档 [" << name_ << "] 已销毁" << std::endl;     }     void write(const std::string& content) {         content_ += content + "\n";         std::cout << "  写入文档 [" << name_ << "]: " << content << std::endl;     }     void read() const {         std::cout << "  读取文档 [" << name_ << "] 内容:\n" << content_;     }     const std::string& getName() const { return name_; } private:     std::string name_;     std::string content_; }; // 智能引用代理:自动管理生命周期 + 访问计数 + 操作日志 template  class SmartProxy { public:     template      explicit SmartProxy(Args&&... args)         : ptr_(std::make_shared(std::forward(args)...)) {}     // 重载 -> 操作符:访问时自动记录     T* operator->() {         ++accessCount_;         return ptr_.get();     }     const T* operator->() const {         ++accessCount_;         return ptr_.get();     }     T& operator*() { ++accessCount_; return *ptr_; }     int getAccessCount() const { return accessCount_; }     int getRefCount() const { return ptr_.use_count(); }     // 允许拷贝(共享所有权,引用计数递增)     SmartProxy(const SmartProxy&) = default;     SmartProxy& operator=(const SmartProxy&) = default; private:     std::shared_ptr ptr_;     mutableint accessCount_ = 0; }; ``` 代码中 `SmartProxy` 是一个模板类,可以代理任意类型的对象。它内部使用 `shared_ptr` 管理真实对象的生命周期,同时在每次通过 `->` 或 `*` 访问对象时递增 `accessCount_`。注意 `accessCount_` 也被声明为 `mutable`,原因与虚拟代理中相同:统计访问次数是代理的内部行为,不应影响对象在语义上的 `const` 性。 > **`operator->` 的工作原理**:当你写 `proxy->write("hello")` 时,编译器会调用 `proxy.operator->()`,这个方法返回真实对象的指针 `T*`,然后编译器再在这个指针上调用 `.write("hello")`。所以 `operator->` 相当于一个"访问拦截器"——每次用箭头访问成员时都会经过它。 **使用示例**: ```cpp int main() {     SmartProxy doc("设计文档");     doc->write("第一章:代理模式");     doc->write("第二章:观察者模式");     doc->read();     std::cout << "\n文档访问次数: " << doc.getAccessCount() << std::endl;     std::cout << "引用计数: " << doc.getRefCount() << std::endl;     {         SmartProxy doc2 = doc;  // 拷贝:共享同一个 Document         std::cout << "拷贝后引用计数: " << doc.getRefCount() << std::endl;         doc2->write("第三章:策略模式");     }     // doc2 离开作用域,引用计数减 1,但 Document 不会被销毁(doc 还持有引用)     std::cout << "doc2 销毁后引用计数: " << doc.getRefCount() << std::endl; } ``` ## C++ 中的天然代理:智能指针 学完四种代理类型后,你可能会发现:C++ 标准库中**最成功的代理模式实现**,其实你每天都在用——那就是**智能指针**。 `std::shared_ptr` 和 `std::unique_ptr` 本质上就是裸指针的代理。它们通过重载 `operator->` 和 `operator*`,让使用方式和裸指针完全一致(对客户端透明),但在背后默默地做了引用计数管理和自动内存释放(额外控制逻辑)。这正是代理模式"同一接口 + 额外控制"的完美体现。 ```cpp // std::shared_ptr 就是一个代理 // 它在"你以为直接操作对象"时,默默做了引用计数 // 不用代理(手动管理内存): Widget* w = new Widget(); w->doSomething(); delete w;  // 忘记 delete → 内存泄漏! // 使用代理(智能指针): auto w = std::make_shared(); w->doSomething();  // 看起来和裸指针一样 // 自动释放,无需 delete ``` **智能指针体现了代理模式的哪些特征?** | 代理特征 | 智能指针的实现 | | -------------------- | ---------------------------------- | | 与真实对象相同的接口 | 重载 `->` 和 `*`,用法与裸指针一致 | | 额外的控制逻辑 | 引用计数、自动释放、线程安全 | | 对客户端透明 | 使用者几乎感受不到差异 | | 不改变真实对象 | 被管理的对象无需任何修改 | ------ ## 代理模式与其他模式的区别 代理模式在类图结构上与装饰器、适配器、外观都有相似之处,初学者很容易混淆。区分它们的关键不是看代码结构,而是看**设计意图**——你写这段代码是为了解决什么问题? ### 代理 vs 装饰器 这是最容易混淆的一对。两者的类结构几乎完全一样(都持有一个同接口的对象,都实现了同一个接口),但设计意图截然不同: - • **代理**的目的是**控制访问**——决定请求能不能到达真实对象(权限拒绝)、什么时候到达(延迟加载)、是否需要到达(缓存命中直接返回)。代理可能会**阻止**请求到达真实对象。 - • **装饰器**的目的是**增强功能**——在真实对象的行为基础上叠加新的行为(加日志、加压缩、加加密)。装饰器**不会阻止**请求,它只是在前后添加新行为。 | 对比项 | 代理模式 | 装饰器模式 | | ------------ | ----------------------------- | -------------------------- | | **意图** | 控制访问 | 增强功能 | | **关注点** | 谁能访问、何时访问 | 添加什么新行为 | | **对象创建** | 代理通常自己创建/管理真实对象 | 装饰器由外部传入被装饰对象 | | **透明性** | 可以改变行为(拒绝访问) | 保持原行为,添加新行为 | | **典型用途** | 延迟加载、权限检查、缓存 | 日志、压缩、加密 | | **层数** | 通常一层代理 | 经常多层嵌套 | ```cpp // 装饰器:增强功能(可以多层叠加) auto stream = std::make_unique( std::make_unique( std::make_unique("data.txt"))); // 代理:控制访问(通常只有一层) auto db = std::make_unique( std::make_unique(), "user", Role::DBA); ``` ### 代理 vs 适配器 适配器解决的是"**接口不兼容**"问题——已有对象的接口和客户端期望的接口不一致,适配器负责做转换。而代理的接口和真实对象**完全一致**,它不做任何转换,只做控制。 简单记忆:**适配器改变接口,代理不改变接口**。 | 对比项 | 代理模式 | 适配器模式 | | ------------ | ---------------------- | ---------------- | | **接口** | 代理与真实对象接口相同 | 适配器改变接口 | | **目的** | 控制访问 | 解决接口不兼容 | | **真实对象** | 与代理接口一致 | 与适配器接口不同 | ### 代理 vs 外观 外观面对的是**多个子系统**,它将多个复杂接口整合为一个简化的高层接口。代理面对的是**单个对象**,它不简化接口,而是在访问这个对象的过程中加入控制逻辑。 简单记忆:**外观简化多个复杂接口,代理控制单个对象的访问**。 | 对比项 | 代理模式 | 外观模式 | | ------------ | ------------ | ---------------- | | **对象数量** | 代理一个对象 | 封装多个子系统 | | **接口关系** | 接口相同 | 创建新的简化接口 | | **目的** | 控制访问 | 简化使用 | ------ ## 最佳实践与常见陷阱 ### 最佳实践 **1. 明确代理的职责** ``` // ✅ 职责清晰:每个代理只做一件事 class CachingProxy : public Service { // 只负责缓存 }; class LoggingProxy : public Service { // 只负责日志 }; // ❌ 职责混乱:一个代理做太多事 class GodProxy : public Service { // 缓存 + 日志 + 权限 + 限流 + ... // 违反单一职责原则 }; ``` 如果需要多种代理功能,可以组合使用: ```cpp auto service = std::make_unique( std::make_unique( std::make_unique())); ``` **2. 使用智能指针管理真实对象** ```cpp // ✅ 使用 unique_ptr class Proxy : public Subject { std::unique_ptr real_; }; // ❌ 使用裸指针 class Proxy : public Subject { RealSubject* real_; // 谁负责释放? }; ``` **3. 延迟加载时注意线程安全** 前面的虚拟代理示例中,`ensureLoaded()` 方法使用 `if (!realImage_)` 来判断是否需要创建真实对象。在单线程环境下这没有问题,但在**多线程**环境下会出现竞态条件:两个线程可能同时发现 `realImage_` 为空,然后各自创建一个真实对象,导致重复创建和内存泄漏。 > **`std::call_once` 关键字**:C++11 标准库提供的线程安全工具,保证传入的函数**在整个程序生命周期中只执行一次**,即使有多个线程同时调用也不会重复执行。它搭配 `std::once_flag` 使用——`once_flag` 是一个标记,记录函数是否已经执行过。第一个到达的线程执行函数并设置标记,后续到达的线程看到标记已设置就直接跳过。 ```cpp // ✅ 线程安全的延迟加载 class ThreadSafeProxy : public Subject { public: void request() override { std::call_once(initFlag_, [this]() { real_ = std::make_unique(); }); real_->request(); } private: std::unique_ptr real_; std::once_flag initFlag_; }; ``` 这种写法比手动加互斥锁(`std::mutex`)更简洁,且没有锁竞争的性能开销——初始化完成后,后续调用不需要任何同步操作。 **4. 保持接口一致性** ```cpp // ✅ 代理应完整实现抽象接口的所有方法 class ImageProxy : public Image { void display() override { /* ... */ } int getWidth() const override { /* ... */ } int getHeight() const override { /* ... */ } }; // ❌ 遗漏某些方法或改变语义 ``` ### 常见陷阱 **陷阱 1:代理链过长导致性能问题** 虽然代理可以像装饰器一样嵌套组合,但每多一层代理就多一层虚函数调用的开销。在高频调用路径上(比如每秒调用数万次的核心接口),4 层代理意味着 4 次额外的虚函数间接调用。建议将代理层数控制在 2-3 层以内,如果确实需要多种功能,可以考虑将相关功能合并到一个代理中。 ``` // ❌ 多层代理嵌套可能带来显著开销 auto service = std::make_unique( std::make_unique( std::make_unique( std::make_unique( std::make_unique())))); // 每次调用经过 4 层转发 // ✅ 合理控制层数,或将多个功能合并到一个代理中 ``` **陷阱 2:虚拟代理的 const 方法问题** ``` class Proxy : public Subject { public: // 问题:const 方法中无法修改 realSubject_ int getValue() const override { // real_ = make_unique<...>(); // 编译错误!const 方法不能修改成员 } private: // ✅ 解决方案:使用 mutable mutable std::unique_ptr real_; }; ``` **陷阱 3:循环依赖** 代理模式的一个基本原则是:**真实对象不应该知道代理的存在**。代理是真实对象的"附加层",真实对象应该能独立工作。如果真实对象反向引用了代理,就会形成循环依赖,导致设计耦合度升高、难以测试和维护。 ``` // ❌ 代理和真实对象互相引用 class RealSubject { Proxy* proxy_; // 反向引用代理——违反设计原则 }; class Proxy { RealSubject* real_; // 引用真实对象 }; ``` ------ ## 写在最后 代理模式是结构型模式中最灵活、最实用的模式之一。它的核心价值在于:**在不修改真实对象的前提下,通过一个"中间人"来控制和增强对象的访问**。 在 C++ 中,代理模式有着天然的应用场景: - • **智能指针**就是代理的经典实现(`shared_ptr`、`unique_ptr`) - • **虚拟代理**解决了大对象延迟加载的性能问题 - • **保护代理**为系统提供了透明的安全层 **记住这两句话**: > **代理模式的核心不是增加功能,而是控制访问。** > **好的代理对客户端完全透明——使用代理和直接使用真实对象没有区别。**