C++设计模式之桥接模式

发布时间:2026/10/26 11:01:12
C++设计模式之桥接模式

前言


桥接模式(Bridge pattern)要解决的问题是「两个维度同时变化导致的类爆炸」。典型例子是一个遥控器要控制多种设备,同时又要有普通遥控器和高级遥控器两种形态。如果按继承来做,就会得到「普通电视遥控器」「高级电视遥控器」「普通收音机遥控器」「高级收音机遥控器」四个类;每增加一种设备或一种遥控器,类的数量按乘法增长。


桥接模式的做法是把这两个维度拆开:遥控器这一侧叫抽象部分(Abstraction),设备这一侧叫实现部分(Implementor)。抽象部分持有一个实现部分的指针,把具体工作委托给它。两个维度各自用继承扩展,类的数量从 M×N 降为 M+N。


常见的误解有两个。第一,把桥接和策略模式混为一谈——两者的类图很像,都是一个对象持有另一个接口,但意图不同:策略是运行期替换算法,桥接是让两个维度独立演化,通常在设计中就固定了组合关系。第二,以为只要用了「组合代替继承」就是桥接——桥接的关键是两个维度都能独立扩展,如果只有一侧会变,那只是普通的组合或策略。


本文用遥控器与设备的例子讲结构,再给出 C++17 的完整实现,最后与策略、适配器对比,说明什么时候值得引入桥接、什么时候是过度设计。


一、继承方案的问题


假设有 2 种设备(电视、收音机)和 2 种遥控器(普通、高级),用继承实现:


RemoteControl
├── TVRemote
│ ├── BasicTVRemote
│ └── AdvancedTVRemote
└── RadioRemote
├── BasicRadioRemote
└── AdvancedRadioRemote

4 个叶子类。如果设备增加到 5 种、遥控器增加到 3 种,就需要 15 个类;每加一种设备,所有遥控器形态都要再写一遍。这种结构的问题不是代码重复这么简单,而是两个本应独立的需求被绑死在一起:遥控器的「高级功能」和设备的「具体型号」在编译期就固定了组合。


桥接把继承树拆成两棵:


Abstraction(遥控器) Implementor(设备)
├── RemoteControl ├── TV
└── AdvancedRemote └── Radio
│
└── 持有 Implementor*

遥控器不认识「电视」或「收音机」,只认识Device接口;设备也不认识遥控器,只提供开关、音量等原语。组合发生在运行时,通过构造函数注入。


二、参与角色




角色职责本例



Abstraction定义高层的控制接口,持有 Implementor 的引用RemoteControl

RefinedAbstraction扩展抽象接口,不直接实现底层细节AdvancedRemote

Implementor定义实现类的接口,通常是纯虚函数Device

ConcreteImplementor实现具体平台/设备的操作TV、Radio

Client把具体的 Implementor 注入 Abstractionmain



关键点在于: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」。区别在意图和变化的方向。策略强调「同一个任务有多种算法,可以在运行时换」;桥接强调「有两套继承体系,各自扩展,互不依赖」。如果只有一个维度会变化,用策略就够了;硬套桥接会增加一层不必要的抽象。


适配器则是「事后补救」:两个已有接口不兼容,用一个包装类转换。桥接是「事先设计」:预见到两个维度都会扩展,提前拆开。把适配器当成桥接,或者把桥接当成适配器,都会让设计意图变得模糊。


五、代价与适用条件


桥接不是没有成本:



  1. 多了一层间接调用,RemoteControl的方法要转发给Device,调用链变长。编译器在能看到具体类型时可能内联,但通过基类指针的虚调用通常无法完全消除,是否可接受要看性能要求。

  2. 接口设计更难。Device提供哪些原语,决定了遥控器能实现什么功能;原语太少,高级遥控器做不了复杂操作;原语太多,又等于把具体设备的细节泄漏到抽象层。

  3. 增加了类的数量。虽然从乘法变成加法,但仍然是两套继承体系加一个组合关系,简单场景下反而更难读。


适用条件可以概括为:确实存在两个独立变化的维度,且两个维度都需要扩展。如果只有一个维度会变,或者变化是运行期的算法替换,优先考虑策略;如果只是想把接口和实现分开,可以先问「实现是否真的会独立演化」,答案是否定的话,普通虚函数加继承就够了。


常见坑点


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. 过度设计❌ 只有一种设备和一种遥控器,却引入四个角色和两层继承。 ✅ 先写简单实现,等第二个维度真的出现再重构;模式是应对变化的手段,不是目的。


总结




角色本例职责



AbstractionRemoteControl高层控制接口,持有设备

RefinedAbstractionAdvancedRemote扩展功能,仍通过设备接口实现

ImplementorDevice定义开关、音量等原语

ConcreteImplementorTV、Radio具体设备的实现

Clientmain组装遥控器与设备



桥接模式的核心不是「组合代替继承」这句口号,而是识别出两个会独立演化的维度,并把它们之间的依赖从编译期移到运行期的组合。识别错了维度,或者只有一个维度会变,引入桥接只会增加抽象层。判断标准很实际:新增一种设备时,是否需要修改遥控器代码?新增一种遥控器时,是否需要修改设备代码?两个答案都是「不需要」,桥接才真正发挥作用。