
C里的代理模式远比教科书里的三件套接口、真实类、代理类要丰富。我在整理项目代码的时候发现同一个“代理”思想在不同场景下长出了完全不同的样子——有管网络请求的有管权限的有管对象生命周期的甚至还有在编译期就把代理逻辑消化掉的。这篇文章就聊聊我见过的、也亲自写过的七种代理模式变体以及它们的适用场景和坑。如果你是准备C面试的或是正在做服务端、客户端、嵌入式方向但每次遇到“这个对象要不要包一层”都举棋不定的开发者这篇文章应该比较对胃口。我会先从最朴素的经典变体讲起再进入现代C里那些真正让代理模式“活”起来的写法最后放一个能直接跑起来的工程示例把缓存、鉴权、日志全叠进同一个代理里。1. 代理模式到底在解决什么问题先搞清楚为什么会有这么多变体1.1 代理的本质是控制访问而不只是转调代理模式这个词很多人在学设计模式的时候都背过一句话为其他对象提供一种代理以控制对这个对象的访问。但“控制访问”这四个字落到实际代码里含义其实非常宽泛。你可以控制“什么时候访问”——比如对象创建成本很高那就等真正用到了再创建这就是虚代理你可以控制“谁能访问”——比如必须登录后才能调用某个服务这是保护代理你还可以控制“怎么访问”——比如把本地调用伪装成远程调用或者把远程调用包装成和本地调用一模一样的形式这是远程代理。说白了代理模式就是在一个调用者和一个被调用者之间塞了一层“中间人”。中间人做得好调用者甚至感觉不到它的存在做得不好就会变成一层画蛇添足的胶水。我在很多项目里看到过有人强行套代理结果唯一的“代理逻辑”就是转发连一点额外控制都没有这种代理除了让调用栈变深以外没有任何价值属于典型的过度设计。1.2 为什么C里的代理模式特别容易长“变体”这个问题的答案需要回到C这个语言本身。C不是纯粹的面向对象语言它同时支持值语义、泛型编程、函数式风格、RAII资源管理这些特性叠加在一起让“代理”这个词的外延变得非常大。比如GoF时代的代理模式描述的是以虚函数为核心的多态代理一个接口、一个真实类、一个代理类三个类通过继承和虚函数构成关系。这套写法在Java里是绝对的主流但在C里你完全可以用模板在编译期生成一个代理连虚函数表都不用运行时零开销你也可以用std::function包一层把任意可调用对象变成代理你甚至可以用shared_ptr的定制删除器做“引用计数代理”让每个对象都自带一个隐形看守员。所以我的理解是C代理模式的“变体”不是设计模式本身变复杂了而是C的表达能力太强同一个控制访问的思想在不同场景里会自然选择最合适的那套实现机制。你在C里写代理本质上是在做一道“选择题”用什么机制实现间接层虚函数、模板、函数包装器还是指针选对了代码干净利落选错了就是一堆无处安放的虚函数和结构体。2. 经典代理变体逐个拆解从远程代理到智能引用2.1 远程代理把网络问题藏在接口后面远程代理是代理模式最早被大规模应用的场景之一目前几乎所有RPC框架都在做这件事。它的核心价值是调用方不需要知道自己调的是一个本地对象还是一个远端服务代理负责把方法调用参数打包序列化、发到网络、等待结果、再返回给调用方。实际工程里的远程代理要比教科书复杂太多。我在一个分布式系统项目里写过内部服务之间的调用封装最头疼的问题不是序列化和网络传输而是异常语义。本地方法调用抛异常调用方直接在栈上捕获但远程调用是跨进程的服务端抛出的异常经过网络传输客户端如何还原成“看起来像本地异常”的东西这里就需要在代理层定义好自己的错误码或者统一的异常包装否则调用方根本不知道是网络断了、服务崩了还是参数不合法。远程代理还有一个容易踩的坑超时处理。本地调用如果不能及时返回最多就是卡住线程远程调用卡住会连带把线程池打满最终拖垮整个服务。所以我后来在远程代理里都会强制加超时和重试策略并且把“是否可重试”作为接口设计的一部分。不是所有远程方法都能安全地重试比如扣款操作盲目重试会造成重复扣款这种接口在代理层就得做成“幂等模式”或者干脆不重试。2.2 虚代理大对象与懒加载的艺术虚代理是C里最“原汁原味”的代理变体它解决的核心问题是创建一个对象很贵但可能整个程序跑完都不会用到它那就在真正需要访问它的时候再创建。最常见的例子是图片加载器、大型文档、游戏里的场景资源。写虚代理的时候有一个细节非常关键真实对象用什么类型持有。我用过裸指针也在老项目里见过用C语言风格malloc的效果都不好。裸指针的主要问题是异常安全和生命周期不明确如果在代理的析构函数里忘记delete内存泄漏就悄无声息地发生了。后来我统一改成unique_ptr代理对象被销毁时真实对象自动被释放代码量少了安全性反而高了。class Image { public: virtual ~Image() default; virtual void display() 0; }; class RealImage : public Image { public: explicit RealImage(std::string file) : m_file(std::move(file)) { loadFromDisk(); } void display() override { std::cout Displaying m_file std::endl; } private: void loadFromDisk() { // 模拟从磁盘加载图片这里可能是耗时操作 std::cout Loading m_file from disk... std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(200)); } std::string m_file; }; class ImageProxy : public Image { public: explicit ImageProxy(std::string file) : m_file(std::move(file)) {} void display() override { // 第一次调用时才真正加载 if (!m_realImage) { m_realImage std::make_uniqueRealImage(m_file); } m_realImage-display(); } private: std::string m_file; std::unique_ptrRealImage m_realImage; };虚代理有一个容易被忽略的优点它可以帮你实现“创建失败也不影响主流程”。比如某个应用程序启动时要初始化插件列表但其中一个插件资源加载失败如果直接在启动流程里new整个程序可能崩用虚代理后只有真正打开那个功能时才会初始化插件失败时只需要弹个提示不影响其他功能。这种东西在做嵌入式GUI的时候非常实用。2.3 保护代理权限控制该放在哪一层保护代理的核心职责是判断调用者是否有权限执行某个操作。听起来简单但在真实的C项目里权限控制放错位置是一件非常麻烦的事。我见过一种很常见的坏味道开发者在每个业务函数内部手动检查权限比如先调用一个checkPermission()函数通过再执行后续逻辑。一开始还好但随着接口越来越多每个函数入口都复制粘贴一段权限检验代码既不美观也容易漏。更麻烦的是权限逻辑越改越复杂动不动就需要全局搜索修改。用保护代理权限逻辑可以收敛到一个类里代理类和真实类实现同一个接口代理类先做权限校验校验通过后才调用真实类的方法。这样业务类自己完全不感知权限系统的存在后续调整权限规则时也只需要改代理类这一处。不过要提醒一句保护代理不能替代系统底层的安全机制。如果你的真实类是一个可以绕过代理直接被调用的对象比如通过友元或者强制类型转换拿到裸指针那代理就形同虚设。所以保护代理比较适合用在“所有外部调用都必须经过统一入口”的场景比如模块间的服务调用接口、插件扩展点等。2.4 智能引用代理当代理成为对象的“看门人”如果把代理模式的定义放宽一点现代C里的shared_ptr本身就是一种极其精妙的代理变体。它代理的是“原始指针”这个对象控制了指针的复制、赋值和销毁行为并且在引用计数归零时自动释放资源。这种思想能够延伸出很多玩法。比如定制删除器就是很典型的例子shared_ptr不一定要调用delete来销毁对象你完全可以传入一个自定义的删除函数在删除前执行日志记录、连接关闭、文件落盘等操作。这个控制在很多框架里被用来做“异步回收”比如把对象的销毁推迟到某个事件循环的特定时机避免在信号处理函数里执行复杂清理。更贴近代理思想的做法是用一个包装类来“看守”底层对象。我在一个日志系统里写过SmartLogProxy它内部持有底层Logger但对外暴露的接口会额外加一层每条日志在写入前自动附带调用者所在的文件行号。这个代理不用继承任何接口它只需要提供和Logger相似的函数签名调用方把代理当成普通Logger使用就行。这类智能引用代理很适合做切面式增强不修改原有类也不要求原有类继承任何基类。3. 现代C带来的新变体模板代理、函数代理与类型擦除3.1 模板化静态代理把代理逻辑在编译期消化掉前面提到的经典代理都是运行时多态也就是通过虚函数表在运行期找到真实对象的实现。虚函数本身开销很低但在某些极致性能场景下程序员就是不愿意为多态付出哪怕一次间接跳转的代价。这时候模板就可以派上用场。C模板代理的核心思路是真实对象的类型是一个模板参数代理和真实对象之间的“接口”不是通过基类约定的而是通过“模拟鸭子类型”来约定的——只要真实对象有代理需要调用的那个成员函数代理代码就能编译通过。这样做的好处是零虚函数开销、零动态分配编译器可能直接内联所有调用。我之前在写一个数值计算库时就用过这种变体。当时需要对矩阵运算做一些边界检查但库里很多核心算法对性能非常敏感不能接受每次运算都走虚函数。最后的方案是一个模板代理类名就叫CheckedMatrixProxy模板参数是底层的矩阵类型它实现operator()和at()但先检查索引范围越界时抛异常。因为所有的检查逻辑都发生在编译期的模板展开里实际运行时代码和手写检查版几乎一样快。这种模板代理还有一个额外的好处接口约束可以非常灵活。运行期接口要求所有类都继承同一个基类这本身就是一种侵入式设计而模板代理只要求类型满足“最小接口”逻辑上更符合现代C的“不要为不用功能付费”理念。3.2 基于std::function的轻量代理函数级别的控制面很多场景下我们不需要代理一个对象只需要代理一个“动作”。比如一个耗时的计算函数想给它加缓存或者一个可能失败的网络请求想给它加重试。如果每次都包一个完整的类杀鸡用牛刀代码量也不友好。std::function非常适合这种函数级代理因为任何可调用对象都可以被转换并存储进去。你可以把一个普通函数、lambda表达式、函数对象统一收编成一个std::function然后在外面包上缓存或者日志逻辑。std::functionint(int) cachedCompute(int (*rawFunc)(int)) { auto cache std::make_sharedstd::unordered_mapint, int(); return [rawFunc, cache](int x) - int { auto it cache-find(x); if (it ! cache-end()) { std::cout cache hit for x std::endl; return it-second; } int result rawFunc(x); (*cache)[x] result; return result; }; }这个例子里lambda捕获了原始函数指针和缓存表对外表现就是一个“带缓存的函数”。每次调用前先查缓存没有命中才真正执行原始计算。这种方式做重试也很顺手包装一个retry函数内部捕获原始可调用对象循环执行并检查返回值或者捕获异常直到成功或者达到最大次数。std::function代理最大的优点是灵活缺点是隐藏了具体类型在某些极端情况下会有额外开销。我个人的经验是不要用它包“每秒调用百万次”的核心循环但用来包一些业务逻辑、网络请求、IO操作完全没问题省下的代码结构复杂度远超性能损耗。3.3 类型擦除与代理的合流shared_ptr、function、any 背后都是“隐形的代理”类型擦除这个概念近几年在C社区里越来越被频繁提及。它的核心目标是把具体类型藏起来只暴露一组行为。你可以把std::function理解成“可调用对象的类型擦除”std::shared_ptr理解成“指针生命周期管理的类型擦除”std::any理解成“任意值的类型擦除”。这种“隐藏类型、保留行为”的思路和代理模式高度同源。代理模式关注的是“在不改变接口的前提下控制访问”类型擦除关注的是“让不同的类型可以通过统一的接口被使用”。两者结合会催生出一类很有意思的设计你定义了一个抽象接口但在它的某个实现里不直接持有具体对象而是持有另一个std::function或者std::any运行到某个时机才解开并调用。我在一个插件系统里就见过这种方式。主程序定义了一个Plugin接口但每个插件内部的具体逻辑并不在编译期可见而是在运行时从配置文件里解析插件路径再通过工厂函数生成一个std::function包装的调用句柄。这个句柄本质上就是一个“函数代理”主程序完全不知道插件内部是怎么实现的但它可以通过统一的接口调用插件。类型擦除和代理模式的合流让“接口”不再局限于虚函数表而是变成了一种更灵活的概念。这也是为什么现在C面试里很爱问“shared_ptr的实现原理”“std::function是如何通过SBO优化性能的”——这些问题背后考察的其实是同一种能力你能不能理解类型信息被隐藏之后控制权和生命周期该如何管理。4. 实战做一个可复用的图片加载代理4.1 场景设计与接口定义很多项目里都会遇到一个典型的代理应用场景加载网络图片。直接调用网络请求的代码散落在业务各处后面想加缓存、加鉴权、加日志就得挨个改调用点。这里我设计了一个最简单的接口和三个代理实现把一个真实项目的骨架完整跑通。需求如下接口是ImageLoader只有一个fetch方法传入图片URL和一个回调函数回调在加载完成后被调用。RemoteImageLoader是真正发送网络请求的类这里用线程模拟耗时操作。CachedImageLoaderProxy是缓存代理重复加载同一个URL时直接返回缓存数据。AuthImageLoaderProxy是保护代理在加载前检查当前会话是否有效。这样的设计完全符合代理模式的经典结构每个代理只做一件事可以单独使用也可以组合使用。4.2 基础实现真实加载器与缓存代理先写接口和真实加载器#include iostream #include memory #include string #include unordered_map #include vector #include functional #include thread #include chrono class ImageLoader { public: virtual ~ImageLoader() default; virtual void fetch(const std::string url, std::functionvoid(const std::vectorchar) callback) 0; }; class RemoteImageLoader : public ImageLoader { public: void fetch(const std::string url, std::functionvoid(const std::vectorchar) callback) override { // 模拟异步网络请求新开线程耗时100ms后回调 std::thread t([url, callback]() { std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::vectorchar data(url.begin(), url.end()); callback(data); }); t.detach(); } };这段代码里的RemoteImageLoader非常简陋但足以表达远程代理的核心思想调用方传入URL和回调fetch方法内部发起异步任务完成后通知调用方。真实项目里这里会换成HTTP客户端或者消息队列但接口形状几乎一样。接着是缓存代理class CachedImageLoaderProxy : public ImageLoader { public: explicit CachedImageLoaderProxy(std::shared_ptrImageLoader target) : m_target(std::move(target)) {} void fetch(const std::string url, std::functionvoid(const std::vectorchar) callback) override { auto it m_cache.find(url); if (it ! m_cache.end()) { std::cout [Cache] hit: url std::endl; callback(it-second); return; } std::cout [Cache] miss: url std::endl; m_target-fetch(url, [this, url, callback](const std::vectorchar data) { m_cache[url] data; callback(data); }); } private: std::shared_ptrImageLoader m_target; std::unordered_mapstd::string, std::vectorchar m_cache; };这里有一个特别重要的细节缓存代理持有底层加载器用的是shared_ptr而不是裸指针。原因是当多个代理组合时同一目标对象可能被多个代理共同持有如果某个代理被提前析构其他代理手里的裸指针就会悬垂。shread_ptr让所有代理共享所有权生命周期安全。后面第6节还会继续展开这个话题。4.3 再加一层保护代理权限检查和日志能力保护代理在调用真实加载器之前做检查。真实项目里的权限判断通常会查会话token、用户角色等信息这里简化成一个bool标志逻辑完全一样class AuthImageLoaderProxy : public ImageLoader { public: AuthImageLoaderProxy(std::shared_ptrImageLoader target, bool sessionValid) : m_target(std::move(target)), m_sessionValid(sessionValid) {} void fetch(const std::string url, std::functionvoid(const std::vectorchar) callback) override { if (!m_sessionValid) { std::cout [Auth] rejected: session invalid, url url std::endl; callback({}); return; } std::cout [Auth] allowed: url url std::endl; m_target-fetch(url, std::move(callback)); } private: std::shared_ptrImageLoader m_target; bool m_sessionValid; };注意这里m_sessionValid是引用。这说明代理不一定非要“持有”状态也可以只“引用”外部状态每次调用时实时检查。这样可以避免代理内部维护一份可能过期的权限副本减少状态同步问题。然后在main函数里组合使用int main() { // 注意这里必须保证会话有效 bool sessionValid true; auto remote std::make_sharedRemoteImageLoader(); auto cached std::make_sharedCachedImageLoaderProxy(remote); auto auth std::make_sharedAuthImageLoaderProxy(cached, sessionValid); std::cout First load std::endl; auth-fetch(https://example.com/a.png, [](const std::vectorchar data) { std::cout callback got data.size() bytes std::endl; }); std::this_thread::sleep_for(std::chrono::milliseconds(200)); std::cout Second load (should be cache hit) std::endl; auth-fetch(https://example.com/a.png, [](const std::vectorchar data) { std::cout callback got data.size() bytes std::endl; }); std::this_thread::sleep_for(std::chrono::milliseconds(200)); std::cout Load with invalid session std::endl; sessionValid false; auth-fetch(https://example.com/b.png, [](const std::vectorchar data) { std::cout callback got data.size() bytes std::endl; }); std::this_thread::sleep_for(std::chrono::milliseconds(200)); return 0; }编译的时候我用的是VS Code g命令非常简单g -stdc17 -Wall -Wextra proxy_image.cpp -o proxy_image如果是在Windows上用MSVC的开发者注意部署时带上Visual C Redistributable运行时库否则目标机器上会报缺少动态库的错误。这个坑很多新人会踩我提一句是为了让你少绕弯。运行结果会清晰展示三件事第一次加载是cache miss第二次加载是cache hitsession失效后所有请求直接被拒绝。三个代理形成了职责明确的流水线保护代理先拦权限缓存代理再查缓存最后才轮到远程加载器真正干活。4.4 这个示例暴露出的一个工程隐患并发回调前面这段代码能跑但有一个隐患在真实项目中几乎必然爆发多个线程同时调用fetch缓存表unordered_map会被并发读写产生数据竞争。真实场景里图片加载会发生在滚动列表、多个UI组件等不同线程。第一次加载同一个URL时如果两个线程同时发现缓存未命中它们会同时发起两个网络请求并且同时写入m_cache轻则重复请求浪费流量重则程序崩溃。解决方案是给缓存代理加锁。比较推荐的做法是使用std::shared_mutex因为大多数场景是“多读少写”大量线程同时读取缓存只有缓存未命中时才会写入。读锁用std::shared_lock写锁用std::unique_lock效率比单一互斥锁好很多。注意加锁的粒度要尽量小拿到缓存数据后尽快释放锁不要在持锁状态下调用回调函数否则可能出现回调函数反过来请求同一个代理造成死锁。5. 别再把代理模式和这些兄弟模式搞混了5.1 代理 vs 装饰器一个是控制一个是增强这是面试中最高频的设计模式辨析题。代理模式和装饰器模式的类结构非常像——都持有一个目标对象都通过相同接口转发调用但意图截然不同。代理模式的核心是“控制访问”我替目标对象把关决定这个请求到底进不进去。虚代理决定“什么时候创建”保护代理决定“谁来访问”远程代理决定“如何访问”。目的是节省资源、加强安全、隐藏复杂性。装饰器模式的核心是“增强功能”我不决定请求能不能进去我只会让请求通过时多做一点事情比如加压缩、加加密、加日志。装饰器可以层层嵌套每一层都乐于让数据通过并且把自己那一层的能力附加上去。一句话总结代理是门卫装饰器是美容师。门卫可以不让某个人进公司美容师只能帮你化妆但不会把你拦在门外。这个区别如果理解到位写出来的代码意图会非常清晰。5.2 代理 vs 适配器 vs 外观接口关系决定了本质代理模式保持的接口和目标对象的接口完全一致调用方感知不到代理的存在。这是它和适配器最大的区别适配器必须转换接口把A接口转换为B接口让原本不兼容的类可以一起工作。外观模式和代理的区别在于作用范围外观模式通常隐藏整个子系统提供一套简化的门面接口里面可能有多个类协同工作代理模式通常只针对单个对象的单个接口做控制虽然可以组合多个代理但每个代理的粒度都很小。我做了一张表方便你速查模式核心意图接口关系典型场景代理模式控制访问与目标对象接口一致懒加载、权限、远程调用、缓存装饰器模式增强功能与目标对象接口一致加日志、压缩、加密、附加职责适配器模式转换接口接口不兼容需要转换接入第三方SDK、老接口兼容外观模式简化门面提供子系统的简化接口封装复杂流程、统一入口理解这四种模式关键不在于背定义而在于看接口和意图。接口一样不一定就是代理还得看它的目的是控制还是增强接口不一样但设计意图是“复用”多半是适配器接口变少但功能更聚合则是外观。6. 真实项目里踩过的坑多线程、生命周期与栈空间6.1 代理类里的线程安全锁的颗粒度非常讲究我在第4节已经提到过缓存代理遇到并发读写时必须加锁。但加锁不是万能的加不好还会引发新问题。第一种问题是持锁时间过长。如果在持锁的条件下调用真实对象的网络请求那么这个请求可能花费几百毫秒甚至几秒期间其他线程全部被阻塞系统的并发能力瞬间变成串行。正确做法是检查缓存、取出数据后立刻释放锁真实的网络请求必须在锁外面执行。第二种问题是代理层锁和业务层锁互相嵌套造成死锁。假设业务代码里先持有一把业务锁然后调用代理的fetch方法代理内部又尝试获取缓存锁而另一个线程已经持有缓存锁正在等待业务锁——经典死锁场景。排查这种问题非常费时我的建议是代理层尽量不要在锁的保护下回调调用方的任何代码包括回调函数和虚函数回调极有可能会反向进入这个代理形成循环等待。6.2 shared_ptr循环引用与悬垂问题最典型的C代理坑代理类持有真实对象用了shared_ptr看起来万事大吉但真实对象如果反过来持有代理的shared_ptr循环引用就出现了。二者互相引用引用计数永远减不到0内存永远不会释放。举一个具体例子一个Service对象会被多个客户端共享它内部出于某种原因又保存了那个“缓存代理”的shared_ptr想主动刷新缓存。当缓存代理和Service互相握有shared_ptr程序退出时内存泄漏。解决方案有两个。一个是把其中一边改成weak_ptr通常是让被代理的真实对象持有代理的弱引用需要时通过lock()提升为shared_ptr。另一个方案是从架构上禁止反向持有让真实对象不感知代理的存在。第二个方案更干净因为代理的核心意义就是“对调用方透明”真实对象更不应该知道代理的存在。还有一个小细节在类成员函数内部需要获取当前对象的shared_ptr时不要直接在构造函数里调用shared_from_this那时引用计数还没建立调用会抛std::bad_weak_ptr异常。必须先让对象从enable_shared_from_this派生并且对象的生命周期必须已经由shared_ptr接管。6.3 栈空间与调用深度的关系懒加载代理也可能成为栈溢出元凶话题引到栈空间是因为我在一个嵌入式项目里踩过一个大坑。当时的UI系统用了虚代理加载复杂界面组件组件内部初始化时又调用了另一个虚代理嵌套几层之后某个页面的加载调用链变得非常深。虽然每一层的栈消耗不算大但嵌入式环境默认栈空间可能只有几十KB叠加了几十帧调用后直接栈溢出程序复位查了半天才定位到是加载链路太长。代理模式的间接层天然会加深调用栈这是无法回避的代价。如果你的运行环境栈空间很小或者代理链条很长就要提前做评估。两种常用的解决思路一是把深嵌套的加载流程改成显式状态机不依赖递归回调二是把大对象的加载和初始化放到独立线程用消息队列把结果送回来避免多层同步调用同时占用栈空间。这类问题在PC端开发里往往被忽略因为默认的栈空间按MB算。但在嵌入式、游戏主机、机车载系统上栈空间是稀缺资源。写代理模式时脑子里要有一根弦每一层代理的调用都是浮在栈上的一次函数调用别让“间接层”变成“压死栈的最后一根稻草”。7. 代理模式变体选型速查我的经验总结最后整理一份选型口诀这基本是我实践下来最简练的版本要隐藏网络通信用远程代理接口保持本地调用形状。要延迟创建大对象用虚代理持有者选unique_ptr。要在调用前做权限校验用保护代理权限检查收敛到一个类。要接管对象生命周期用智能引用代理定制shared_ptr删除器最省事。要给某个函数加缓存、重试、耗时统计用std::function包一层比写类更轻。要极致性能且调用者是模板化代码用模板代理零虚函数表开销。要让插件或配置驱动的调用更灵活用类型擦除代理把具体类型藏起来。我最近在一个服务端项目里就因为“提前把代理层想清楚”省了不少事。一开始只是给缓存模块加日志后来发现要加鉴权再过两个迭代又要加负载均衡最后全都在代理层叠加完成业务代码一行没改。这种“一块砖一块砖往上垒”的爽感大概就是设计模式真正的价值。最后再分享一个小技巧每次写完代理你都要问自己一句如果有一天把代理类完全删掉调用方代码需要改吗如果需要改说明代理的接口设计有问题如果完全不用改说明这个代理真正做到了“透明控制”。透明的代理才是好代理。