桥接模式:模式动机与定义
你有没有遇到过这种场景?
一个类,因为两个维度的变化,膨胀得像个气球。比如一个Window类,既要支持不同的操作系统(Windows、Linux、macOS),又要支持不同的窗口风格(普通、对话框、全屏)。
我早年接手过一个图形库,里面就是这种写法。每个新风格都要为每个操作系统写一遍实现。那代码,啧啧,又臭又长。后来我重构时用了桥接模式,才把这两个维度彻底解耦。
说白了,桥接模式就是解决「多维变化」问题的利器。它的核心思想很简单:将抽象部分与它的实现部分分离,使它们都可以独立地变化。
你想想看,如果不用桥接,你会怎么处理?
- 用继承?那得有多少子类?2个维度 × 3种变化 = 6个类。3个维度 × 5种变化 = 15个类。指数级爆炸。
- 用if-else?那代码里全是switch-case,改一个地方得动整个类。
桥接模式就是告诉你:别把抽象和实现绑死。让它们通过组合的方式协作,而不是继承。
角色与结构
桥接模式有四个核心角色。我习惯这么记:
| 角色 | 名称 | 职责 |
|---|---|---|
| Abstraction | 抽象类 | 定义抽象接口,持有Implementor的引用 |
| RefinedAbstraction | 扩展抽象类 | 对Abstraction进行扩展,增加新功能 |
| Implementor | 实现类接口 | 定义实现类的接口,通常比Abstraction更底层 |
| ConcreteImplementor | 具体实现类 | 实现Implementor接口,提供具体操作 |
嗯,这里要注意:Abstraction和Implementor之间是组合关系,不是继承。这是桥接模式的关键。
下面这张图,我画了桥接模式的核心结构。你看一眼就明白了。
你看,Abstraction 通过组合持有 Implementor 的引用。这样,抽象和实现就可以各自独立变化了。
C++代码实现
光说不练假把式。咱们直接上代码。我以一个跨平台的绘图程序为例。
先定义实现类接口:
// 实现类接口
class DrawingAPI {
public:
virtual ~DrawingAPI() = default;
virtual void drawCircle(double x, double y, double radius) = 0;
};
然后是两个具体实现:
class DrawingAPI1 : public DrawingAPI {
public:
void drawCircle(double x, double y, double radius) override {
std::cout << "API1: 在 (" << x << ", " << y
<< ") 画圆,半径 " << radius << std::endl;
}
};
class DrawingAPI2 : public DrawingAPI {
public:
void drawCircle(double x, double y, double radius) override {
std::cout << "API2: 在 (" << x << ", " << y
<< ") 画圆,半径 " << radius << std::endl;
}
};
接下来是抽象类:
// 抽象类
class Shape {
protected:
DrawingAPI* drawingAPI; // 桥接的关键:持有实现类的指针
public:
Shape(DrawingAPI* api) : drawingAPI(api) {}
virtual ~Shape() = default;
virtual void draw() = 0; // 抽象方法
virtual void resize(double factor) = 0; // 另一个抽象方法
};
扩展抽象类:
class CircleShape : public Shape {
private:
double x, y, radius;
public:
CircleShape(double x, double y, double r, DrawingAPI* api)
: Shape(api), x(x), y(y), radius(r) {}
void draw() override {
drawingAPI->drawCircle(x, y, radius);
}
void resize(double factor) override {
radius *= factor;
}
};
使用起来是这样的:
int main() {
DrawingAPI1 api1;
DrawingAPI2 api2;
CircleShape circle1(1, 2, 3, &api1);
CircleShape circle2(5, 7, 11, &api2);
circle1.draw(); // 输出:API1: 在 (1, 2) 画圆,半径 3
circle2.draw(); // 输出:API2: 在 (5, 7) 画圆,半径 11
circle1.resize(2);
circle1.draw(); // 输出:API1: 在 (1, 2) 画圆,半径 6
return 0;
}
解决多维变化问题
回到开头的问题。为什么桥接模式能解决多维变化?
因为每个维度都变成了一个独立的类层次。你想想看:
- 「形状」是一个维度:圆形、矩形、三角形……
- 「绘制方式」是另一个维度:OpenGL、DirectX、软件渲染……
如果没有桥接,你要写 OpenGLCircle、DirectXCircle、OpenGLRect…… 每新增一个形状或绘制方式,都要新增一堆类。
用了桥接之后:
- 新增形状?继承
Shape就行。 - 新增绘制方式?继承
DrawingAPI就行。 - 两者互不干扰。
我在项目中遇到过类似的需求:一个消息通知系统,既要支持不同的消息类型(文本、图片、视频),又要支持不同的发送渠道(邮件、短信、推送)。当时就是用桥接模式搞定的。后来产品经理说要加一个「语音消息」类型,我只加了一个类就完事了。要是用继承,那得改多少地方啊。
最后总结一下:
- 桥接模式适用于多维变化的场景。
- 它通过组合代替继承,把抽象和实现解耦。
- 代码结构清晰,扩展起来很舒服。
嗯,桥接模式就讲到这里。你可以在自己的项目里试试,特别是那些「一个类要处理多种变化」的地方。