
⭐️在这个怀疑的年代,我们依然需要信仰。
个人主页 :YYYing.
⭐️设计模式系列专栏:设计模式系列
系列上期内容:【设计模式系列 (六) 】适配器模式
系列下期内容:暂无
目录
[1. 概述](#1. 概述)
[2. 结构](#2. 结构)
[3. 仓库实现:数据持久化(bridge/)](#3. 仓库实现:数据持久化(bridge/))
[3.1 场景](#3.1 场景)
[3.2 Implementor:实现类接口](#3.2 Implementor:实现类接口)
[3.3 ConcreteImplementor:两个具体实现](#3.3 ConcreteImplementor:两个具体实现)
[3.4 Abstraction:持有实现层抽象的那一层(桥)](#3.4 Abstraction:持有实现层抽象的那一层(桥))
[3.5 Client:配置驱动的客户端](#3.5 Client:配置驱动的客户端)
[3.6 关于 AbstractionImpl](#3.6 关于 AbstractionImpl)
[3.7 桥接 vs 多层继承:类个数的账](#3.7 桥接 vs 多层继承:类个数的账)
[4. 优缺点与适用环境](#4. 优缺点与适用环境)
[5. 面试专题](#5. 面试专题)
[5.1 开场题:说说桥接模式吧](#5.1 开场题:说说桥接模式吧)
[5.2 必问:桥接 vs 适配器](#5.2 必问:桥接 vs 适配器)
[5.3 高频:桥接 vs 策略](#5.3 高频:桥接 vs 策略)
[5.4 高频:桥接能不能完全取代多继承](#5.4 高频:桥接能不能完全取代多继承)
[5.5 追问清单](#5.5 追问清单)
一辆车,可以按"车型"分(轿车、SUV、卡车),也可以按"发动机"分(汽油、电动、混动)。如果一条路走到黑用继承,你要写 3 × 3 = 9 个类;而实际上"车型"和"发动机"是两个互不相干的变化方向------轿车能装汽油机,SUV 也能装电动,组合本该是自由的。
桥接模式解决的就是这件事:把两个独立变化的维度分开,用组合代替继承。
本文代码取自 design-pattern-cpp 仓库的 design-pattern/bridge/,使用 C++11。为阅读方便,片段省略了头文件保护宏。
1. 概述
Wikipedia :The bridge pattern is a design pattern used in software engineering that is meant to decouple an abstraction from its implementation so that the two can vary independently.
桥接模式是软件工程中使用的一种设计模式,旨在 "将抽象与其实现分离,以便两者可以独立变化"
三个关键词:
① 两个独立的维度
这是使用桥接模式的前提。只有一个变化方向时,用策略模式就够;只有当类同时沿着两条轴变化、且两条轴互不依赖时,桥接才有意义。识别"哪两个维度是独立的"靠经验,这也是桥接模式最大的门槛。
② 别名"柄体模式"(Handle And Body)
抽象部分是"柄",实现部分是"体"------你握着柄,体可以随便换。
③ 用关联取代多层继承
多层继承是静态 的(编译期绑定,类爆炸),桥接是动态的(运行期换实现)。它比多层继承方案更符合单一职责和开闭原则。
桥接模式属于结构型模式 ,且通常在开发前期设计------这一点常和适配器对比,见 6.2。
2. 结构
| 角色 | 职责 |
|---|---|
| Abstraction(抽象类) | 定义抽象接口,持有一个 Implementor 对象 ,二者是关联关系。既可以有抽象业务方法,也可以有具体业务方法 |
| AbstractionImpl(扩充抽象类) | 继承 Abstraction,实现其中的抽象业务方法,在方法里调用 Implementor 的业务方法。通常是具体类 |
| Implementor(实现类接口) | 定义实现类的接口。一般只提供基本操作,与 Abstraction 的接口可以完全不同 |
| ConcreteImplementor(具体实现类) | 实现 Implementor 接口,提供基本操作的具体实现,运行时替换父类对象 |

关键点:Abstraction 里持有的成员类型是实现层的抽象,所以抽象层和实现层可以各自扩展、互不修改。
3. 仓库实现:数据持久化(bridge/)
3.1 场景
同一个"保存数据"的业务,可以存到数据库 ,也可以存到文件系统。这里两个维度是:
-
抽象维度 :业务侧怎么发起持久化(
Persistence,将来可以扩展出带缓存、带事务的变体) -
实现维度 :数据落到哪里(
DataBase/FileSystem)

3.2 Implementor:实现类接口
cpp
namespace bp
{
class PersistenceImplementor
{
public:
virtual void saveData() = 0;
virtual void updateData() = 0;
};
}
只声明基本操作,不掺业务语义。
3.3 ConcreteImplementor:两个具体实现
cpp
class DataBase : public PersistenceImplementor
{
DECLARE_CLASS(bp::DataBase);
public:
void saveData() override;
void updateData() override;
};
void bp::DataBase::saveData() { std::cout << "成功保存到数据库" << std::endl; }
void bp::DataBase::updateData() { std::cout << "成功更新到数据库" << std::endl; }
class FileSystem : public PersistenceImplementor
{
DECLARE_CLASS(bp::FileSystem);
public:
void saveData() override;
void updateData() override;
};
void bp::FileSystem::saveData() { std::cout << "成功保存到文件中" << std::endl; }
void bp::FileSystem::updateData() { std::cout << "成功保存到文件中" << std::endl; }
3.4 Abstraction:持有实现层抽象的那一层(桥)
cpp
namespace bp
{
class PersistenceImplementor; // ★ 前向声明,头文件不依赖实现层
class Persistence
{
private:
PersistenceImplementor* impl; // ★ 桥:成员类型是"实现类接口"
public:
Persistence(PersistenceImplementor* impl);
void saveData();
void updateData();
};
}
bp::Persistence::Persistence(PersistenceImplementor* impl)
{
this->impl = impl;
}
void bp::Persistence::saveData() { impl->saveData(); }
void bp::Persistence::updateData() { impl->updateData(); }
Persistence 自己不做任何实际工作 ,只是把请求转发给被持有的实现对象。这一层就是"柄",DataBase / FileSystem 是"体"。
3.5 Client:配置驱动的客户端
cpp
int main()
{
// 创建持久化实现对象
CREATE_PROPERTIES(prop, conf);
std::string cls = prop.getProperty("brp"); // conf.properties: brp=bp::DataBase
GET_INSTANCE_BY_NAME(PersistenceImplementor*, impl, cls);
// 创建持久化对象:把实现注入抽象
Persistence persistence(impl);
// 完成持久化操作
persistence.saveData();
persistence.updateData();
delete impl; // 如上所述,所有权在客户端
return 0;
}
换实现只改配置:
cpp
brp=bp::DataBase # 存数据库
# brp=bp::FileSystem # 存文件
DECLARE_CLASS / IMPLEMENT_CLASS / GET_INSTANCE_BY_NAME 是工厂篇讲过的自注册反射宏,作用是按字符串类名创建对象。
3.6 关于 AbstractionImpl
上图中有个 AbstractionImpl 角色,但例子把它省了------Persistence 直接就把两个方法透传下去。真实项目里,扩展的那一层才是桥接的价值所在,比如:
cpp
class TransactionalPersistence : public Persistence // 扩展抽象维度
{
public:
void saveData() {
beginTransaction();
Persistence::saveData(); // 还是走那把"桥"
commit();
}
};
TransactionalPersistence × {DataBase, FileSystem} 两种新组合,只需要 1 个新类------这就是 3.7 要说的效果。
3.7 桥接 vs 多层继承:类个数的账
设想"形状"(圆、方形、三角)× "颜色"(红、蓝)两个维度:
| 方案 | 类个数 | 新增一种颜色 | 新增一种形状 |
|---|---|---|---|
| 多层继承 | m × n = 3 × 2 = 6 | +3 个类 | +2 个类 |
| 桥接 | m + n = 3 + 2 = 5 | +1 个类 | +1 个类 |
规模一大,差距是爆炸性的:10 × 10 时,继承要 100 个类,桥接只要 20 个。而且继承方案里,每个类都同时承担了"形状"和"颜色"两份职责,违背单一职责。
一句话记忆:继承是 m × n,桥接是 m + n。
4. 优缺点与适用环境
主要优点
-
分离抽象接口及其实现部分,用对象间的关联关系解耦了抽象与实现固有的绑定关系,两者可沿各自维度独立变化;
-
取代多层继承方案,极大减少子类个数(m + n < m × n),符合单一职责;
-
可扩展性高:任意扩展一个维度都不需要修改原有系统,符合开闭原则;
-
抽象层的实现细节对客户端透明,抽象层里可以随时替换实现。
主要缺点
-
增加了理解与设计难度:关联关系建立在抽象层,要求一开始就针对抽象层设计编程;
-
要求正确识别两个独立变化的维度,识别错了模式就白用了,使用范围有局限性。
适用环境
-
想拆分或重组一个具有多重功能的庞杂类(例如能与多个数据库服务器交互的类);
-
希望在几个独立维度上扩展一个类;
-
需要在运行时切换不同实现方法。
5. 面试专题
5.1 开场题:说说桥接模式吧
按概念 → 角色 → 场景 → 扩展的顺序答:
概念 :桥接模式将抽象部分与实现部分分离 ,使二者可以独立变化。它用对象间的关联关系取代了传统的多层继承,别名"柄体模式"(Handle And Body),属于结构型模式。
角色:Abstraction 抽象类持有 Implementor 接口对象;RefinedAbstraction 扩充抽象接口,在业务方法里转发给 Implementor;ConcreteImplementor 提供基本操作的具体实现。
适用场景:类存在两个独立变化的维度(典型如"形状 × 颜色");想拆分一个多重功能的庞杂类;需要在运行时切换实现。
可扩展:接上"和适配器的区别"(必问,见 5.2)。如果面试官追问"为什么不直接继承",答 3.7 的 m × n 与 m + n。
5.2 必问:桥接 vs 适配器
两个模式结构上都是"持有一个对象、转发调用",但意图和时机完全不同:
| 桥接 Bridge | 适配器 Adapter | |
|---|---|---|
| 意图 | 分离两个可独立变化的维度,让它们各自扩展 | 转换接口,让不兼容的类能协作 |
| 时机 | 开发前期设计,是架构决定 | 已有程序中的事后补救 |
| 维度 | 两个维度同时存在且独立变化 | 只有一个"接口不对口"的问题 |
| 关系的性质 | 抽象与实现是对称的、长期共存 | Target 与 Adaptee 是主从的,Adaptee 是被迫兼容的一方 |
| 有没有"兼容"的意味 | 没有,本来就是自己设计的 | 有,是它存在的唯一理由 |
一句话记忆:桥接是"预先分家",适配器是"事后接线"。
追问变体:"适配器能不能改造成桥接?" 可以------当适配器越来越多时,说明接口设计本身没分好层,此时把 Target 层和 Adaptee 层重新设计成两个独立维度,就是向桥接的演进。
5.3 高频:桥接 vs 策略
两者都是"持有接口、转发调用",区别在于维度的数量:
策略 只有一个维度(算法),Context 是稳定 的,变化的是内部算法的选择,本质是替换 ; 桥接 有两个维度,抽象层和实现层都会各自扩展 ,本质是二维正交分解。
换个说法:策略模式里 Abstraction 通常不参与继承扩展;桥接模式里的 Abstraction 一定有自己的继承树(RefinedAbstraction)。结构相近,看有没有两条继承线就能分清。
5.4 高频:桥接能不能完全取代多继承
能解决的问题 :两个独立维度上的组合爆炸(m × n → m + n)。 不能解决的问题 :真正的**"is-a"多继承**------一个类确实需要同时具备两个父类的身份和行为(如"既是文件又是流")。这种语义上的多继承,桥接替代不了。
另外 C++ 的多继承容易带来菱形继承和
this指针调整的问题,用组合(桥接)反而更安全。
5.5 追问清单
| 追问 | 答法 |
|---|---|
| 桥接模式属于哪一类? | 结构型模式,别名柄体模式(Handle And Body) |
| 桥接模式体现哪些设计原则? | 单一职责(两个维度各管一摊)、开闭原则(扩展维度不改原码)、组合复用原则(关联取代继承)、里氏代换、依赖倒转(两侧都依赖抽象) |
| 桥接模式的"桥"指的是什么? | Abstraction 里持有的那个 Implementor 类型的成员,也就是抽象与实现之间的关联关系 |
| Abstraction 和 Implementor 的接口必须一致吗? | 不必 ,事实上可以完全不同。Implementor 只提供基本操作,Abstraction 可能做更复杂的组合操作 |
| 运行时能换实现吗? | 可以,这是桥接相对继承的核心优势。但要注意换之前先处理旧实现的所有权,避免泄漏或双重释放 |
| 谁负责创建 Implementor? | 通常是工厂/反射(本仓库用配置 + 自注册反射),再注入 Abstraction。桥接本身不管创建,只管使用------这是它和工厂模式经常配合出现的原因 |
| 桥接模式的缺点? | 提高了理解与设计难度,且必须先正确识别两个独立维度,识别不出来就不该用 |
| 判断要不要用桥接的标准? | 数一下:这个类是不是同时沿着两条轴变化?是 → 桥接;只有一条 → 策略,够了 |
| 框架里哪里见过桥接? | JDBC 驱动、JDK 的 AWT/Swing Peer、C++ 的 iostream(stream 与 streambuf 分离)、游戏引擎里"渲染 API × 平台" |
结语
我是YYYing,后面还有更精彩的内容,希望各位能多多关注支持一下主包。
无限进步,我们下次再见!