 】适配器模式)
⭐️在这个怀疑的年代我们依然需要信仰。个人主页 YYYing.⭐️设计模式系列专栏设计模式系列系列上期内容【设计模式系列 (五) 】原型模式系列下期内容暂无目录1. 概述2. 结构3. 两种实现3.1 对象适配器组合 / 委托—— 推荐3.2 类适配器多继承3.3 两者对比必问4. 仓库实现船长与船adapter/4.1 场景4.2 Target客户端期望的接口4.3 Adaptee已有的、接口不兼容的类4.4 Adapter两个对象适配器4.5 Client只面向 Target 编程5. 默认适配器模式Default Adapter Pattern5.1 问题5.2 做法5.3 Java 里很常见C 里少见6. 优缺点与适用环境7. 面试专题7.1 开场题说说适配器模式吧7.2 必问类适配器 vs 对象适配器7.3 必问适配器 vs 装饰器 vs 代理7.4 高频追问清单7.5 什么时候不该用适配器结语出国要带转换插头笔记本电源上写着 AC Adapter——这两件事和代码里的适配器模式是同一个道理东西是好的就是插不上加一层转接。本文代码取自 design-pattern-cpp 仓库的design-pattern/adapter/使用 C11。为阅读方便片段省略了头文件保护宏。1. 概述WikipediaIn software engineering, the adapter pattern is a software design pattern that allowsthe interface of an existing class to be used as another interface. It is often used tomake existing classes work with others without modifying their source code.在软件工程中适配器模式是一种软件设计模式它允许将现有类的接口用作另一个接口。它通常用于 使现有类与其他类一起工作而无需修改其源代码。GoFConvert the interface of a class into another interface the clients expect. Adapter lets classes work together that couldnt otherwise because of incompatible interfaces.将类的接口转换为客户端期望的另一个接口。适配器让那些接口不兼容的类可以一起工作。tip在适配器模式定义中所提及的接口是指广义的接口它可以表示一个方法或者方法的集合三个关键词① 接口转换把一个类的接口转换成客户端期望的另一个接口。注意是转换不是增强也不是控制访问——这个区别是面试的高频考点见 7.3。② 别名叫 Wrapper包装器③ 不改源码适配器模式存在的前提是已有的类接口可用但不对口而我们不能或不想改它。典型场景就是第三方库、遗留系统、别人维护的模块。如果两个模块都是自己写的正确做法是直接改接口而不是加一层适配器——这一点在面试里经常作为什么时候不该用的标准答案。适配器模式属于结构型模式Structural Pattern。2. 结构角色职责Target目标接口客户端期望的接口。可以是抽象类或接口Adaptee适配者已经存在的、接口不兼容的那个类Adapter适配器同时了解 Target 和 Adaptee把 Adaptee 的接口转换成 Target 的接口Client客户端只面向 Target 编程感知不到 Adaptee 的存在关键点客户端只认识 Target。Adaptee 被适配器包在里面客户端一行代码都不用改。3. 两种实现3.1 对象适配器组合 / 委托—— 推荐class Adapter : public Target { private: Adaptee adaptee; // ★ 持有被适配者 public: Adapter(Adaptee adaptee) { this-adaptee adaptee; } void request() override { // 转换把请求委托给 Adaptee adaptee.specificRequest(); } };用组合把 Adaptee 装进来在 Target 的方法里做转发。3.2 类适配器多继承class Adapter : public Adaptee, public Target // ★ 同时继承两者 { public: void request() override { // 转换直接调用父类实现 Adaptee::specificRequest(); } };用多继承同时具备两者的类型request()里直接调父类的实现。3.3 两者对比必问对象适配器组合类适配器多继承实现方式继承 Target 持有Adaptee同时继承Adaptee 和 Target能否适配 Adaptee 的子类✅ 能。成员声明成基类指针即可适配整棵继承树❌ 不能。继承是编译期绑定的一个适配器只能适配一个具体类是否暴露 Adaptee 接口✅ 不暴露Adaptee 藏在成员里❌ 暴露。客户端可以绕过 Target 直接调 Adaptee 的公有方法能否覆写 Adaptee 的部分行为不能除非自己继承 Adaptee 再组合能重写 Adaptee 的虚函数即可语言限制所有 OOP 语言都支持需要多继承Java / C# 写不出来二者只能选其一继承/实现耦合度低高推荐度首选少数派C 里才有的选择一句话记忆对象适配器用一个 Adaptee类适配器是一个 Adaptee。4. 仓库实现船长与船adapter/4.1 场景船长Captain只会划船他只认识RowingBoat这个接口。但现有的船是FishingBoat渔船——只有sali()打渔SteamBoat汽艇——只有surf()冲浪两个类的接口都和RowingBoat不对口而且它们是已有的、不该被改的类。这正是适配器的用武之地。4.2 Target客户端期望的接口namespace adapter { class RowingBoat { public: virtual void row() 0; }; }4.3 Adaptee已有的、接口不兼容的类class FishingBoat // adapter/ee/FishingBoat.h { public: void sali() { // 仓库里的原拼写 std::cout 使用渔船打渔 std::endl; } }; class SteamBoat // adapter/ee/SteamBoat.h { public: void surf() { std::cout 使用汽艇冲浪 std::endl; } };两个都是普通类不是RowingBoat的子类也没有row()方法。4.4 Adapter两个对象适配器class FishingBoatAdapter : public RowingBoat { DECLARE_CLASS(adapter::FishingBoatAdapter); private: FishingBoat boat; // ★ 组合持有 Adaptee public: void row() override; }; void adapter::FishingBoatAdapter::row() { boat.sali(); // ★ 接口转换row → sali } class SteamBoatAdapter : public RowingBoat { DECLARE_CLASS(adapter::SteamBoatAdapter); private: SteamBoat boat; public: void row() override; }; void adapter::SteamBoatAdapter::row() { boat.surf(); // ★ 接口转换row → surf }每个适配器只有薄薄一层继承 Target组合 Adaptee在row()里转发一次调用。转换逻辑集中在这一处主体代码零修改。DECLARE_CLASS/IMPLEMENT_CLASS是工厂篇讲过的自注册反射宏——IMPLEMENT_CLASS会在静态初始化阶段 new 一个DynamicClass把类名 → 创建函数注册进ClassFactory单例从而支持按字符串类名创建对象。4.5 Client只面向 Target 编程int main() { // 创建适配器 CREATE_PROPERTIES(prop, conf); std::string clsName prop.getProperty(ap); // conf.properties: apadapter::SteamBoatAdapter GET_INSTANCE_BY_NAME(RowingBoat*, boat, clsName); // 反射创建返回 RowingBoat* // 创建船长 Captain ca(boat); // 开船 ca.row(); // 内存释放 delete boat; return 0; }船长这边class Captain { private: RowingBoat* boat; // ★ 成员类型是 Target不是具体船 public: Captain(RowingBoat* boat) { this-boat boat; } void row() { std::cout 东东哥正在; boat-row(); } };船长全程只认RowingBoat*根本不知道背后是渔船还是汽艇。想换船只改config/conf.properties里的ap这一行apadapter::SteamBoatAdapter代码一行不用动。这就是适配器带来的价值客户端与具体实现彻底解耦而这个解耦点还被配置文件外置了。5. 默认适配器模式Default Adapter Pattern5.1 问题接口方法很多而实现类只关心其中一两个剩下的被迫写一堆空方法class ServiceInterface { // 有十几个方法 public: virtual void m1() 0; virtual void m2() 0; virtual void m3() 0; // ... };每个实现类都得把这十几个方法全实现一遍哪怕只用得上一个。代码臃肿且脆弱——接口一加方法所有实现类全要改。5.2 做法给接口配一个抽象类把所有方法空实现具体类继承它只覆盖需要的方法class ServiceAdapter : public ServiceInterface // 全部方法空实现 { public: void m1() override {} void m2() override {} void m3() override {} }; class ConcreteService : public ServiceAdapter { public: void m2() override { /* 只关心这一个 */ } };ServiceAdapter就是缺省适配器。5.3 Java 里很常见C 里少见Java / C# 里这是标准套路AWT/Swing 的一大堆XxxAdapterMouseAdapter、KeyAdapter、WindowAdapter都是它。但C 里几乎不需要专门写这个类——因为 C 的虚函数本来就可以不是纯虚的可以直接在接口类里给空实现class ServiceInterface { public: virtual void m1() {} // 默认空实现子类按需覆盖 virtual void m2() {} };接口类本身就已经充当了缺省适配器再单独抽一层是多余的。这也是一道不错的对比题同一个模式在不同语言里的存在感差别很大取决于语言特性。6. 优缺点与适用环境主要优点客户端与实现解耦客户端只面向 Target 编程换实现不改客户端复用现成的类而不必修改它符合开闭原则——这是它在遗留系统和第三方库场景下的核心价值把接口转换的细节集中在一个类里符合单一职责转换逻辑只有一处排查问题方便可以把多个接口不一致的类统一到一个接口下给客户端提供一致入口。主要缺点适配器本身是额外的间接层滥用会让系统出现一堆为适配而适配的类可读性下降类适配器依赖多继承语言受限且耦合高只能适配接口不能适配语义如果 Adaptee 的行为和 Target 的语义差得太远适配器里会堆满转换逻辑此时应该考虑重构而不是硬套适配器适配器一旦过多会掩盖接口设计本就不合理这个根本问题。适用环境想使用一个已存在的类但它的接口不符合需求最典型的场景想创建一个可以复用的类与一些彼此之间没有太大关联的类包括将来可能引入的类协同工作需要统一多个不同接口的类对外提供一致入口统一的日志、支付、存储、缓存接口。7. 面试专题7.1 开场题说说适配器模式吧考察点是概念、角色、应用场景按这个顺序答概念适配器模式将一个类的接口转换成客户端期望的另一个接口使原本因接口不兼容而无法一起工作的类可以一起工作。它属于结构型模式别名 Wrapper。角色目标接口 Target 是客户端期望的接口适配者 Adaptee 是已有但接口不兼容的类适配器 Adapter 同时了解两者负责转换客户端只面向 Target 编程。使用场景想使用一个已存在的类但其接口不符合要求时需要统一多个不同接口的类时尤其是不能修改源码的场合——第三方库、遗留系统。可扩展把两种实现方式的对比、和装饰器/代理的区别接上见下。7.2 必问类适配器 vs 对象适配器见 3.3 的表格浓缩成三句话实现上类适配器用多继承同时继承 Adaptee 和 Target对象适配器用组合持有 Adaptee。能力上对象适配器能适配 Adaptee 的整个继承树成员是基类指针类适配器只能适配一个具体类反过来类适配器能覆写 Adaptee 的行为且不需要额外的对象。代价上类适配器暴露 Adaptee 的公有接口、耦合高、还要求语言支持多继承Java / C# 写不出来所以实践中对象适配器是首选。7.3 必问适配器 vs 装饰器 vs 代理这三个模式结构上几乎一模一样——都是继承一个接口在方法里转发给另一个对象。区分它们只能靠意图这是面试的标准答案。适配器 Adapter装饰器 Decorator代理 Proxy意图转换接口——让不兼容的接口能协作增强功能——不改接口的前提下动态添加职责控制访问——在客户端和目标之间加一层管控接口转换成另一个接口与目标同一个接口与目标同一个接口目标从哪来外部传入/已有的、类型不同外部传入的通常是同一个抽象类型自己创建或持有目标客户端感知感知不到 Adaptee 存在感知不到装饰器存在感知不到目标存在典型用途兼容第三方库、遗留代码层层叠加的功能IO 流、加密、压缩延迟加载、权限校验、日志、缓存、AOP一句话记忆适配器管接口对不对装饰器管功能够不够代理管能不能给你用。7.4 高频追问清单追问答法适配器模式属于哪一类结构型模式别名 Wrapper适配器和桥接模式的区别桥接是设计之初的架构决定把抽象部分与实现部分分离让两者都能独立变化适配器是事后补救让已有的不兼容接口能协同工作。桥接是预先分家适配器是事后接线适配器和外观模式Facade的区别Facade 是给一个子系统提供简化的统一入口不转换接口、也不增加功能Adapter 是转换单个类/接口。Facade 是简化Adapter 是转换适配器能做成双向的吗可以双向适配器同时实现 Target 和 Adaptee 两套接口但实践中很少见因为说明两边接口设计都有问题适配器里能不能加业务逻辑不应该。适配器的职责只有转换。一旦塞进业务逻辑它就变成了装饰器或代理职责混乱适配器是线程安全的吗适配器本身通常无状态是否线程安全取决于 Adaptee。适配器不会自动给你加锁什么时候不该用适配器见 7.5。核心判断这个接口改不改得动——改得动就直接改别包一层适配器和多写一个转换函数有什么区别转换函数是散落在调用处的、静态的适配器是面向接口的、多态的——客户端拿着Target*就能工作不用关心后面是谁7.5 什么时候不该用适配器判断标准就一条这个不兼容的接口我到底改不改得动两边都是自己写的→ 直接改接口把方法名统一、参数对齐。加适配器只是把设计问题藏起来只是方法名不一样语义完全一致→ 优先考虑改接口或加一层薄封装函数不必引入模式语义差异很大比如把一个同步、阻塞、可能抛异常的接口适配成异步、不阻塞的接口→ 适配器里会堆满状态机、线程池、回调这不叫适配器这叫重写。此时应该重新设计边界或者老老实实包一个真正的中间层适配器数量越来越多→ 这是信号接口设计本身有问题该重构了。适配器的价值在于在改不动的地方做兼容。改得动的地方还套适配器是标准的过度设计。结语我是YYYing后面还有更精彩的内容希望各位能多多关注支持一下主包。无限进步我们下次再见