【设计模式系列 (七) 】桥接模式

⭐️在这个怀疑的年代,我们依然需要信仰。

个人主页 :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,后面还有更精彩的内容,希望各位能多多关注支持一下主包。

无限进步,我们下次再见!

相关推荐
lizhongxuan1 小时前
Agent Sandbox: Firecracker 镜像、启动与 I/O
后端
高山有多高2 小时前
【Linux笔记】Linux自定义Shell
linux·c++
悠仁さん2 小时前
【C++】vector 类
开发语言·c++
_upupup2 小时前
智能指针的原理及简单模拟实现(C++)
开发语言·c++
l1t2 小时前
DeepSeek总结的OpenZL压缩变换器
c++·算法
打工仔折腾 AI2 小时前
飞牛OS上用Docker Compose部署ExerciseDiary运动记录并配置远程访问
运维·人工智能·后端·python·docker·容器·ai agent 实战
郝学胜-神的一滴3 小时前
C++ Templates 01:拨开迷雾,读懂C++模板经典著作
开发语言·c++·程序人生·游戏·游戏引擎·软件构建
小蒜学长3 小时前
基于Python的动物救助站管理系统的设计与实现(代码+数据库+LW)
数据库·后端·python·django·动物救助
坤坤子吖3 小时前
算法学习——高精度加减乘除
c++·笔记·学习·算法