桥接模式

设计模式笔记:桥接模式 (Bridge Pattern)

一、一句话概括

抽象部分与它的实现部分相分离,使它们都可以独立地变化。它就像一座桥,连接了两个独立变化的“大陆”。

二、为什么需要它 (Why - The Pain Point)

让我们回到之前装饰模式中提到的“类爆炸”问题,但这次情况更复杂。假设我们要开发一个可以绘制不同形状(Shape) 的程序,这些形状可以在不同的操作系统(OS) 上用不同的 API 来绘制。

  • 第一个维度(抽象部分):形状。我们有 Circle, Square, Triangle 等。
  • 第二个维度(实现部分):绘制 API。我们有 WindowsAPI, LinuxAPI, MacOSAPI 等。

如果使用传统的继承方式,我们会怎么做?

1
2
3
4
5
6
7
8
9
10
11
12
// 继承方案,导致类爆炸
abstract class Shape { abstract void draw(); }

// Windows 平台的形状
class WindowsCircle extends Shape { /* 使用 Windows API 绘制圆形 */ }
class WindowsSquare extends Shape { /* 使用 Windows API 绘制正方形 */ }

// Linux 平台的形状
class LinuxCircle extends Shape { /* 使用 Linux API 绘制圆形 */ }
class LinuxSquare extends Shape { /* 使用 Linux API 绘制正方形 */ }

// ... 等等

痛点:维度灾难 (Dimensionality Curse)

这种设计的维护成本极高:

  1. 类爆炸:如果 M 种形状和 N 种绘制 API,就需要 M * N 个类。每增加一个新的形状(比如 Triangle),就需要为所有平台增加 N 个新类 (WindowsTriangle, LinuxTriangle…)。每增加一个新的平台(比如 Android),就需要为所有形状增加 M 个新类。
  2. 刚性耦合WindowsCircle 这个类将“圆形”这个抽象概念和“Windows API 绘制”这个具体实现紧紧地绑定在了一起。它们无法独立变化。

核心问题:当一个系统存在多个独立变化的维度时,如何避免使用继承导致类数量的笛卡尔积式增长,并让每个维度都能自由扩展?

三、它是什么 (What - The Solution)

桥接模式优雅地解决了这个问题,它的核心思想是用组合(或聚合)替代继承。它将原来纠缠在一起的两个维度彻底分开。

核心思想和角色:

  1. **抽象 (Abstraction)**:
    • 定义了高层控制逻辑的接口或抽象类。它不关心具体的实现细节。
    • 它内部持有一个实现部分接口(Implementor)的引用。 这是“桥”的关键。
    • 在我们的例子中,这就是 Shape 抽象类。
  2. **修正抽象 (Refined Abstraction)**:
    • 继承自 Abstraction,实现了更具体的业务逻辑。
    • 例如,Circle, Square 类。它们的 draw() 方法会定义如何绘制一个圆形或方形的逻辑,但具体的“画点”、“画线”操作会委托给持有的 Implementor 对象去完成。
  3. **实现者接口 (Implementor)**:
    • 定义了底层实现操作的接口。它与 Abstraction 的接口可以完全不同。
    • 它只关注提供原子性的、基础的操作。
    • 在我们的例子中,可以是一个 DrawingAPI 接口,包含 drawCircle(), drawSquare() 等方法。
  4. **具体实现者 (Concrete Implementor)**:
    • 实现了 Implementor 接口,提供了具体的实现。
    • 例如,WindowsDrawingAPI, LinuxDrawingAPI

工作流程:
客户端创建一个 RefinedAbstraction 的实例(如 Circle),并在创建时“注入”一个 ConcreteImplementor 的实例(如 WindowsDrawingAPI)。当客户端调用 circle.draw() 时:

  1. Circledraw() 方法被调用。
  2. Circle 的方法内部会调用它持有的 DrawingAPI 对象的相应方法(如 drawingAPI.drawCircle(...))。
  3. 因为注入的是 WindowsDrawingAPI,所以最终会执行 Windows 平台的绘制代码。

通过这种方式,“形状”和“绘制API”这两个维度被完全解耦,可以独立地进行扩展。

桥接模式 vs. 适配器模式

  • 意图不同
    • 适配器:是事后补救。当两个已有的、不兼容的接口需要协同工作时使用。
    • 桥接:是事前设计。在系统设计之初,主动将可能独立变化的维度分离,以应对未来的变化。
  • 结构相似但目的不同:两者都用到了组合/委托,但桥接模式的目的是分离抽象和实现,而适配器模式的目的是匹配两个不同的接口。

四、怎么做 (How - The Blueprint)

基本步骤(以形状和绘制 API 为例):

  1. **定义实现者接口 DrawingAPI**,包含原子操作如 drawCircle(), drawLine()
  2. 创建具体实现者 WindowsDrawingAPI, LinuxDrawingAPI,实现 DrawingAPI 接口。
  3. **定义抽象类 Shape**。它包含一个 DrawingAPI 的引用,并通过构造函数注入。
  4. 创建修正抽象类 Circle, Square,继承自 Shape。实现它们自己的 draw() 方法,并在方法内部调用 drawingAPI 的方法来完成实际绘制。
  5. 客户端根据需要,自由组合 ShapeDrawingAPI

Java 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// 1. 实现者接口 (Implementor)
interface DrawingAPI {
void drawCircle(double x, double y, double radius);
void drawSquare(double x, double y, double side);
}

// 2. 具体实现者 (Concrete Implementor)
class WindowsDrawingAPI implements DrawingAPI {
@Override
public void drawCircle(double x, double y, double radius) {
System.out.printf("WindowsAPI: Drawing a Circle at (%.1f, %.1f) with radius %.1f\n", x, y, radius);
}
@Override
public void drawSquare(double x, double y, double side) {
System.out.printf("WindowsAPI: Drawing a Square at (%.1f, %.1f) with side %.1f\n", x, y, side);
}
}

class LinuxDrawingAPI implements DrawingAPI {
@Override
public void drawCircle(double x, double y, double radius) {
System.out.printf("LinuxAPI: Drawing a Circle at (%.1f, %.1f) with radius %.1f\n", x, y, radius);
}
@Override
public void drawSquare(double x, double y, double side) {
System.out.printf("LinuxAPI: Drawing a Square at (%.1f, %.1f) with side %.1f\n", x, y, side);
}
}

// 3. 抽象 (Abstraction)
abstract class Shape {
// 包含一个实现者的引用,这就是“桥”
protected DrawingAPI drawingAPI;

protected Shape(DrawingAPI drawingAPI) {
this.drawingAPI = drawingAPI;
}

// 高层业务逻辑
public abstract void draw();
public abstract void resize(double percentage);
}

// 4. 修正抽象 (Refined Abstraction)
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 percentage) {
this.radius *= (1 + percentage / 100.0);
}
}

class Square extends Shape {
private double x, y, side;

public Square(double x, double y, double side, DrawingAPI drawingAPI) {
super(drawingAPI);
this.x = x;
this.y = y;
this.side = side;
}

@Override
public void draw() {
drawingAPI.drawSquare(x, y, side);
}

@Override
public void resize(double percentage) {
this.side *= (1 + percentage / 100.0);
}
}

// 5. 客户端
public class BridgePatternDemo {
public static void main(String[] args) {
// 可以自由组合“抽象”和“实现”
Shape windowsCircle = new Circle(10, 20, 5, new WindowsDrawingAPI());
Shape linuxSquare = new Square(5, 5, 10, new LinuxDrawingAPI());

windowsCircle.draw();
linuxSquare.draw();

System.out.println("\n--- Resizing Linux Square ---");
linuxSquare.resize(50); // 调整大小
linuxSquare.draw(); // 再次绘制
}
}

五、UML 类图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@startuml
skinparam classAttributeIconSize 0
hide empty members

' Implementor Hierarchy
interface DrawingAPI {
+ void drawCircle(x, y, radius)
+ void drawSquare(x, y, side)
}
class WindowsDrawingAPI implements DrawingAPI
class LinuxDrawingAPI implements DrawingAPI

' Abstraction Hierarchy
abstract class Shape {
# DrawingAPI drawingAPI
+ {abstract} void draw()
}
class Circle extends Shape {
- double x, y, radius
+ void draw()
}
class Square extends Shape {
- double x, y, side
+ void draw()
}

' The "Bridge"
' Shape HAS-A DrawingAPI (Aggregation)
Shape o-- "1" DrawingAPI

class BridgePatternDemo {}
BridgePatternDemo ..> Shape : uses
BridgePatternDemo ..> DrawingAPI : uses
@endumluml

六、优缺点分析

优点 (Pros):

  1. 分离抽象和实现:这是该模式的核心,它使得两部分可以独立演化,大大提高了系统的灵活性和可扩展性。
  2. 避免类爆炸:通过组合替代继承,将 M*N 的问题变成了 M+N 的问题。
  3. 符合开闭原则:可以非常方便地增加新的抽象(新形状)和新的实现(新平台),而无需修改现有代码。
  4. 细节对客户端隐藏:客户端在高层(Shape)工作,无需关心底层的平台实现细节。

缺点 (Cons):

  1. 增加系统复杂度:引入了额外的类和接口,如果系统只有一个固定的实现,使用桥接模式会显得过度设计。
  2. 理解难度:对于初学者来说,理解“抽象”和“实现”的分离可能需要一些时间。

七、在 Java 中的应用

  • JDBC (Java Database Connectivity): JDBC 是桥接模式的典范。

    • Abstraction: JDBC API 本身(如 Connection, Statement 等接口)。你的业务代码就是基于这些高层抽象编写的。
    • Implementor: java.sql.Driver 接口。
    • Concrete Implementor: 各个数据库厂商提供的驱动实现(MySQLDriver, OracleDriver 等)。

    你的应用程序(Abstraction)通过 JDBC API 与数据库交互,而 JDBC DriverManager 则通过加载不同的数据库驱动(Concrete Implementor)来“桥接”到具体的数据库。这样,你的代码无需改变,就可以从 MySQL 切换到 Oracle,只需更换驱动即可。

  • AWT (Abstract Window Toolkit) - 在旧版本中,AWT 使用了桥接模式来连接 Java 的 UI 组件和底层的操作系统对等体(peers)。

桥接模式是一种强大的架构模式,适用于那些需要在多个维度上进行扩展的复杂系统。准备好后,我们就可以进入今天的最后一组模式了。