尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

C++适配器模式实战:类继承、对象组合、模板与函数适配器的工程抉择

C++适配器模式实战:类继承、对象组合、模板与函数适配器的工程抉择 最近在重构一套线上服务时碰到一个特别典型的场面底层换了新的数据访问组件接口形态和调用方式跟老组件完全对不上而上层几十个业务模块都是按照老接口写的。动上层工作量巨大且容易出回归改底层又怕带出隐藏的兼容问题。最后我选择在中间加了一层适配把这套新旧接口之间的形状差异全部消化掉。这就是本文要聊的 C 适配器模式以及它在真实工程中演化出的各种变体。适配器模式的经典定义是“将一个类的接口转换成客户期望的另一个接口”但 C 因为有多继承、模板、运算符重载、RAII 这么多额外工具适配器的实现方式远比教科书里那一两种写法丰富。不同变体在耦合度、运行时开销、扩展性、代码可读性上的取舍完全不同。我把自己在实际项目中用过的几种变体、踩过的坑、判断取舍的依据整理出来给准备搞接口兼容层、对接第三方库、或者想让老代码吃上新组件的朋友做个参考。1. 从最早的多继承适配器说起当继承不再是唯一答案在 C 里第一次接触适配器模式的人大多会先看到用“公开继承两个类”实现适配器的写法。Target 是客户期望的接口Adaptee 是你手上那个接口不匹配的类型适配器同时继承两者override 需要的 Target 虚函数内部把调用转发给 Adaptee 的成员函数。1.1 类适配器的典型代码长什么样class ITarget { public: virtual ~ITarget() default; virtual void ProcessRequest(int id) 0; }; class LegacyService { public: void Handle(const std::string serialized, int priority); }; class LegacyServiceAdapter : public ITarget, public LegacyService { public: void ProcessRequest(int id) override { std::string payload BuildSerializedPayload(id); Handle(payload, 0); } };这种写法的好处是直观。Adaptee 的公开成员函数会被继承进适配器编译期就能完成绑定没有虚函数转发带来的额外调用开销对象内存布局也比较紧凑。在早期组件化思想不成熟的年代这种写法是很多框架对接老模块的标准姿势。1.2 为什么我现在不推荐用它作为默认方案实际工程里用多继承适配器麻烦会逐渐露头。第一个问题是类型关系被污染。适配器既是一个 ITarget也是一个 LegacyService编译器允许你把适配器指针当 LegacyService 指针传给任何接受老类型的函数。短期内这很方便但后续如果有人直接越过 ITarget 接口去调 LegacyService 里的老函数适配层的约束就形同虚设接口兼容的目标被绕过去了。第二个问题是多继承的构造与初始化顺序。如果 Adaptee 和 Target 各自有复杂的初始化依赖适配器的构造顺序是基类声明顺序决定的很容易构造中途抛出异常排查起来需要同时看多个基类的构造逻辑。第三个问题更隐蔽当 Adaptee 是一个有着大量公开接口的庞大类型时适配器对外暴露的方法面会异常大客户代码可以看到一堆不该看到的东西。我在某个老项目里就见过一个适配器类同时继承了三个业务基类最后这个类的接口面膨胀到几十个方法团队里没人说得清哪些方法是被适配层真正使用的。后面重构时所有人一致同意回到组合的方式——所以如果你的系统里没有特别强的“适配器需要伪装成老类型”的需求我更建议用下面这种对象组合适配器。2. 对象组合适配器实际项目里更常驻的形态组合适配器的思路很简单适配器内部持有一个 Adaptee 对象或指针、引用在实现 Target 接口时手动完成函数调用转换。这是 Java、C# 里最主流的适配器写法在 C 里也是我在业务代码中默认首选的一种方案。2.1 组合适配器的基本骨架与工厂函数class LegacyServiceAdapter : public ITarget { public: explicit LegacyServiceAdapter(std::shared_ptrLegacyService service) : service_(std::move(service)) {} void ProcessRequest(int id) override { std::string payload BuildSerializedPayload(id); service_-Handle(payload, 0); } private: std::shared_ptrLegacyService service_; }; std::unique_ptrITarget CreateTargetFromLegacy( std::shared_ptrLegacyService service) { return std::make_uniqueLegacyServiceAdapter(std::move(service)); }我习惯在组合适配器里传 shared_ptr 而不是裸指针或引用原因在于使用方的生命周期管理。适配器层往往是在系统启动阶段创建、在运行期间被多方持有的如果直接传裸指针调用方和适配器之间就会产生“谁拥有指针”的模糊感。shared_ptr 把所有权语义明确下来适配器对象可以在某个环节被替换而底层服务对象存活时间不受这个替换动作影响。2.2 处理 Adaptee 是值类型时的性能与生命周期问题val 类型的 Adaptee 在某些时候没法用指针——比如第三方库只暴露了具体的值对象。这时候有两种处理方式。一种是直接在适配器内部持有一个 Adaptee 成员对象构造时拷贝进去另一种是保存 const 引用。前者的问题是拷贝成本如果 Adaptee 内含大量缓冲区或者句柄资源一次适配器的拷贝构造会拖慢初始化后者的问题是引用必须指向活得比适配器更久的对象。我实际踩过的一个坑把临时对象 bind 到适配器的成员引用上然后适配器在函数返回后继续被使用临时对象已经析构所有成员访问都是未定义行为。解决的时候我用了一个笨办法——禁止适配器在构造时接收临时对象。做法是对成员引用用 const但构造函数参数改用 unique_ptrclass ConstRefAdapter : public ITarget { public: explicit ConstRefAdapter(std::unique_ptrLegacyService owned) : owned_(std::move(owned)), ref_(*owned_) {} private: std::unique_ptrLegacyService owned_; const LegacyService ref_; };这样适配器持有所有权同时内部引用始终指向自己拥有且不会提前析构的对象。如果需要修改 Adaptee 状态把 ref_ 换成非 const 引用同样有效。2.3 接口方法太多时适配器怎么设计才不臃肿如果 Adaptee 包含大量方法而调用方只用了其中一小部分组合适配器会面临代码膨胀。可以考虑引入“接口分面”思路——适配器只实现客户端真正需要的窄接口剩余能力通过提供适配器本身的查询方法暴露出来让更高级别的模块去访问。这种做法本质上是“适配器优先暴露面窄、底层能力按需透传”的原则。class LegacyGateway { public: void PushEvent(const std::string topic, const std::string data); void PullEvents(const std::string topic, EventHandler handler); void RegisterProtocol(const std::string name, ProtocolConfig config); // 可能有几十个额外方法 }; class EventGatewayAdapter : public IEventSource { public: void Push(const std::string topic, const std::string data) override { gateway_-PushEvent(topic, data); } void Subscribe(const std::string topic, EventHandler handler) override { gateway_-PullEvents(topic, handler); } };这是我在某个采集网关项目里的真实做法底层网关组件有三十多个方法但上层业务只关心“发事件”和“订阅事件”适配器就只实现这两个能力。其余方法通过一个 GetUnderlying 函数以网关对象指针的方式提供给真正需要的模块避免所有逻辑挤进一个巨大的适配器类。这个设计让新增适配器类型的成本大幅下降。3. 模板适配器C 独有的接口统一利器组合适配器解决的是“一个特定 Adaptee 与特定 Target 之间的形状匹配”但如果你的场景是有几十个不同 Adaptee都要适配到同一套 Target 接口一个个写适配器类就太低效了。C 的模板机制让适配器可以做到“一次书写批量适用”这是 Java、C# 里很难达到的高度。3.1 标准库里的模板适配器典范stack 与 queue 的底层容器切换C 标准库里的 std::stack 和 std::queue 就是模板适配器最经典的实现。stack 对外只允许通过 push/pop/top 操作但底层实际存元素的容器可以在实例化时指定——默认是 std::deque也可以换成 std::vector 或 std::list。底层容器拥有远超过 stack 接口的方法但 stack 故意只暴露后进先出的操作语义。这个设计的本质就是把“容器存储能力”这一 Adaptee适配成“栈语义”这一 Target。而且因为用的是模板不需要为每种容器写专门的适配器类编译器在实例化时自动完成兼容性检查。你在实际代码里可以直接这么用// 底层容器的适配模板适配器的标准库范例 std::stackint, std::vectorint stackWithVector; // 底层用 vector仍只有栈语义 std::queueint, std::listint queueWithList; // 底层用 list标准库这么设计是为了让 stack 不要被绑死在 deque 上——有些场景 vector 的缓存局部性更好有些场景 list 的中间插入更合适。调用方不关心底层容器的具体形态只关心栈和队列的语义。这种适配器几乎零运行时开销所有转换都在编译期完成。3.2 用模板写一个容器接口适配器受 stack 的启发我在给几个历史遗留容器类型做统一访问接口时也用了模板适配器。假设三个老的容器类分别叫 OldVector、OldLinkedList、OldSortedList它们内部结构完全不同但都需要能走同一个 RangeReader 接口去读数据。接口定义是这样的template typename T class RangeReader { public: virtual ~RangeReader() default; virtual size_t Size() const 0; virtual T At(size_t index) const 0; };如果按对象适配器的思路要给每个容器写一个适配器类。但用模板适配器只需要一套逻辑template typename Container class ContainerRangeAdapter : public RangeReadertypename Container::value_type { public: explicit ContainerRangeAdapter(const Container c) : container_(c) {} size_t Size() const override { return container_.Size(); } typename Container::value_type At(size_t index) const override { return container_.GetAt(index); } private: const Container container_; };之后无论是 OldVector、OldLinkedList 还是老排序表只要提供 Size() 和 GetAt() 成员函数都能用一个统一的适配器模板完成适配。不需要三个类只要三个别名using VectorRange ContainerRangeAdapterOldVector; using LinkedListRange ContainerRangeAdapterOldLinkedList; using SortedListRange ContainerRangeAdapterOldSortedList;这里有一个关键点模板适配器依赖的是“duck typing”也就是结构兼容性而非继承关系。只要类型满足模板内使用的成员函数约束就可以完成适配。这也是模板适配器与多继承适配器最大的哲学差异——约束是在编译期由使用点决定的而不是在类型定义时由继承关系锁死的。3.3 模板适配器结合概念约束后的使用体验模板适配器一开始有个缺点——如果容器类型不满足要求编译错误会出现在实例化点报错信息往往又长又绕。我接手过一段代码一个模板适配器连续嵌套了三层编译失败时报出的错误信息包含几十个类名看得人头皮发麻。后来我改用 C20 的 concept 给适配器加上约束情况好转很多。下面这个示例给模板适配器定义了清晰的协议要求出错时直接提示缺了哪个接口而不是甩出一屏模板实例化过程template typename T concept RangeCompatible requires(const T t, size_t i) { { t.Size() } - std::same_assize_t; { t.GetAt(i) } - std::convertible_totypename T::value_type; }; template RangeCompatible Container class ContainerRangeAdapter : public RangeReadertypename Container::value_type { // 实现同上 };约束写出来的本身就是适配器的使用契约新接入一种容器时只需要对照约束检查成员函数是否齐全不用去读适配器内部实现。我在新代码里基本都这个套路项目里后来加入的容器类型几乎没有因为适配器编译问题卡过壳。4. 函数层面的适配器std::function 与回调的接口转换前几节聊的都是“类的接口”适配但真实 C 项目里的接口不只有类——还有大量回调函数、函数指针、lambda、函数对象。它们之间的调用约定、参数形态、返回值类型经常不一致。C 标准库提供的 std::function 本质上就是一种适配器变体它把各种可调用对象统一适配成一个可以被任意调用的类型。4.1 std::function 如何做类型擦除std::function 的核心魔法叫类型擦除。你把它想象成一个“万能插座”——背后不管是接线板、变压器还是手机充电头只要最终能输出插座需要的电流插头上呈现出来的就是一个统一的接口。std::function 在内部用一个非模板基类指针指向一个模板派生类这个模板派生类具体保存了传入的 lambda、函数指针或 bind 表达式并在 operator() 时调用真正保存的那个对象。使用场景非常典型。比如一个事件分发中心希望不同模块都往里面注册回调但每个模块的注册函数形态居然有三种void OldCallback(); // 老模块回调无参数 void NewCallbackWithContext(void* ctx); // 新模块要求上下文指针 int SweetCallback(double ratio, int flags);// 另一个模块带参数且需要返回 int你不能要求事件中心去知道所有回调形态。用 std::function 做一层适配把三种形态统一成无返回值的无参回调就可以全量注册using SimpleCallback std::functionvoid(); SimpleCallback adaptOld [] { OldCallback(); }; SimpleCallback adaptNew [ctx getContext()] { NewCallbackWithContext(ctx); }; SimpleCallback adaptSweet [] { int result SweetCallback(0.5, 3); consume(result); };注意第三个适配里有个容易被忽略的细节——返回值是 int但目标接口是不要求返回的 void。std::function 允许返回类型忽略也就是接受返回 int 的 lambda 注册到 std::functionvoid()这本质上是把返回值“适配丢弃”了。如果你的适配策略不是丢弃而是取反、取绝对值、转换成 bool同样可以在 lambda 内部做再返回目标类型。这就是函数适配层的常用套路参数转换、上下文绑定、返回值重定义都浓缩在一个小 lambda 里。4.2 std::bind 和 lambda 的选择早期 C 代码里函数适配还常用 std::bind 做参数重排和绑定。std::bind 可以把一个多参数函数的某些参数固定并调整参数顺序从而适配到另一个回调签名。void ApplyDiscount(double rate, double price, bool member); // 某平台回调要求void(double price) auto adapter std::bind(ApplyDiscount, 0.8, std::placeholders::_1, true);现在我把新代码里 std::bind 的使用率已经降到很低了。相比之下 lambda 捕获上下文更直观适配意图一眼能看懂。std::bind 的参数占位符对于不熟的人很难读而且当你需要同时做类型转换、条件判断、返回值变换时它在表达能力上也不如 lambda 灵活。有一点值得提如果你是在调试一个线上崩溃发现崩溃发生在 std::function 调用处而注册的 lambda 捕获了一堆引用十有八九是引用捕获的某个对象生命周期已经结束。我遇到过的案例是某个插件系统在热卸载后回调函数还能被触发但被捕获对象已经析构——这类问题用弱引用捕获、或者通知机制来注销回调通常会缓解适配层本身也要避免持有裸引用捕获状态尽量用 shared_ptr 或值拷贝。4.3 回调适配层的压测与性能观察函数适配层的性能有时会成为热点。定制一个带类型擦除的 std::function比直接调用一个函数指针要慢一些在每秒几百万次调用的纯计算场景这个差距是可感知的。某个图像处理管线里我用 std::function 做算子回调适配压测发现比手写函数表慢了大概 3~8%。后来评估后并不认为这是瓶颈因为实际调用里算子的计算开销远大于分发开销。但如果你的场景真的是高频、多态分发又不支持模板展开建议选一组在编译期就能确定的调用形态直接用函数指针表否则 std::function 的灵活性和可读性收益通常大于那点性能损失。性能结论依赖具体场景合理评估不要拿到任何模板就开始满口 top。5. 迭代器适配器你看不出它是适配器但它确实是迭代器是 C 对适配器模式最隐蔽但最出色的运用。你平时天天用 std::reverse_iterator、std::back_inserter、istream_iterator但很少有人把这些工具与适配器模式联系起来。迭代器适配器的核心思想是把一个类型的遍历行为包装成另一种迭代器语义不改变底层容器或流的数据结构。5.1 标准库里的迭代器适配器家族std::reverse_iterator 就是最典型的例子。它包装一个正向迭代器把 操作转换成底层迭代器的 --* 操作返回的内容也会做对应偏移从而实现逆序遍历。std::vectorint v{1, 2, 3, 4, 5}; auto rbegin std::make_reverse_iterator(v.end()); auto rend std::make_reverse_iterator(v.begin()); for (auto it rbegin; it ! rend; it) { printf(%d\n, *it); // 输出 5 4 3 2 1 }std::back_inserter 则是把“容器”适配成“输出迭代器”的代表。它包装容器的 push_back 接口容器本来没有迭代器语义通过 back_insert_iterator 这一适配层就可以在 std::copy 这类泛型算法中直接作为目标迭代器使用std::vectorint src{1, 2, 3}; std::vectorint dst; std::copy(src.begin(), src.end(), std::back_inserter(dst));flow 迭代器 istream_iterator 更是把“输入流”适配成了“只读迭代器”。流本身没有 begin/end 概念但用一个包装层就能让标准算法直接对流做遍历。这些适配器全部从标准库层面证明了接口转换不一定要改类继承关系在算法层做操作符重载和语义转换也能完成适配。5.2 给自己封一个缓存迭代器适配器第一次让我真正决策出迭代器适配器价值的是在一个日志分析模块里。当时数据源是一个很大很慢的文本文件每读一行要做大量正则和格式解析但业务方经常需要在一个分析流程里反复读前 200 行、重复访问某些行内容。如果直接硬读整个文件内存吃不消每次重新解析又太慢。解决方案就是做一个缓存迭代器适配器包装一个输入迭代器但把已解析出来的行存进一个 LRU 缓冲区迭代器前进时先查缓存缺了才从数据源解析。template typename InnerIterator class CachingIterator { public: using value_type std::string; // 核心逻辑*it 解引用前校验缓存 const std::string operator*() const { if (!cache_.contains(current())) { cache_.emplace(current(), loadFromInner()); } return cache_[current()]; } private: InnerIterator inner_; std::mapstd::string, std::string cache_; };这个适配器的价值在于上层所有代码只需要认识普通的迭代器接口完全不感知缓存逻辑。接不接缓存、用什么缓存策略都是适配层内部的事情。类似的思路还可以用在加密文件流、远程数据块读取、压缩数据解包等场景上——底层的访问接口五花八门适配到统一迭代器后上层算法代码就可以做到一定程度的通用化。5.3 迭代器适配器的接口兼容成本用迭代器适配器的时候最关键的一点是分清“迭代器类别”。标准库把迭代器分成输入、输出、前向、双向、随机访问五类。适配器能做多大范围的语义转换取决于它能不能把内部迭代器的能力完全暴露出来。比如用单向流适配出的迭代器就很难支持随机访问这时外层算法如果用 std::sort 就会编译失败或者退化成极低效操作。我给某个同事代码评审时发现一个典型问题他用一个按行读取的输入迭代器适配器去给 std::sort 用结果 sort 要求随机访问迭代器编译直接报错。后来改成先把前 N 行读入 vector再做排序本质上是把一个不符合迭代器类别要求的操作重新设计成符合要求的操作。适配器不是万能的它能实现的语义范围受底层能力的物理限制单向数据流不可能被适配成双向随机访问除非你先做缓存或全量加载。6. 适配器实现中的隐藏陷阱与取舍判断前面几种变体各有各的适用位置但适配器写多了以后我发现真正让项目出问题的常常不是模式选型而是实现细节上的疏漏。下面这几个坑是我亲身踩过或者评审时抓到过的每个都有对应的处理方式。6.1 缺少虚析构函数导致的未定义行为类适配器或多态 Target 接口几乎都要求基类析构函数是 virtual。这个道理写下来很简单但工程里经常因为人为疏忽或某位开发者写的接口没有虚析构导致 delete 基类指针时派生类析构函数不被调用资源泄漏悄然发生。我在一个模拟项目 X 中排查过这样的问题某个适配器持有底层的网络连接句柄析构时本应 close 连接并归还连接池。但因为 Target 基类析构不是 virtual所有适配器对象被 delete 时都只走了基类析构句柄永远没被释放最终连接池耗尽导致服务假死。排查过程花了很久——崩溃不在一个固定位置对象泄露也不是立刻体现等到业务高峰期才暴露。修复很简单把基类析构改成虚函数。但我想给一个额外的建议把基类设计成纯接口析构函数做成 virtual 并且在头文件里直接 default这样既保证接口类型可安全析构也让所有派生类不用显式实现空析构。6.2 对象切片问题当适配器试图持有派生类值类型的组合适配器里如果 Adaptee 本身是一棵继承树而你保存的是它的基类成员对象就有切片风险。派生类部分的数据会被丢弃行为也随之走样。我曾经接到过一个 bug某个适配器内部保存的类型是最基础的 ProtocolMessage 基类而实际传入的是继承自它的带签名校验的 SignedMessage。适配层在转发时把 SignedMessage 切片成 ProtocolMessage所有签名校验字段全部丢失服务端校验直接失败。当时定位的方式是在适配层构造函数里加了个 size 断言检查成员对象大小与传入对象的动态类型信息是否一致很快发现切片确实发生了。最后适配器持有 unique_ptr 而不是基类对象问题解决。诊断切片的最快手段在适配器构造函数里用 typeid(*ptr).name() 打印动态类型再对比适配器内部存储对象的实际类型。如果你看到传入的是 SignedMessage、存储后变成 ProtocolMessage基本就是切片无误。6.3 生命周期混乱被绑定的临时对象与悬垂引用这个坑在函数适配层和迭代器适配层都碰到过。典型模式是把临时对象或临时容器传给适配器构造适配器内部存了 const 引用临时对象一析构引用悬空。编译器通常不会帮你拦住这种错误只有运行时才能体现。规避手段有三个层级第一层适配器构造函数尽量只收拥有所有权的智能指针第二层如果确实要存 const 引用文档和接口签名里明确标明“引用生命周期必须长于适配器对象”第三层在关键路径上加断言比如适配器析构时检查引用目标是否仍在注册表中。这个检查和断言组合让我少遇见了很多不明不白的崩溃。6.4 过度适配当模式成了习惯而不是方案不是所有接口不匹配都要上适配器。有时候两种接口其实概念上就是两套东西强行适配会引入语义扭曲。比如某个团队的旧协议设计里有“连接状态”的概念全部调用方都依赖它新协议里把连接做成了无状态的请求/响应模型。有人写了一个适配器把新协议模拟出旧协议的连接状态语义结果新协议内部的连接池、重试机制被这个适配层搞得很混乱每次重连都把适配器里的状态清零跟旧调用方的状态预期冲突最后成了一个怎么改都修不好的历史包袱。正确的做法可能是改造调用方——既然新模型是全新的就把上层从“状态感知”改成“请求驱动”。适配器的适用边界是接口形态不匹配而不是领域语义不兼容。如果两者语义本身已经不同直接改调用方往往是更省钱的方案。以下是适配器实现与设计中的一个总分判断参考表维度多继承适配器对象组合适配器模板适配器函数适配器单一适配可以但不利长期维护推荐不是首选回调场景好用批量适配类爆炸差评需要在每个类里写类似代码最佳不适用运行时开销无额外开销一次虚函数转发编译期完全零开销类型擦除有少量代价类型关系污染类型系统干净仅面向 Target编译期依赖结构与继承无关擦除具体类型生命周期风险中中需要管理底层对象归属低持有引用需特别注意高lambda 捕获最易出错最怕什么接口面膨胀对象切片、悬垂引用编译错误信息爆长捕获对象的生命周期6.5 接入适配层后的行为验证工具适配器本质上是薄薄一层转发但它会掩盖问题——底层对象的问题经过适配层之后报错位置常常找不准。我在自己的项目里养成一个习惯凡是新建适配器第一个测试用例不写业务行为先写“转发忠实性测试”。具体做法是给适配器传入一个所有操作都带计数和断言的 mock 对象验证适配器有没有漏转发、重复转发、或者转了错误参数。针对接口适配器会写这样的框架#include cassert class RecordingLegacyService : public LegacyService { public: void Handle(const std::string serialized, int priority) override { log.push_back(serialized); lastPriority priority; } std::vectorstd::string log; int lastPriority -1; }; // 测试适配器转发时参数是否正确传递 void testAdapterForwarding() { auto service std::make_sharedRecordingLegacyService(); LegacyServiceAdapter adapter(service); adapter.ProcessRequest(42); assert(service-log.size() 1); assert(service-log[0] serialized-with-42); // 还可以进一步验证 priority 是否等于你预期的那个初值 }这种测试写起来成本不高但能帮你把适配层的“忠实度”锁定住防止后来的人往适配器里夹带私货。我项目里至少三分之一的接口回归问题是通过这种转发忠实性测试提前拦截下来的。确实适配器就是一小撮设计巧思在不同语言里适配器模式的表现形态差别很大。C 因为模板、多继承、运算符重载、类型擦除这些通用机制让适配器不再局限于“某个类实现某个接口”而是可以发生在类型系统、函数签名、迭代器语义、回调约定各个层面。我自己的选择标准很简单单个类型接单个接口优先对象组合一批类型接一套接口模板适配器更合适函数回调适配闭眼用 std::function如果发现适配器开始改变语义而不是单纯转换形态就回头重新审视是不是方向错了。最后分享一个一直在用的小技巧给适配器类统一取一个 Adapter 后缀不是必须的但建议把适配器实例的创建动作统一放在一个单独的头文件和工厂函数里这样代码评审的时候一眼能看出哪里是适配层遇到性能异常或接口改动也能第一时间定位到这个边界。适配层的价值不在于炫技而在于让两套原本没有关系的接口可以稳定地共存替系统扛住变化带来的震荡。
返回列表