10. 桥接模式:分离抽象与实现的艺术
桥接模式,说实话,是我在项目中用得最频繁的结构型模式之一。为什么?因为它解决了一个很实在的问题——当你的抽象和实现都需要独立变化时,继承体系会迅速膨胀到失控。我见过太多项目,一开始用继承挺爽,后来改一个需求要动十几个类,那叫一个酸爽。
模式动机与定义
先聊聊动机。你想想看,假设我们要开发一个图形绘制系统。有圆形、矩形、三角形这些形状,每种形状又可以用不同的方式绘制——比如用OpenGL、DirectX或者软件渲染。
如果用继承,你会得到什么?
// 糟糕的继承设计
class CircleOpenGL : public Shape { ... };
class CircleDirectX : public Shape { ... };
class CircleSoftware : public Shape { ... };
class RectOpenGL : public Shape { ... };
class RectDirectX : public Shape { ... };
class RectSoftware : public Shape { ... };
// ... 每增加一种形状或绘制方式,类数量就翻倍
嗯,这里要注意。3种形状 × 3种绘制方式 = 9个类。如果变成10种形状、5种绘制方式呢?50个类。这还不算完,如果绘制方式需要新增参数,你得改多少个类?
桥接模式的核心思想很简单:把抽象和实现解耦,让它们可以独立变化。说白了,就是把「形状」和「绘制方式」这两个维度拆开,各自维护自己的继承体系,然后用一个「桥」把它们连起来。
定义:桥接模式(Bridge Pattern)将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种结构型设计模式,通过组合关系替代继承关系,从而降低抽象和实现之间的耦合度。
角色与结构
桥接模式涉及四个核心角色,我习惯这么记:
| 角色 | 名称 | 职责 |
|---|---|---|
| Abstraction | 抽象类 | 定义抽象接口,持有Implementor的引用 |
| RefinedAbstraction | 扩展抽象类 | 扩展Abstraction,增加新的业务逻辑 |
| Implementor | 实现类接口 | 定义实现类的接口,通常比Abstraction更底层 |
| ConcreteImplementor | 具体实现类 | 实现Implementor接口,提供具体操作 |
结构上,Abstraction「桥接」到Implementor,而不是通过继承绑定。这样两边各玩各的,互不干扰。
个人经验:我在重构一个老旧图形引擎时,发现绘制逻辑和形状逻辑完全耦合在一起。每次加新形状都要复制粘贴绘制代码。用桥接模式重构后,绘制方式独立成一个接口,新形状只需要关心自己的几何数据,绘制的事情交给具体的Implementor去处理。代码量减少了40%,而且测试起来也方便多了。
下面这张图展示了桥接模式的核心结构:
C++代码实现
直接上代码。先定义实现接口:
// 实现类接口 - 绘制方式
class Renderer {
public:
virtual ~Renderer() = default;
virtual void renderCircle(float x, float y, float radius) = 0;
virtual void renderRect(float x, float y, float width, float height) = 0;
};
然后是两个具体实现:
class OpenGLRenderer : public Renderer {
public:
void renderCircle(float x, float y, float radius) override {
std::cout << "[OpenGL] 绘制圆形 at (" << x << ", " << y
<< "), 半径: " << radius << std::endl;
}
void renderRect(float x, float y, float width, float height) override {
std::cout << "[OpenGL] 绘制矩形 at (" << x << ", " << y
<< "), 尺寸: " << width << "x" << height << std::endl;
}
};
class DirectXRenderer : public Renderer {
public:
void renderCircle(float x, float y, float radius) override {
std::cout << "[DirectX] 绘制圆形 at (" << x << ", " << y
<< "), 半径: " << radius << std::endl;
}
void renderRect(float x, float y, float width, float height) override {
std::cout << "[DirectX] 绘制矩形 at (" << x << ", " << y
<< "), 尺寸: " << width << "x" << height << std::endl;
}
};
抽象类持有实现接口的引用:
class Shape {
protected:
Renderer* renderer_; // 桥接的关键:组合而非继承
public:
explicit Shape(Renderer* renderer) : renderer_(renderer) {
if (!renderer_) throw std::invalid_argument("Renderer不能为空");
}
virtual ~Shape() = default;
virtual void draw() = 0;
// 运行时切换实现
void setRenderer(Renderer* renderer) { renderer_ = renderer; }
};
具体形状类:
class Circle : public Shape {
float x_, y_, radius_;
public:
Circle(Renderer* renderer, float x, float y, float r)
: Shape(renderer), x_(x), y_(y), radius_(r) {}
void draw() override {
renderer_->renderCircle(x_, y_, radius_);
}
void resize(float factor) { radius_ *= factor; }
};
class Rectangle : public Shape {
float x_, y_, width_, height_;
public:
Rectangle(Renderer* renderer, float x, float y, float w, float h)
: Shape(renderer), x_(x), y_(y), width_(w), height_(h) {}
void draw() override {
renderer_->renderRect(x_, y_, width_, height_);
}
};
使用示例:
int main() {
OpenGLRenderer opengl;
DirectXRenderer directx;
// 同一个形状,不同的绘制方式
Circle circle1(&opengl, 100, 100, 50);
Circle circle2(&directx, 200, 200, 30);
circle1.draw(); // [OpenGL] 绘制圆形...
circle2.draw(); // [DirectX] 绘制圆形...
// 运行时切换实现
circle1.setRenderer(&directx);
circle1.draw(); // 现在用DirectX绘制了
return 0;
}
避坑指南:我曾经在项目中犯过一个错误——把Renderer的创建逻辑也放到了Shape里。结果Shape既要管形状,又要管渲染器的生命周期,耦合度又上去了。记住:桥接模式只负责连接,不负责创建。Renderer应该由外部传入,Shape只负责使用。
分离抽象与实现
桥接模式最核心的价值,就是让抽象和实现可以各自独立演化。我总结了几点实践经验:
- 什么时候用?当你发现一个类有两个独立变化的维度,而且用继承会导致类爆炸时。比如形状×绘制方式、消息×发送渠道、设备×操作系统。
- 什么时候别用?如果只有一个维度会变化,或者变化频率极低,用桥接反而增加了复杂度。别为了用模式而用模式。
- 性能考量:桥接模式引入了一层间接调用,对性能有微小影响。但在99%的业务场景中,这点开销可以忽略不计。如果你在做实时渲染引擎,可能需要权衡。
我的习惯:在设计初期,我会先问自己一个问题:「这个类的哪些部分可能会变化?」如果答案是「多个维度」,我就会考虑桥接模式。另外,我建议把Implementor设计得尽量稳定,因为它是被多个Abstraction依赖的。一旦Implementor接口频繁变动,所有Abstraction都得跟着改,那就得不偿失了。
桥接模式还有一个容易被忽略的好处——它让单元测试变得简单。你可以为Renderer写一个Mock实现,然后在测试Shape时注入Mock,验证Shape是否正确调用了Renderer的方法。这在继承体系中很难做到,因为实现和抽象是绑死的。
嗯,总结一下。桥接模式不是什么高深莫测的东西,它就是一个很朴素的道理:当事情变得复杂时,拆开来各自处理,然后用一个桥连起来。你在项目中遇到类似的问题时,不妨试试这个思路。
公众号:蓝海资料掘金营,微信deep3321