前言
桥接模式(Bridge pattern)要解决的问题是「两个维度同时变化导致的类爆炸」。典型例子是一个遥控器要控制多种设备,同时又要有普通遥控器和高级遥控器两种形态。如果按继承来做,就会得到「普通电视遥控器」「高级电视遥控器」「普通收音机遥控器」「高级收音机遥控器」四个类;每增加一种设备或一种遥控器,类的数量按乘法增长。
桥接模式的做法是把这两个维度拆开:遥控器这一侧叫抽象部分(Abstraction),设备这一侧叫实现部分(Implementor)。抽象部分持有一个实现部分的指针,把具体工作委托给它。两个维度各自用继承扩展,类的数量从 M×N 降为 M+N。
常见的误解有两个。第一,把桥接和策略模式混为一谈——两者的类图很像,都是一个对象持有另一个接口,但意图不同:策略是运行期替换算法,桥接是让两个维度独立演化,通常在设计中就固定了组合关系。第二,以为只要用了「组合代替继承」就是桥接——桥接的关键是两个维度都能独立扩展,如果只有一侧会变,那只是普通的组合或策略。
本文用遥控器与设备的例子讲结构,再给出 C++17 的完整实现,最后与策略、适配器对比,说明什么时候值得引入桥接、什么时候是过度设计。
一、继承方案的问题
假设有 2 种设备(电视、收音机)和 2 种遥控器(普通、高级),用继承实现:
RemoteControl
├── TVRemote
│ ├── BasicTVRemote
│ └── AdvancedTVRemote
└── RadioRemote
├── BasicRadioRemote
└── AdvancedRadioRemote4 个叶子类。如果设备增加到 5 种、遥控器增加到 3 种,就需要 15 个类;每加一种设备,所有遥控器形态都要再写一遍。这种结构的问题不是代码重复这么简单,而是两个本应独立的需求被绑死在一起:遥控器的「高级功能」和设备的「具体型号」在编译期就固定了组合。
桥接把继承树拆成两棵:
Abstraction(遥控器) Implementor(设备)
├── RemoteControl ├── TV
└── AdvancedRemote └── Radio
│
└── 持有 Implementor*遥控器不认识「电视」或「收音机」,只认识Device接口;设备也不认识遥控器,只提供开关、音量等原语。组合发生在运行时,通过构造函数注入。
二、参与角色
| 角色 | 职责 | 本例 |
|---|
| Abstraction | 定义高层的控制接口,持有 Implementor 的引用 | RemoteControl |
| RefinedAbstraction | 扩展抽象接口,不直接实现底层细节 | AdvancedRemote |
| Implementor | 定义实现类的接口,通常是纯虚函数 | Device |
| ConcreteImplementor | 实现具体平台/设备的操作 | TV、Radio |
| Client | 把具体的 Implementor 注入 Abstraction | main |
关键点在于:Abstraction 中的方法不直接做事,而是把调用转发给 Implementor。RemoteControl::volumeUp()不知道音量是怎么调的,它只调用device_->setVolume(...)。这样,新增设备只要实现Device,新增遥控器只要继承RemoteControl,两边互不影响。
三、完整实现
// 适用于 C++17
#include <algorithm>
#include <iostream>
#include <memory>
#include <string>
#include <utility>
// 实现部分:设备
class Device {
public:
virtual ~Device() = default;
virtual void enable() = 0;
virtual void disable() = 0;
virtual void setVolume(int percent) = 0;
virtual int volume() const = 0;
virtual std::string name() const = 0;
};
class TV : public Device {
public:
void enable() override { on_ = true; }
void disable() override { on_ = false; }
void setVolume(int percent) override {
volume_ = std::max(0, std::min(100, percent));
}
int volume() const override { return volume_; }
std::string name() const override { return on_ ? "TV(on)" : "TV(off)"; }
private:
bool on_ = false;
int volume_ = 30;
};
class Radio : public Device {
public:
void enable() override { on_ = true; }
void disable() override { on_ = false; }
void setVolume(int percent) override {
volume_ = std::max(0, std::min(100, percent));
}
int volume() const override { return volume_; }
std::string name() const override { return on_ ? "Radio(on)" : "Radio(off)"; }
private:
bool on_ = false;
int volume_ = 50;
};设备接口的析构函数必须是virtual。客户端通过std::unique_ptr<Device>持有对象,销毁时经基类指针调用析构函数;如果基类析构不是虚函数,通过基类指针delete派生对象是未定义行为(undefined behavior, UB)。
接着是抽象部分:
// 适用于 C++17
class RemoteControl {
public:
explicit RemoteControl(std::unique_ptr<Device> device)
: device_(std::move(device)) {}
virtual ~RemoteControl() = default;
RemoteControl(const RemoteControl&) = delete;
RemoteControl& operator=(const RemoteControl&) = delete;
void togglePower() {
if (on_) {
device_->disable();
on_ = false;
} else {
device_->enable();
on_ = true;
}
}
void volumeUp() {
device_->setVolume(std::min(100, device_->volume() + 10));
}
void volumeDown() {
device_->setVolume(std::max(0, device_->volume() - 10));
}
virtual std::string status() const {
return device_->name() + " vol=" + std::to_string(device_->volume());
}
protected:
Device& device() { return *device_; }
private:
std::unique_ptr<Device> device_;
bool on_ = false;
};
class AdvancedRemote : public RemoteControl {
public:
using RemoteControl::RemoteControl; // 继承构造函数
void mute() {
device().setVolume(0);
}
std::string status() const override {
return "Advanced: " + RemoteControl::status();
}
};RemoteControl持有std::unique_ptr<Device>,表示它对设备的独占所有权。拷贝构造被删除,因为unique_ptr不可拷贝;如果确实需要复制遥控器,应当想清楚复制后是否共享同一个设备,这属于业务语义,不能靠默认行为决定。
AdvancedRemote用using RemoteControl::RemoteControl;继承基类构造函数,这样AdvancedRemote r(std::make_unique<TV>());可以直接构造,不必手写转发构造函数。继承来的构造函数只初始化基类部分,AdvancedRemote没有额外的数据成员,所以够用;如果它有自己的成员,需要在类内给默认初始化,或另写构造函数。
device()是 protected 的访问器,供派生类调用底层原语。mute()直接用setVolume(0),而不是调用volumeDown()若干次,避免依赖步长。
使用
// 适用于 C++17
int main() {
RemoteControl basic(std::make_unique<TV>());
basic.togglePower();
basic.volumeUp();
std::cout << basic.status() << '\n';
AdvancedRemote advanced(std::make_unique<Radio>());
advanced.togglePower();
advanced.mute();
std::cout << advanced.status() << '\n';
return 0;
}客户端只依赖RemoteControl、AdvancedRemote、Device及其实现。要新增一种设备(例如Projector),只需实现Device接口;要新增一种遥控器(例如带定时功能的TimedRemote),只需继承RemoteControl。两边都不需要修改对方的代码,类的数量按加法增长。
四、与策略、适配器的区别
| 模式 | 目的 | 结构 | 变化维度 |
|---|
| 桥接 | 让抽象与实现独立演化 | 抽象持有实现接口 | 两个维度,设计期确定 |
| 策略 | 运行期替换算法 | 上下文持有策略接口 | 一个维度,运行期切换 |
| 适配器 | 让不兼容接口协同工作 | 包装已有对象,转换接口 | 不改被适配者 |
| 抽象工厂 | 创建一系列相关对象 | 工厂接口 + 具体工厂 | 产品族 |
桥接和策略在代码层面几乎一样:都是「A 持有 B 的指针,调用转发给 B」。区别在意图和变化的方向。策略强调「同一个任务有多种算法,可以在运行时换」;桥接强调「有两套继承体系,各自扩展,互不依赖」。如果只有一个维度会变化,用策略就够了;硬套桥接会增加一层不必要的抽象。
适配器则是「事后补救」:两个已有接口不兼容,用一个包装类转换。桥接是「事先设计」:预见到两个维度都会扩展,提前拆开。把适配器当成桥接,或者把桥接当成适配器,都会让设计意图变得模糊。
五、代价与适用条件
桥接不是没有成本:
- 多了一层间接调用,
RemoteControl的方法要转发给Device,调用链变长。编译器在能看到具体类型时可能内联,但通过基类指针的虚调用通常无法完全消除,是否可接受要看性能要求。 - 接口设计更难。
Device提供哪些原语,决定了遥控器能实现什么功能;原语太少,高级遥控器做不了复杂操作;原语太多,又等于把具体设备的细节泄漏到抽象层。 - 增加了类的数量。虽然从乘法变成加法,但仍然是两套继承体系加一个组合关系,简单场景下反而更难读。
适用条件可以概括为:确实存在两个独立变化的维度,且两个维度都需要扩展。如果只有一个维度会变,或者变化是运行期的算法替换,优先考虑策略;如果只是想把接口和实现分开,可以先问「实现是否真的会独立演化」,答案是否定的话,普通虚函数加继承就够了。
常见坑点
1. Implementor 析构不是虚函数❌class Device { public: ~Device() = default; ... };,通过Device*删除派生对象是 UB。 ✅virtual ~Device() = default;。
2. 抽象部分拷贝导致所有权混乱❌RemoteControl持有裸指针,默认拷贝构造让两个遥控器指向同一设备,析构时重复释放。 ✅ 用unique_ptr并禁用拷贝,或明确实现共享语义(shared_ptr)。
3. 在抽象部分直接 new 具体实现❌RemoteControl() : device_(new TV) {}—— 抽象重新耦合到具体设备,桥接失去意义。 ✅ 由构造函数参数注入,客户端决定组合。
4. 把桥接写成策略❌ 只有一个「算法」维度,却硬拆出抽象与实现两套继承。 ✅ 单维度用策略;只有两个维度都独立变化时才用桥接。
5. 实现接口泄漏具体类型❌ 在RemoteControl里dynamic_cast<TV*>(device_.get())做特判。 ✅ 把差异抽象成Device的虚函数;必须特判说明接口设计不完整。
6. 派生类忘记转发或状态不同步❌AdvancedRemote::mute()直接改自己的成员,没有调用device().setVolume(0)。 ✅ 所有底层操作都通过 Implementor 接口完成。
7. 用裸指针且不写所有权❌Device* device_;构造时赋值,析构时不释放,或释放了但调用方还在用。 ✅std::unique_ptr<Device>表达独占;需要共享时用std::shared_ptr并写清生命周期。
8. 过度设计❌ 只有一种设备和一种遥控器,却引入四个角色和两层继承。 ✅ 先写简单实现,等第二个维度真的出现再重构;模式是应对变化的手段,不是目的。
总结
| 角色 | 本例 | 职责 |
|---|
| Abstraction | RemoteControl | 高层控制接口,持有设备 |
| RefinedAbstraction | AdvancedRemote | 扩展功能,仍通过设备接口实现 |
| Implementor | Device | 定义开关、音量等原语 |
| ConcreteImplementor | TV、Radio | 具体设备的实现 |
| Client | main | 组装遥控器与设备 |
桥接模式的核心不是「组合代替继承」这句口号,而是识别出两个会独立演化的维度,并把它们之间的依赖从编译期移到运行期的组合。识别错了维度,或者只有一个维度会变,引入桥接只会增加抽象层。判断标准很实际:新增一种设备时,是否需要修改遥控器代码?新增一种遥控器时,是否需要修改设备代码?两个答案都是「不需要」,桥接才真正发挥作用。