深入浅出设计模式【七、桥接模式】
一、桥接模式介绍
桥接模式处理的是一个类存在多个维度的变化。如果使用继承,会导致类的数量急剧增长(M*N个类),且扩展困难(增加一个维度或一个变化都需要修改很多代码)。
该模式建议将其中一个维度抽取出来,变成独立的层次结构,并在原始类中引用这个新层次的对象。这样,原始类中的所有操作都可以委托给这个引用的对象来完成。这种分离使得两个层次可以独立扩展,而不会相互影响。
二、核心概念与意图
-
核心概念:
- 抽象 (Abstraction): 定义抽象接口,其中包含一个对实现对象的引用。它通常定义了一些基于底层实现的高层控制逻辑。
- 精化抽象 (Refined Abstraction): 是抽象的子类,扩展或改变了抽象定义的接口。
- 实现者 (Implementor): 定义实现类的接口。这个接口不一定要与抽象的接口完全一致;实际上,这两个接口可以完全不同。一般来讲,实现者接口只提供基本操作。
- 具体实现 (Concrete Implementor): 实现实现者接口,提供具体的操作实现。
-
意图:
- 将抽象部分与它的实现部分分离,使它们都可以独立地变化。
- 使用“组合/聚合”关系代替继承关系,从而获得比继承更好的灵活性。
- 避免在多维度变化时,使用继承导致的类层次结构过于复杂(“类爆炸”)的问题。
三、适用场景剖析
桥接模式在以下场景中非常有效:
- 一个类存在多个独立变化的维度,且这些维度都需要进行扩展时: 例如,一个图形类,其形状(圆形、方形)和颜色(红色、蓝色)都是可以独立变化的维度。使用桥接模式,形状是抽象,颜色是实现。
- 需要在抽象和实现之间提供更多的灵活性时: 例如,希望在运行时动态地切换实现(如更换数据库驱动)。
- 不希望使用继承,或因为多层继承导致类的数量急剧增加时: 继承是一种强耦合的静态关系,桥接模式通过组合提供了更松散的耦合。
- 需要对客户端隐藏实现细节时: 客户端只依赖于抽象接口,完全不知道实现的细节,这符合“依赖倒置原则”。
四、UML 类图解析
以下Mermaid类图清晰地展示了桥接模式的结构和角色间的关系,它完美体现了两个独立变化的层次:
Abstraction: 抽象部分的基类。它持有一个对Implementor对象的引用(桥接的核心)。它的operation()方法通常会调用implementor.operationImpl()。RefinedAbstraction: 抽象部分的扩展,可以增加或修改抽象部分的行为。Implementor: 实现部分的接口。它定义了底层、基础的操作。注意,它的接口可能和Abstraction的接口完全不同。ConcreteImplementorA,ConcreteImplementorB: 实现部分的具体实现,真正实现Implementor接口定义的方法。Client: 只需要与Abstraction层次结构交互,完全不知道Implementor层次结构的存在。
关键: Abstraction 和 Implementor 之间是组合关系,这条线就是“桥”。通过这座桥,抽象和实现可以独立发展。
五、各种实现方式及其优缺点
桥接模式的实现相对标准,但其设计思想比实现方式更重要。
1. 标准实现(基于接口/抽象类)
即上述UML所描述的方式,这是最经典和推荐的方式。
// 实现部分接口
public interface DrawingAPI { // Implementor
void drawCircle(double x, double y, double radius);
}
// 具体实现A
public class WindowsAPI implements DrawingAPI {
@Override
public void drawCircle(double x, double y, double radius) {
System.out.printf("WindowsAPI.circle at (%f, %f) radius %f\n", x, y, radius);
}
}
// 具体实现B
public class LinuxAPI implements DrawingAPI {
@Override
public void drawCircle(double x, double y, double radius) {
System.out.printf("LinuxAPI.circle at (%f, %f) radius %f\n", x, y, radius);
}
}
// 抽象部分
public abstract class Shape { // Abstraction
protected DrawingAPI drawingAPI; // 桥接的关键引用
protected Shape(DrawingAPI drawingAPI) {
this.drawingAPI = drawingAPI;
}
public abstract void draw();
}
// 精化抽象
public class CircleShape extends Shape { // RefinedAbstraction
private double x, y, radius;
public CircleShape(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);
}
}
// 客户端
public class Client {
public static void main(String[] args) {
Shape circle1 = new CircleShape(1, 2, 3, new WindowsAPI());
Shape circle2 = new CircleShape(5, 7, 11, new LinuxAPI());
circle1.draw(); // 输出: WindowsAPI.circle at (1.000000, 2.000000) radius 3.000000
circle2.draw(); // 输出: LinuxAPI.circle at (5.000000, 7.000000) radius 11.000000
}
}
- 优点:
- 符合开闭原则: 可以独立地扩展抽象层次和实现层次,无需修改现有代码。新增一种形状或一种绘图API都非常容易。
- 符合单一职责原则: 抽象部分专注于高层逻辑,实现部分专注于底层细节。
- 极大的灵活性: 可以在运行时动态地改变实现(通过setter方法修改
drawingAPI引用)。
- 缺点:
- 增加了系统的复杂性: 对高内聚的类使用桥接模式可能会使代码变得更加复杂,显得“过度设计”。
六、最佳实践
- 识别变化维度: 成功应用桥接模式的关键在于识别出系统中哪些是独立变化的维度。一个维度是抽象,另一个(或多个)维度是实现。
- 组合优于继承: 桥接模式是“组合优于继承”这一原则的杰出体现。当你发现使用继承来处理多个变化维度会导致类层次结构复杂时,就应考虑使用桥接模式。
- 不要过度设计: 如果系统只有一个变化的维度,或者多个维度但变化非常稳定,那么简单的继承可能就足够了。桥接模式是针对特定复杂场景的解决方案。
- 与策略模式区分:
- 桥接模式: 关注于长期存在的、结构性的“抽象-实现”分离。它们的关系是静态的(虽然在运行时可以切换)。例如,一个形状和它的绘制方式。
- 策略模式: 关注于短期的、行为性的算法替换。策略是完成特定任务的一种可选方式,通常更轻量级,切换更频繁。例如,排序算法。
七、在开发中的演变和应用
桥接模式的思想在现代架构和框架设计中是基础性的:
- 驱动程序设计: 桥接模式是驱动程序架构的理论基础。操作系统或框架定义抽象的接口(如JDBC的
Connection,Statement),而数据库厂商提供具体的实现(如MySQL Driver, Oracle Driver)。应用程序只依赖于抽象接口,可以随时切换底层驱动(实现)。 - 插件架构: 许多软件支持插件功能。核心程序是“抽象”,定义了插件接口(“实现者接口”),而第三方插件是“具体实现”。这允许程序的功能被独立地扩展。
- 跨平台开发: 抽象部分定义业务逻辑,而实现部分提供针对不同平台(Windows, macOS, Linux)的具体实现。这使得核心业务逻辑可以复用,而只需为每个平台编写少量的原生代码。
八、真实开发案例(Java语言内部、知名开源框架、工具)
-
JDBC (Java Database Connectivity):
这是桥接模式最经典的应用。Abstraction:javax.sql.DataSource,java.sql.Connection,java.sql.Statement,java.sql.ResultSet。这些是Java标准库定义的抽象接口。Implementor: 各大数据库厂商提供的驱动包,如mysql-connector-java.jar。它包含了ConnectionImpl,StatementImpl等具体实现类。- 桥接: 应用程序代码只依赖于JDBC抽象接口。通过
Class.forName("com.mysql.cj.jdbc.Driver")和DriverManager.getConnection(),程序在运行时将抽象的Connection桥接到MySQL的具体实现上。更换数据库只需更换驱动JAR包和连接字符串,无需修改业务代码。
-
SLF4J (Simple Logging Facade for Java):
SLF4J是一个日志门面(抽象),它背后可以桥接到多种日志实现(实现)。Abstraction:org.slf4j.Logger,org.slf4j.LoggerFactory。Implementor: Logback, Log4j 2, java.util.logging 等。- 桥接: 应用程序使用SLF4J的API打印日志。通过在项目中引入不同的绑定包(如
slf4j-log4j12.jar),SLF4J会在运行时自动桥接到对应的日志实现上。
-
AWT/Swing中的Peer架构:
Java的AWT GUI工具包早期使用了桥接模式(Peer架构)。Abstraction:java.awt.Button,java.awt.Window等。Implementor: 平台相关的对等实体(Peer),如sun.awt.windows.WButtonPeer。- 抽象AWT组件将所有的操作委托给一个平台相关的实现(Peer),从而实现了“编写一次,到处运行”。
九、总结
| 方面 | 总结 |
|---|---|
| 模式类型 | 结构型设计模式 |
| 核心意图 | 将抽象部分与它的实现部分分离,使它们都可以独立地变化。 |
| 关键角色 | 抽象 (Abstraction)、精化抽象 (RefinedAbstraction)、实现者 (Implementor)、具体实现 (ConcreteImplementor) |
| 主要优点 | 1. 解耦抽象与实现:极大提高了系统的灵活性。 2. 取代多层继承:解决了类爆炸问题,层次清晰。 3. 符合开闭原则和单一职责原则:抽象和实现可以独立扩展。 |
| 主要缺点 | 1. 增加理解与设计难度:需要能正确识别系统中两个独立变化的维度。 2. 可能增加代码复杂度:对简单系统可能显得过度设计。 |
| 适用场景 | 1. 一个类存在多个独立变化的维度,且都需要扩展。 2. 不希望使用继承导致类爆炸。 3. 需要在运行时动态切换实现。 |
| 核心手段 | 使用组合/聚合关系代替继承关系。 |
| 关系与对比 | vs. 适配器模式: 适配器是事后补救,用于连接两个不兼容的接口。桥接模式是事前设计,用于将抽象和实现分离,使它们能独立演化。 |
| 现代应用 | JDBC、日志门面(SLF4J) 等标准规范的核心设计思想,是构建可扩展框架和组件的基础。 |
桥接模式是一种理解起来略有难度,但一旦掌握就会极大提升架构设计能力的高级模式。它教导我们不要急于使用继承来搭建关系,而是先分析变化的维度,并通过组合来构建更灵活、更健壮的系统。它是构建大型、可扩展框架(如Spring、JDBC)的理论基石,是区分普通程序员和架构师的重要知识标志。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120427

浙公网安备 33010602011771号