桥接模式
桥接模式
Rif设计模式笔记:桥接模式 (Bridge Pattern)
一、一句话概括
将抽象部分与它的实现部分相分离,使它们都可以独立地变化。它就像一座桥,连接了两个独立变化的“大陆”。
二、为什么需要它 (Why - The Pain Point)
让我们回到之前装饰模式中提到的“类爆炸”问题,但这次情况更复杂。假设我们要开发一个可以绘制不同形状(Shape) 的程序,这些形状可以在不同的操作系统(OS) 上用不同的 API 来绘制。
- 第一个维度(抽象部分):形状。我们有
Circle,Square,Triangle等。 - 第二个维度(实现部分):绘制 API。我们有
WindowsAPI,LinuxAPI,MacOSAPI等。
如果使用传统的继承方式,我们会怎么做?
1 | // 继承方案,导致类爆炸 |
痛点:维度灾难 (Dimensionality Curse)
这种设计的维护成本极高:
- 类爆炸:如果 M 种形状和 N 种绘制 API,就需要
M * N个类。每增加一个新的形状(比如Triangle),就需要为所有平台增加 N 个新类 (WindowsTriangle,LinuxTriangle…)。每增加一个新的平台(比如Android),就需要为所有形状增加 M 个新类。 - 刚性耦合:
WindowsCircle这个类将“圆形”这个抽象概念和“Windows API 绘制”这个具体实现紧紧地绑定在了一起。它们无法独立变化。
核心问题:当一个系统存在多个独立变化的维度时,如何避免使用继承导致类数量的笛卡尔积式增长,并让每个维度都能自由扩展?
三、它是什么 (What - The Solution)
桥接模式优雅地解决了这个问题,它的核心思想是用组合(或聚合)替代继承。它将原来纠缠在一起的两个维度彻底分开。
核心思想和角色:
- **抽象 (Abstraction)**:
- 定义了高层控制逻辑的接口或抽象类。它不关心具体的实现细节。
- 它内部持有一个实现部分接口(
Implementor)的引用。 这是“桥”的关键。 - 在我们的例子中,这就是
Shape抽象类。
- **修正抽象 (Refined Abstraction)**:
- 继承自
Abstraction,实现了更具体的业务逻辑。 - 例如,
Circle,Square类。它们的draw()方法会定义如何绘制一个圆形或方形的逻辑,但具体的“画点”、“画线”操作会委托给持有的Implementor对象去完成。
- 继承自
- **实现者接口 (Implementor)**:
- 定义了底层实现操作的接口。它与
Abstraction的接口可以完全不同。 - 它只关注提供原子性的、基础的操作。
- 在我们的例子中,可以是一个
DrawingAPI接口,包含drawCircle(),drawSquare()等方法。
- 定义了底层实现操作的接口。它与
- **具体实现者 (Concrete Implementor)**:
- 实现了
Implementor接口,提供了具体的实现。 - 例如,
WindowsDrawingAPI,LinuxDrawingAPI。
- 实现了
工作流程:
客户端创建一个 RefinedAbstraction 的实例(如 Circle),并在创建时“注入”一个 ConcreteImplementor 的实例(如 WindowsDrawingAPI)。当客户端调用 circle.draw() 时:
Circle的draw()方法被调用。Circle的方法内部会调用它持有的DrawingAPI对象的相应方法(如drawingAPI.drawCircle(...))。- 因为注入的是
WindowsDrawingAPI,所以最终会执行 Windows 平台的绘制代码。
通过这种方式,“形状”和“绘制API”这两个维度被完全解耦,可以独立地进行扩展。
桥接模式 vs. 适配器模式
- 意图不同:
- 适配器:是事后补救。当两个已有的、不兼容的接口需要协同工作时使用。
- 桥接:是事前设计。在系统设计之初,主动将可能独立变化的维度分离,以应对未来的变化。
- 结构相似但目的不同:两者都用到了组合/委托,但桥接模式的目的是分离抽象和实现,而适配器模式的目的是匹配两个不同的接口。
四、怎么做 (How - The Blueprint)
基本步骤(以形状和绘制 API 为例):
- **定义实现者接口
DrawingAPI**,包含原子操作如drawCircle(),drawLine()。 - 创建具体实现者
WindowsDrawingAPI,LinuxDrawingAPI,实现DrawingAPI接口。 - **定义抽象类
Shape**。它包含一个DrawingAPI的引用,并通过构造函数注入。 - 创建修正抽象类
Circle,Square,继承自Shape。实现它们自己的draw()方法,并在方法内部调用drawingAPI的方法来完成实际绘制。 - 客户端根据需要,自由组合
Shape和DrawingAPI。
Java 示例代码
1 | // 1. 实现者接口 (Implementor) |
五、UML 类图
1 | @startuml |
六、优缺点分析
优点 (Pros):
- 分离抽象和实现:这是该模式的核心,它使得两部分可以独立演化,大大提高了系统的灵活性和可扩展性。
- 避免类爆炸:通过组合替代继承,将
M*N的问题变成了M+N的问题。 - 符合开闭原则:可以非常方便地增加新的抽象(新形状)和新的实现(新平台),而无需修改现有代码。
- 细节对客户端隐藏:客户端在高层(
Shape)工作,无需关心底层的平台实现细节。
缺点 (Cons):
- 增加系统复杂度:引入了额外的类和接口,如果系统只有一个固定的实现,使用桥接模式会显得过度设计。
- 理解难度:对于初学者来说,理解“抽象”和“实现”的分离可能需要一些时间。
七、在 Java 中的应用
JDBC (Java Database Connectivity): JDBC 是桥接模式的典范。
- Abstraction: JDBC API 本身(如
Connection,Statement等接口)。你的业务代码就是基于这些高层抽象编写的。 - Implementor:
java.sql.Driver接口。 - Concrete Implementor: 各个数据库厂商提供的驱动实现(
MySQLDriver,OracleDriver等)。
你的应用程序(
Abstraction)通过 JDBC API 与数据库交互,而 JDBCDriverManager则通过加载不同的数据库驱动(Concrete Implementor)来“桥接”到具体的数据库。这样,你的代码无需改变,就可以从 MySQL 切换到 Oracle,只需更换驱动即可。- Abstraction: JDBC API 本身(如
AWT (Abstract Window Toolkit) - 在旧版本中,AWT 使用了桥接模式来连接 Java 的 UI 组件和底层的操作系统对等体(peers)。
桥接模式是一种强大的架构模式,适用于那些需要在多个维度上进行扩展的复杂系统。准备好后,我们就可以进入今天的最后一组模式了。