
⭐️在这个怀疑的年代,我们依然需要信仰。
个人主页 :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 Pattern)
5.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/,使用 C++11。为阅读方便,片段省略了头文件保护宏。
1. 概述
Wikipedia:In software engineering, the adapter pattern is a software design pattern that allows the interface of an existing class to be used as another interface. It is often used to make existing classes work with others without modifying their source code.
在软件工程中,适配器模式是一种软件设计模式,它允许将现有类的接口用作另一个接口。它通常用于 使现有类与其他类一起工作,而无需修改其源代码。
GoF:Convert the interface of a class into another interface the clients expect. Adapter lets classes work together that couldn't 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: ap=adapter::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 这一行:
ap=adapter::SteamBoatAdapter
代码一行不用动。这就是适配器带来的价值:客户端与具体实现彻底解耦,而这个解耦点还被配置文件外置了。
5. 默认适配器模式(Default Adapter Pattern)
5.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 的一大堆 XxxAdapter(MouseAdapter、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 代理
这三个模式结构上几乎一模一样——都是"继承一个接口,在方法里转发给另一个对象"。区分它们只能靠意图,这是面试的标准答案。
| 意图 | 转换接口——让不兼容的接口能协作 | 增强功能——不改接口的前提下动态添加职责 | 控制访问——在客户端和目标之间加一层管控 |
| 接口 | 转换成另一个接口 | 与目标同一个接口 | 与目标同一个接口 |
| 目标从哪来 | 外部传入/已有的、类型不同 | 外部传入的,通常是同一个抽象类型 | 自己创建或持有目标 |
| 客户端感知 | 感知不到 Adaptee 存在 | 感知不到装饰器存在 | 感知不到目标存在 |
| 典型用途 | 兼容第三方库、遗留代码 | 层层叠加的功能(IO 流、加密、压缩) | 延迟加载、权限校验、日志、缓存、AOP |
一句话记忆:
适配器管"接口对不对",装饰器管"功能够不够",代理管"能不能给你用"。
7.4 高频追问清单
| 适配器模式属于哪一类? | 结构型模式,别名 Wrapper |
| 适配器和桥接模式的区别? | 桥接是设计之初的架构决定,把抽象部分与实现部分分离,让两者都能独立变化;适配器是事后补救,让已有的不兼容接口能协同工作。桥接是"预先分家",适配器是"事后接线" |
| 适配器和外观模式(Facade)的区别? | Facade 是给一个子系统提供简化的统一入口,不转换接口、也不增加功能;Adapter 是转换单个类/接口。Facade 是"简化",Adapter 是"转换" |
| 适配器能做成双向的吗? | 可以(双向适配器同时实现 Target 和 Adaptee 两套接口),但实践中很少见,因为说明两边接口设计都有问题 |
| 适配器里能不能加业务逻辑? | 不应该。适配器的职责只有"转换"。一旦塞进业务逻辑,它就变成了装饰器或代理,职责混乱 |
| 适配器是线程安全的吗? | 适配器本身通常无状态,是否线程安全取决于 Adaptee。适配器不会自动给你加锁 |
| 什么时候不该用适配器? | 见 7.5。核心判断:这个接口改不改得动——改得动就直接改,别包一层 |
| 适配器和"多写一个转换函数"有什么区别? | 转换函数是散落在调用处的、静态的;适配器是面向接口的、多态的——客户端拿着 Target* 就能工作,不用关心后面是谁 |
7.5 什么时候不该用适配器
判断标准就一条:这个不兼容的接口,我到底改不改得动?
-
两边都是自己写的 → 直接改接口,把方法名统一、参数对齐。加适配器只是把设计问题藏起来;
-
只是方法名不一样,语义完全一致 → 优先考虑改接口或加一层薄封装函数,不必引入模式;
-
语义差异很大(比如把一个"同步、阻塞、可能抛异常"的接口适配成"异步、不阻塞"的接口)→ 适配器里会堆满状态机、线程池、回调,这不叫适配器,这叫重写。此时应该重新设计边界,或者老老实实包一个真正的中间层;
-
适配器数量越来越多 → 这是信号:接口设计本身有问题,该重构了。
适配器的价值在于在"改不动"的地方做兼容。改得动的地方还套适配器,是标准的过度设计。
结语
我是YYYing,后面还有更精彩的内容,希望各位能多多关注支持一下主包。
无限进步,我们下次再见!


![set 与 map :从 [] 暗坑到严格弱序,把有序关联容器用明白-171主机测评](https://www.171host.com/wp-content/uploads/2026/10/20261031153944-6ae60bc0ee6df-220x150.png)

