图说设计模式之桥接模式(Bridge Pattern):抽象与实现的解耦之道
图说设计模式之桥接模式(Bridge Pattern):抽象与实现的解耦之道
导读:桥接模式(Bridge Pattern)是结构型模式中用于处理"两个独立变化维度"问题的经典方案,它把抽象部分与实现部分分离,让二者可以各自独立演进。本文以「图说设计模式」仓库中 structural_patterns/bridge.rst 为主干,结合 code/Bridge 目录下的完整 C++ 实现源码,讲解桥接模式的动机、结构、代码运行流程、优缺点与适用场景,并给出跨平台视频播放器等典型实战案例,帮助你掌握"用关联关系替代继承关系"的核心设计思想。
图:桥接模式类图——Abstraction 持有 Implementor 的引用,二者通过关联关系连接(图片来源:structural_patterns/bridge.rst)
模式动机:为什么需要桥接模式
设想我们需要绘制矩形、圆形、椭圆、正方形等多种形状,同时这些图形还需要具备红色、绿色、蓝色等不同颜色。如果采用朴素的设计,至少会面临两种方案:
- 方案一:为每一种形状都提供一套各种颜色的版本。例如
RedRectangle、GreenRectangle、BlueCircle……假设有 4 种形状 × 3 种颜色,就需要 12 个类;维度越多、选项越多,类的数量呈笛卡尔积式爆炸增长。 - 方案二:根据实际需要对形状和颜色进行组合。形状类与颜色类各自独立,运行时再组合。
对于存在两个变化维度(即两个变化原因)的系统,方案二可以显著减少系统中类的个数,并且扩展更加方便——新增一种颜色或一种形状都无需改动既有类。方案二正是桥接模式的应用:桥接模式将继承关系转换为关联关系,从而降低类与类之间的耦合,减少代码编写量。
这正是"合成复用原则"的体现:优先使用对象组合/聚合而非继承来达到复用目的。
模式定义与别名
桥接模式(Bridge Pattern):将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称作柄体(Handle and Body)模式或接口(Interface)模式。
从定义可以看出,桥接模式关注的不是"抽象"与"实现"本身,而是它们之间耦合方式的改变——把继承这种静态绑定关系,换成组合/聚合这种动态绑定关系。
模式结构:四大角色
桥接模式包含如下四个角色:
| 角色 | 名称 | 职责 |
|---|---|---|
| Abstraction | 抽象类 | 定义抽象接口,持有并维护一个 Implementor 类型的对象引用 |
| RefinedAbstraction | 扩充抽象类 | 扩充由 Abstraction 定义的接口,实现抽象业务方法,并在其中调用实现类接口定义的方法 |
| Implementor | 实现类接口 | 定义实现类的接口,仅提供基本操作 |
| ConcreteImplementor | 具体实现类 | 实现 Implementor 接口,为基本操作提供不同实现 |
以仓库 code/Bridge 中的源码为例,逐个角色对应如下:
- Implementor.h:定义抽象接口
operationImp(),对应"实现化"一侧的抽象; - ConcreteImplementorA.h / ConcreteImplementorB.h:分别以
A、B两种风格实现operationImp(); - Abstraction.h:定义
operation()接口,并声明Implementor* m_pImp作为对实现一侧的关联; - RefinedAbstraction.h:继承 Abstraction,重写
operation(),在方法体内委托m_pImp->operationImp()完成具体动作。
图:桥接模式时序图——客户端创建具体实现对象并注入抽象对象,调用抽象方法后经由实现接口完成实际操作(图片来源:structural_patterns/bridge.rst)
代码分析:仓库 C++ 实现逐行解读
仓库在 code/Bridge 目录下提供了可直接编译运行的桥接模式示例。核心入口是 main.cpp,我们先看客户端如何组织对象:
#include <iostream>
#include "ConcreteImplementorA.h"
#include "ConcreteImplementorB.h"
#include "RefinedAbstraction.h"
#include "Abstraction.h"
using namespace std;
int main(int argc, char *argv[])
{
Implementor * pImp = new ConcreteImplementorA();
Abstraction * pa = new RefinedAbstraction(pImp);
pa->operation();
Abstraction * pb = new RefinedAbstraction(new ConcreteImplementorB());
pb->operation();
delete pa;
delete pb;
return 0;
}
关键调用链(对应原文档 main.cpp 第 12–17 行的强调部分):
- 创建具体实现对象
ConcreteImplementorA; - 用该实现对象构造
RefinedAbstraction(抽象对象); - 调用
pa->operation()触发业务操作; - 换用
ConcreteImplementorB再构造一个抽象对象并调用pb->operation()——同一个抽象类,配合不同的实现类,即可表现出完全不同的行为,而客户端代码结构不变。
抽象侧:Abstraction 与 RefinedAbstraction
Abstraction.h 中,抽象类通过构造注入的方式持有实现对象:
class Abstraction
{
public:
Abstraction();
virtual ~Abstraction();
Abstraction(Implementor* imp);
virtual void operation();
protected:
Implementor* m_pImp;
};
在 Abstraction.cpp 中可以看到两个关键点:
- 构造函数
Abstraction(Implementor* imp)将外部传入的实现对象保存到m_pImp,完成"搭桥"; - 析构函数
~Abstraction()中执行delete m_pImp;,负责释放实现对象的内存,体现了抽象类对实现对象的所有权维护。
RefinedAbstraction.cpp 第 26–28 行(原文档强调部分)展示了扩充抽象类如何编排业务逻辑:
void RefinedAbstraction::operation(){
cout << "do something else ,and then " << endl;
m_pImp->operationImp();
}
即:先执行抽象侧的附加逻辑("do something else"),再通过 m_pImp->operationImp() 委托给实现侧。抽象侧可以扩展更多复杂操作,而基本操作则完全由实现侧负责——这正是原文档总结中"抽象类定义的接口可能会做更多更复杂的操作,而实现类接口仅提供基本操作"的落地体现。
实现侧:Implementor 与 ConcreteImplementor
Implementor.h 定义实现接口:
class Implementor
{
public:
Implementor();
virtual ~Implementor();
virtual void operationImp();
};
两个具体实现类分别给出不同的输出:
- ConcreteImplementorA.cpp:
cout << "imp in ConcreteImplementorA style." << endl; - ConcreteImplementorB.cpp:
cout << "imp in ConcreteImplementorB style." << endl;
运行结果
编译并运行 code/Bridge/main.cpp 后输出如下(对应仓库 Bridge_run.jpg 的运行截图):
do something else ,and then
imp in ConcreteImplementorA style.
do something else ,and then
imp in ConcreteImplementorB style.
图:桥接模式示例程序真实运行输出(图片来源:structural_patterns/bridge.rst)
可以看到,两次调用走的是同一个 RefinedAbstraction::operation(),但最终输出的实现细节不同——抽象部分与实现部分确实实现了"独立变化"。
模式分析:抽象化、实现化与脱耦
理解桥接模式,重点在于理解如何将**抽象化(Abstraction)与实现化(Implementation)**脱耦,使二者可以独立地变化。三个关键概念:
- 抽象化:抽象化就是忽略一些信息,把不同的实体当作同样的实体对待。在面向对象中,将对象的共同性质抽取出来形成类的过程即为抽象化的过程。
- 实现化:针对抽象化给出的具体实现,就是实现化。抽象化与实现化是一对互逆的概念,实现化产生的对象比抽象化更具体,是对抽象化事物的进一步具体化的产物。
- 脱耦:脱耦就是将抽象化和实现化之间的耦合解脱开,或者说是将它们之间的强关联改换成弱关联,将两个角色之间的继承关系改为关联关系。桥接模式中的所谓脱耦,就是指在一个软件系统的抽象化和实现化之间使用关联关系(组合或者聚合关系)而不是继承关系,从而使两者可以相对独立地变化——这就是桥接模式的用意。
回到仓库代码:Abstraction 不再通过继承去"成为"某个实现,而是通过成员指针 m_pImp 关联一个实现对象;运行期由客户端决定注入 A 还是 B 实现。这种运行时动态绑定就是"脱耦"的直观证据。
实战实例:跨平台视频播放器设计
假设需要开发一个跨平台视频播放器,它需要在不同操作系统平台(Windows、Linux、Unix 等)上播放多种格式的视频文件(MPEG、RMVB、AVI、WMV 等)。这里存在两个独立变化的维度:
- 维度一:操作系统平台(Windows / Linux / Unix……)
- 维度二:视频格式(MPEG / RMVB / AVI / WMV……)
若用继承方案,平台 × 格式的组合将产生大量类;而采用桥接模式,可以:
- 将"播放控制"抽象为 Abstraction 一侧(扩充抽象类可派生出针对不同格式的播放器,如 MPEG 播放器、RMVB 播放器);
- 将"平台相关实现"抽象为 Implementor 一侧(如 Windows 实现、Linux 实现、Unix 实现);
- 运行时把具体的格式播放器与具体的平台实现动态组合,即可覆盖所有平台 × 格式的播放需求,且新增一种格式或一种平台都只需新增一个类。
优点
- 分离抽象接口及其实现部分:抽象部分与实现部分可以各自独立演进,互不牵制。
- 优于多继承方案:桥接模式有时类似于多继承方案,但多继承方案违背了类的单一职责原则(即一个类只有一个变化的原因),复用性比较差,而且多继承结构中类的个数非常庞大;桥接模式是比多继承方案更好的解决方法。
- 提高系统的可扩充性:在两个变化维度中任意扩展一个维度,都不需要修改原有系统。
- 实现细节对客户透明:可以对用户隐藏实现细节。
缺点
- 桥接模式的引入会增加系统的理解与设计难度:由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程,对设计者的抽象能力要求更高。
- 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性——并非所有系统都天然存在两个正交的变化轴。
适用环境
在以下情况下可以使用桥接模式:
- 如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系;
- 抽象化角色和实现化角色可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合;
- 一个类存在两个独立变化的维度,且这两个维度都需要进行扩展;
- 虽然在系统中使用继承是没有问题的,但由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者;
- 对于那些不希望使用继承,或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
模式应用:Java AWT Peer 架构
一个真实的经典应用是 Java 的 GUI 体系:一个 Java 桌面软件总是带有所在操作系统的视感(LookAndFeel)。如果一个 Java 软件是在 Unix 系统上开发的,那么开发人员看到的是 Motif 用户界面的视感;在 Windows 上使用这个系统的用户看到的是 Windows 用户界面的视感;而在 Macintosh 上使用的用户看到的则是 Macintosh 用户界面的视感。Java 语言是通过所谓的 Peer 架构做到这一点的:Java 为 AWT 中的每一个 GUI 构件都提供了一个 Peer 构件,AWT 中的 Peer 架构就使用了桥接模式——GUI 构件(抽象)与各平台的原生 Peer 实现(实现)通过关联关系连接,从而做到"一次编写、处处运行"。
模式扩展:与适配器模式的联用
桥接模式和适配器模式用于设计的不同阶段:
- 桥接模式用于系统的初步设计:对于存在两个独立变化维度的类,可以将其分为抽象化和实现化两个角色,使它们可以分别进行变化;
- 在初步设计完成之后,当发现系统与已有类无法协同工作时,可以采用适配器模式;
- 但有时候在设计初期也需要考虑适配器模式,特别是那些涉及大量第三方应用接口的情况。
总结
- 桥接模式将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称作柄体(Handle and Body)模式或接口(Interface)模式。
- 桥接模式包含四个角色:抽象类中定义了一个实现类接口类型的对象并可以维护该对象;扩充抽象类扩充由抽象类定义的接口,它实现了在抽象类中定义的抽象业务方法,在扩充抽象类中可以调用在实现类接口中定义的业务方法;实现类接口定义了实现类的接口,它仅提供基本操作,而抽象类定义的接口可能会做更多更复杂的操作;具体实现类实现了实现类接口并具体实现它,在不同的具体实现类中提供基本操作的不同实现,在程序运行时,具体实现类对象将替换其父类对象,提供给客户端具体的业务操作方法。
- 在桥接模式中,抽象化与实现化脱耦,它们可以沿着各自的维度独立变化。
- 桥接模式的主要优点是分离抽象接口及其实现部分,是比多继承方案更好的解决方法;它还提高了系统的可扩充性,在两个变化维度中任意扩展一个维度都不需要修改原有系统;实现细节对客户透明,可以对用户隐藏实现细节。其主要缺点是增加系统的理解与设计难度,且识别出系统中两个独立变化的维度并不是一件容易的事情。
- 桥接模式的适用情况包括:需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系;抽象化角色和实现化角色可以以继承的方式独立扩展而互不影响;一个类存在两个独立变化的维度,且这两个维度都需要进行扩展;设计要求需要独立管理抽象化角色和具体化角色;不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统。
如果你希望进一步探索,可以参考仓库中的其他结构型模式文档:适配器模式、装饰模式、外观模式 等(总览见 structural_patterns/structural.rst),以及 code/Bridge 目录下完整的源码与 mingw5 编译产物。


