桥接模式:抽象与实现分离的艺术
桥接模式,说白了就是「把抽象和实现拆开,让它们各自独立变化」。
我刚开始接触设计模式时,总觉得这玩意儿有点多余。直到有一次,我在一个图形绘制系统里被多重继承折磨得死去活来……嗯,从那以后,我对桥接模式就再也不敢小瞧了。
为什么需要桥接模式?
你想想看,假设我们要开发一个跨平台的绘图程序。抽象层面有「圆形」、「矩形」这些形状,实现层面有「Windows渲染」、「Linux渲染」这些平台。
如果不用桥接模式,最直接的做法就是:
- Windows圆形类
- Windows矩形类
- Linux圆形类
- Linux矩形类
这还只是2种形状×2个平台。如果再加一种形状,或者再加一个平台,类数量就会爆炸式增长。这就是典型的「类爆炸」问题。
核心思想:用组合代替继承。把「抽象」和「实现」放在两个独立的类层次结构中,通过组合关系连接它们。
桥接模式的结构
桥接模式有四个角色:
- Abstraction(抽象类):定义抽象接口,持有Implementor的引用
- RefinedAbstraction(扩展抽象类):扩展抽象接口
- Implementor(实现类接口):定义实现类的接口
- ConcreteImplementor(具体实现类):实现Implementor接口
我习惯把桥接模式理解成「遥控器」和「电视机」的关系。遥控器是抽象,电视机是实现。你换遥控器不影响电视机,换电视机也不影响遥控器——只要接口对得上就行。
SVG结构图:桥接模式核心逻辑
Java实现:图形绘制系统
咱们直接上代码。先定义实现接口:
// 实现类接口
public interface DrawingAPI {
void drawCircle(double x, double y, double radius);
void drawRectangle(double x, double y, double width, double height);
}
// 具体实现:Windows平台
public class WindowsDrawingAPI implements DrawingAPI {
@Override
public void drawCircle(double x, double y, double radius) {
System.out.println("Windows绘制圆形,中心(" + x + "," + y + "),半径" + radius);
}
@Override
public void drawRectangle(double x, double y, double width, double height) {
System.out.println("Windows绘制矩形,左上角(" + x + "," + y + "),宽" + width + "高" + height);
}
}
// 具体实现:Linux平台
public class LinuxDrawingAPI implements DrawingAPI {
@Override
public void drawCircle(double x, double y, double radius) {
System.out.println("Linux绘制圆形,中心(" + x + "," + y + "),半径" + radius);
}
@Override
public void drawRectangle(double x, double y, double width, double height) {
System.out.println("Linux绘制矩形,左上角(" + x + "," + y + "),宽" + width + "高" + height);
}
}
然后是抽象类:
// 抽象类
public abstract class Shape {
protected DrawingAPI drawingAPI;
protected Shape(DrawingAPI drawingAPI) {
this.drawingAPI = drawingAPI;
}
public abstract void draw(); // 委托给实现
public abstract void resize(double factor);
}
// 扩展抽象类:圆形
public class Circle extends Shape {
private double x, y, radius;
public Circle(double x, double y, double radius, DrawingAPI drawingAPI) {
super(drawingAPI);
this.x = x;
this.y = y;
this.radius = radius;
}
@Override
public void draw() {
drawingAPI.drawCircle(x, y, radius);
}
@Override
public void resize(double factor) {
radius *= factor;
}
}
// 扩展抽象类:矩形
public class Rectangle extends Shape {
private double x, y, width, height;
public Rectangle(double x, double y, double width, double height, DrawingAPI drawingAPI) {
super(drawingAPI);
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
@Override
public void draw() {
drawingAPI.drawRectangle(x, y, width, height);
}
@Override
public void resize(double factor) {
width *= factor;
height *= factor;
}
}
客户端使用:
public class Client {
public static void main(String[] args) {
Shape circle = new Circle(10, 20, 5, new WindowsDrawingAPI());
Shape rectangle = new Rectangle(30, 40, 100, 50, new LinuxDrawingAPI());
circle.draw(); // Windows绘制圆形...
rectangle.draw(); // Linux绘制矩形...
// 运行时切换实现
circle = new Circle(10, 20, 5, new LinuxDrawingAPI());
circle.draw(); // Linux绘制圆形...
}
}
C++实现:多重继承的替代方案
在C++里,很多人遇到这种场景第一反应就是多重继承。但多重继承带来的菱形继承、虚继承等问题,说实话挺让人头疼的。
桥接模式提供了一种更优雅的方案:
// 实现类接口
class DrawingAPI {
public:
virtual ~DrawingAPI() = default;
virtual void drawCircle(double x, double y, double radius) = 0;
virtual void drawRectangle(double x, double y, double width, double height) = 0;
};
// 具体实现
class WindowsDrawingAPI : public DrawingAPI {
public:
void drawCircle(double x, double y, double radius) override {
std::cout << "Windows绘制圆形" << std::endl;
}
void drawRectangle(double x, double y, double width, double height) override {
std::cout << "Windows绘制矩形" << std::endl;
}
};
class LinuxDrawingAPI : public DrawingAPI {
public:
void drawCircle(double x, double y, double radius) override {
std::cout << "Linux绘制圆形" << std::endl;
}
void drawRectangle(double x, double y, double width, double height) override {
std::cout << "Linux绘制矩形" << 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 Circle : public Shape {
double x, y, radius;
public:
Circle(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;
}
};
我的经验:在C++项目中,如果遇到「一个类需要从多个维度变化」的场景,我通常会先考虑桥接模式,而不是直接上多重继承。桥接模式让代码更容易测试,也更容易扩展。
JDBC驱动桥接:最经典的例子
说到桥接模式的实际应用,JDBC驱动绝对是最经典的例子。你想想看:
- 抽象部分:JDBC API(Connection、Statement、ResultSet)
- 实现部分:各种数据库驱动(MySQL驱动、Oracle驱动、PostgreSQL驱动)
你在代码里写的是JDBC标准接口,底层实际调用的是具体驱动的实现。这就是桥接模式——抽象和实现分离,各自独立变化。
// JDBC桥接模式示意
// 抽象:JDBC Connection
Connection conn = DriverManager.getConnection(url, user, password);
// 实际返回的是 MySQLConnection 或 OracleConnection
// 抽象:Statement
Statement stmt = conn.createStatement();
// 实际返回的是 MySQLStatement 或 OracleStatement
// 你只依赖抽象接口,不依赖具体实现
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
为什么JDBC要用桥接模式?因为数据库厂商太多了。如果不用桥接,每换一个数据库就得改一遍业务代码,那不得累死?
桥接模式 vs 适配器模式
这两个模式容易搞混,我简单说说区别:
| 对比维度 | 桥接模式 | 适配器模式 |
|---|---|---|
| 设计目的 | 分离抽象与实现,让它们独立变化 | 让不兼容的接口协同工作 |
| 使用时机 | 设计阶段,提前规划 | 后期维护,适配已有代码 |
| 关系方向 | 抽象依赖实现(组合) | 适配器包装被适配者 |
| 变化维度 | 抽象和实现两个维度独立变化 | 通常只适配一个维度 |
注意:桥接模式不是万能的。如果抽象和实现之间没有独立变化的可能,强行使用桥接模式只会增加复杂度。我曾经在一个小项目里过度设计,用了桥接模式,结果代码量翻了一倍,维护起来反而更麻烦。
什么时候用桥接模式?
我个人总结了几个适用场景:
- 多维度变化:抽象和实现都需要独立扩展。比如形状和平台、消息和发送方式
- 运行时切换:需要在运行时动态选择实现。比如根据配置文件选择数据库驱动
- 避免类爆炸:如果不用桥接,类数量会呈乘积增长
- 共享实现:多个抽象对象可以共享同一个实现对象
避坑指南
我曾经在一个项目中,把桥接模式和策略模式搞混了。桥接模式关注的是「抽象与实现的分离」,策略模式关注的是「算法的封装与替换」。虽然结构上有点像,但意图完全不同。
还有一次,我在一个只有两个变化维度的场景里用了桥接模式,结果发现直接用工厂模式更简单。所以我的建议是:
- 如果只有两个变化维度,先考虑工厂模式
- 如果变化维度超过两个,或者需要在运行时动态切换,再考虑桥接模式
- 不要为了用设计模式而用设计模式
桥接模式的核心价值,说白了就是「解耦」。把抽象和实现拆开,让它们各自独立演化。这个思想在很多地方都能看到,比如操作系统和硬件驱动、前端框架和渲染引擎……嗯,理解了桥接模式,你就掌握了一种应对复杂变化的思维方式。