文章目录
- 1 基本介绍
- 2 案例
- 2.1 OperatingSystem 抽象类
- 2.2 LinuxOS 类
- 2.3 WindowsOS 类
- 2.4 FileOperation 类
- 2.5 FileAppender 类
- 2.6 FileReplicator 类
- 2.7 Client 类
- 2.8 Client 类运行结果
- 2.9 总结
- 3 各角色之间的关系
- 3.1 角色
- 3.1.1 Implementor ( 实现者 )
- 3.1.2 ConcreteImplementor ( 具体实现者 )
- 3.1.3 Abstraction ( 抽象化 )
- 3.1.4 RefinedAbstraction ( 改善后的抽象化 )
- 3.2 类图
- 4 注意事项
- 5 其思想在源码中的使用
- 6 优缺点
- 7 适用场景
- 8 总结
1 基本介绍
桥接模式(Bridge Pattern)是一种 结构型 设计模式,它的主要目的是将 功能 与 实现 分离,使它们都可以 独立地变化。
2 案例
本案例的目的是:在 Linux 和 Windows 系统中,对文件使用不同的操作。
两个系统的文件系统都使用 Map
来实现,为了凸显出两个系统的不同,使用不同的 Map
实现类:
- 在 Linux 系统中使用
ConcurrentHashMap
(线程安全)作为Map
的实现类 - 在 Windows 系统中使用
HashMap
(线程不安全)作为Map
的实现类。
文件的操作有两种:
- 基础操作:打开文件、将内容覆盖文件、关闭文件。
- 进阶操作:需要 复合 基础操作的操作,共有以下三种:
- 追加操作:将指定内容追加到文件尾部。
- 复制操作:将指定源文件复制到指定目标文件。
- 展示操作:展示文件的内容。注意:由于不想导致类的关系太过复杂,所以将其放到基础操作中实现。
2.1 OperatingSystem 抽象类
public abstract class OperatingSystem { // 操作系统 抽象类
/**
* 打开文件,如果没有,就新建一个文件
* @param fileName 待打开文件名
* @return 文件中的内容
*/
public abstract String openFile(String fileName);
/**
* 修改文件
* @param fileName 待修改文件名
* @param content 新的内容
*/
public abstract void modifyFile(String fileName, String content);
/**
* 关闭文件
* @param fileName 待关闭文件名
*/
public abstract void closeFile(String fileName);
}
2.2 LinuxOS 类
public class LinuxOS extends OperatingSystem { // Linux 操作系统,它的文件系统线程安全
/**
* 文件系统,key 为 文件名,value 为 文件内容
* 使用 ConcurrentHashMap 保证线程安全
*/
private final Map<String, String> fileSystem = new ConcurrentHashMap<>();
@Override
public String openFile(String fileName) {
if (!fileSystem.containsKey(fileName)) { // 如果不存在某个文件
fileSystem.put(fileName, ""); // 则创建一个空文件
}
return fileSystem.get(fileName);
}
@Override
public void modifyFile(String fileName, String content) {
fileSystem.put(fileName, content);
}
@Override
public void closeFile(String fileName) {
// 在本案例中无需实现
}
}
2.3 WindowsOS 类
public class WindowsOS extends OperatingSystem { // Windows 操作系统,它的文件系统线程不安全
/**
* 文件系统,key 为 文件名,value 为 文件内容
* 使用 HashMap 可能会出现线程不安全的问题
*/
private final Map<String, String> fileSystem = new HashMap<>();
@Override
public String openFile(String fileName) {
if (!fileSystem.containsKey(fileName)) { // 如果不存在某个文件
fileSystem.put(fileName, ""); // 则创建一个空文件
}
return fileSystem.get(fileName);
}
@Override
public void modifyFile(String fileName, String content) {
fileSystem.put(fileName, content);
}
@Override
public void closeFile(String fileName) {
// 在本案例中无需实现
}
}
2.4 FileOperation 类
public class FileOperation { // 基础文件操作的类
private final OperatingSystem os; // 操作系统
public FileOperation(OperatingSystem os) {
this.os = os;
}
/**
* 打开文件,如果没有,就新建一个文件
* @param fileName 待打开文件名
* @return 文件中的内容
*/
public String openFile(String fileName) {
return os.openFile(fileName);
}
/**
* 修改文件
* @param fileName 待修改文件名
* @param content 新的内容
*/
public void modifyFile(String fileName, String content) {
os.modifyFile(fileName, content);
}
/**
* 关闭文件
* @param fileName 待关闭文件名
*/
public void closeFile(String fileName) {
os.closeFile(fileName);
}
/**
* 显示文件的内容
* @param fileName 待显示内容的文件名
*/
public void displayFile(String fileName) {
String content = os.openFile(fileName);
System.out.println("-----------------------------------------------------------");
System.out.println("[" + fileName + "]");
System.out.println(content);
System.out.println("-----------------------------------------------------------");
os.closeFile(fileName);
}
}
2.5 FileAppender 类
public class FileAppender extends FileOperation { // 处理 追加内容操作 的类
public FileAppender(OperatingSystem os) {
super(os);
}
/**
* 将指定内容追加到文件尾部
* @param fileName 待追加文件名
* @param appendContent 追加内容
*/
public void append(String fileName, String appendContent) {
String content = super.openFile(fileName); // 指定文件原有的内容
content += appendContent; // 将新内容追加到原有的内容后
super.modifyFile(fileName, content); // 修改文件
super.closeFile(fileName); // 关闭文件
}
}
2.6 FileReplicator 类
public class FileReplicator extends FileOperation { // 处理 复制文件操作 的类
public FileReplicator(OperatingSystem os) {
super(os);
}
/**
* 将 源文件的内容 拷贝到 目标文件中
* @param srcFileName 源文件名称
* @param dstFileName 目标文件名称
*/
public void replicate(String srcFileName, String dstFileName) {
String srcContent = super.openFile(srcFileName); // 打开源文件,并获取源文件的内容
super.openFile(dstFileName); // 打开目标文件
super.modifyFile(dstFileName, srcContent); // 将 目标文件的内容 修改为 源文件的内容
super.closeFile(dstFileName); // 关闭目标文件
super.closeFile(srcFileName); // 关闭源文件
}
}
2.7 Client 类
public class Client { // 使用 两种系统 分别执行 两种操作 的客户端
public static void main(String[] args) {
// 文件名称
final String srcFileName = "src.txt"; // 源文件
final String dstFileName = "dst.txt"; // 目标文件
// 操作系统
OperatingSystem linuxOS = new LinuxOS();
OperatingSystem windowsOS = new WindowsOS();
// 各种文件操作
FileOperation linuxFileOperation = new FileOperation(linuxOS);
FileAppender linuxFileAppender = new FileAppender(linuxOS);
FileReplicator linuxFileReplicator = new FileReplicator(linuxOS);
FileOperation windowsFileOperation = new FileOperation(windowsOS);
FileAppender windowsFileAppender = new FileAppender(windowsOS);
FileReplicator windowsFileReplicator = new FileReplicator(windowsOS);
// 先分别在 Linux 和 Windows 系统中创建一个文件,并写入不同内容,之后查看内容
System.out.println("操作一:创建文件并写入内容");
linuxFileOperation.openFile(srcFileName);
linuxFileOperation.modifyFile(srcFileName, "Hello, LinuxOS!");
linuxFileOperation.closeFile(srcFileName);
linuxFileOperation.displayFile(srcFileName);
windowsFileOperation.openFile(srcFileName);
windowsFileOperation.modifyFile(srcFileName, "Hello, WindowsOS!");
windowsFileOperation.closeFile(srcFileName);
windowsFileOperation.displayFile(srcFileName);
System.out.println("===============================" +
"=============================="); // 分隔
// 然后给这两个文件追加不同的内容,并查看
System.out.println("操作二:给文件追加内容");
linuxFileAppender.append(srcFileName,
"\nI am a programmer using Linux system.");
linuxFileOperation.displayFile(srcFileName);
windowsFileAppender.append(srcFileName,
"\nI am a programmer using Windows system.");
windowsFileOperation.displayFile(srcFileName);
System.out.println("===============================" +
"=============================="); // 分隔
// 最后将两个文件复制到新的文件中,并查看
System.out.println("操作三:复制文件内容");
linuxFileReplicator.replicate(srcFileName, dstFileName);
linuxFileOperation.displayFile(dstFileName);
windowsFileReplicator.replicate(srcFileName, dstFileName);
windowsFileOperation.displayFile(dstFileName);
}
}
2.8 Client 类运行结果
操作一:创建文件并写入内容
-----------------------------------------------------------
[src.txt]
Hello, LinuxOS!
-----------------------------------------------------------
-----------------------------------------------------------
[src.txt]
Hello, WindowsOS!
-----------------------------------------------------------
=============================================================
操作二:给文件追加内容
-----------------------------------------------------------
[src.txt]
Hello, LinuxOS!
I am a programmer using Linux system.
-----------------------------------------------------------
-----------------------------------------------------------
[src.txt]
Hello, WindowsOS!
I am a programmer using Windows system.
-----------------------------------------------------------
=============================================================
操作三:复制文件内容
-----------------------------------------------------------
[dst.txt]
Hello, LinuxOS!
I am a programmer using Linux system.
-----------------------------------------------------------
-----------------------------------------------------------
[dst.txt]
Hello, WindowsOS!
I am a programmer using Windows system.
-----------------------------------------------------------
2.9 总结
在本案例中,OperatingSystem
抽象类 和 LinuxOS, WindowsOS
类组成了 实现,FileOperation, FileAppender, FileReplicator
类组成了 功能,使用 FileOperation
类将 实现 与 功能 分隔开来。
由于不想再增加本案例的复杂度,所以将 展示操作 放到本类中,依照本模式的思想,应该将 展示操作 也放到一个单独的类中,大家可以将 写一个类来处理 展示操作 当作练习。
在添加新的功能(或实现)时,无需为其添加对应的实现(或功能)类:
- 如果想要添加一个新功能,只 需要继承 功能 体系中的某个类即可。如将 展示操作 写到独立的类中。
- 如果想要添加一个新实现,只 需要继承
OperatingSystem
抽象类即可。如添加MacOS
类。
3 各角色之间的关系
3.1 角色
3.1.1 Implementor ( 实现者 )
该角色负责 定义 用于实现 Abstraction 角色的接口的 方法,位于 实现 体系的顶端。在本案例中,OperatingSystem
抽象类扮演该角色。
3.1.2 ConcreteImplementor ( 具体实现者 )
该角色负责 实现 在 Implementor 角色中定义的 方法。在本案例中,LinuxOS, WindowsOS
类都扮演了该角色。
3.1.3 Abstraction ( 抽象化 )
该角色负责 使用 Implementor 角色的方法,定义基本的功能,位于 功能 体系的顶端。此外,还需要保存 Implementor 角色的实例。在本案例中,FileOperation
类扮演了该角色。
3.1.4 RefinedAbstraction ( 改善后的抽象化 )
该角色负责 在 Abstraction 角色的基础上添加新功能。在本案例中,FileAppender, FileReplicator
类都扮演了该角色。
3.2 类图
说明:
- Implementor 角色中的方法
method1(), method2()
是abstract
的,强制其子类实现。 - Implementor 角色也可以是接口,这样一来,ConcreteImplementor 角色与它之间的关系就是 实现 了。
- Abstraction 角色 和 Implementor 角色 中间的 聚合 关系 就代表了 桥接模式 的 桥接。
4 注意事项
- 分离 功能 和 实现:在使用桥接模式进行设计时,需要 辨析 系统的 功能 和 实现,从而将其分离开来。这无疑增加了设计难度。
- 使用范围局限性:如果想要使用桥接模式,则系统中必须存在 两个或多个独立变化的维度,并且这些维度需要 独立扩展。如果系统中没有这样的应用场景,强行使用桥接模式可能会导致设计 过度复杂,反而降低系统的可维护性和可扩展性。
5 其思想在源码中的使用
JDBC(Java Database Connectivity)在使用桥接模式时,主要体现在它如何处理 数据库驱动的加载 和 数据库的使用 上:
- 实现体系:
- 抽象实现:在 JDBC 中,
java.sql.Driver
接口扮演了 Implementor 角色。它定义了一个驱动应该实现的接口,包括如何连接到数据库等方法。 - 具体实现:各个数据库厂商提供的驱动类(如
com.mysql.cj.jdbc.Driver
)(直接或间接地) 实现了java.sql.Driver
接口,扮演 ConcreteImplementor 角色。这些驱动类包含了与特定数据库交互的具体逻辑。
- 抽象实现:在 JDBC 中,
- 功能体系——DriverManager 类:
DriverManager
类在 JDBC 中起到了 桥梁 的作用,它维护了一个已注册的 JDBC 驱动列表(registeredDrivers
)。当通过Class.forName()
方法加载数据库驱动时,实际上是在调用 该驱动类的静态代码块,其中包含了 将自身实例注册到DriverManager
的registeredDrivers
列表中 的代码。- 当调用
DriverManager.getConnection()
方法获取数据库连接时,DriverManager
会遍历registeredDrivers
列表,尝试使用列表中的每个驱动与数据库建立连接,直到找到成功的连接或所有驱动都尝试失败为止。 - 此外,
DriverManager
类独立承担了所有的 功能,没有将更复杂的功能交给子类实现,这就是 JDBC 使用的桥接模式 与 常规桥接模式 的不同之处。
- 实现 与 功能 的分离:
- 实现——数据库驱动:不同的数据库需要不同的驱动来实现与 JDBC 的交互。
- 功能——数据库操作:
DriverManager
类提供了所有的数据库操作。
- 桥接模式的优势:
- 扩展性强:由于功能与实现分离,当需要支持新的数据库时,只需提供新的驱动实现即可,无需修改现有的 JDBC 代码。
- 耦合度低:JDBC 的抽象接口与具体数据库实现之间通过
DriverManager
进行桥接,降低了它们之间的耦合度。
6 优缺点
优点:
- 提高可扩展性:桥接模式将 功能 与 实现 分离,使得它们可以 独立地变化。这意味着我们可以在不修改抽象代码的情况下,通过增加新的实现类来扩展系统的功能。
- 符合开闭原则:桥接模式符合开闭原则,即对扩展开放,对修改关闭。这意味着我们可以在不修改现有代码的情况下,通过扩展系统来满足新的需求。
- 降低系统的耦合度:通过桥接模式,我们可以减少类之间的直接依赖关系,降低系统的耦合度。这有助于提高系统的 可维护性 和 可测试性。
- 支持多种变化:当一个类存在 多个角度的变化 时(例如,一个类既需要支持不同的操作,又需要支持不同的实现方式),桥接模式可以很好地处理这种情况,因为它允许在多个维度上进行变化。
缺点:
- 增加系统的复杂性:桥接模式增加了系统的抽象层次,使得系统的理解和实现变得更加复杂。特别是当系统中存在大量的抽象类和实现类时,这种复杂性可能会变得难以管理。
- 增加设计的难度:在设计初期,正确地识别出哪些部分(功能部分)应该 被抽象化,哪些部分(实现部分)应该 被实现化,并不是一件容易的事情。这需要设计者对系统有深入的理解和丰富的经验。
- 增加对象的数量:由于桥接模式将 功能 和 实现 分离,因此在运行时可能会产生大量的对象。这可能会增加系统的内存开销,并降低系统的性能。
- 过度设计:在某些情况下,可能会存在过度使用桥接模式的风险。如果系统中的变化维度并不明显,或者变化的可能性很小,那么使用桥接模式可能会导致过度设计,增加不必要的复杂性。
7 适用场景
- 数据库连接 与 驱动:在连接数据库时,桥接模式可以用于分离 数据库连接接口(功能)和 不同的数据库驱动程序(实现)。这样,在不修改连接代码的情况下,可以轻松地切换不同的数据库。例如,在 JDBC(Java Database Connectivity)中,
Driver
接口作为抽象部分,不同的数据库厂商(如 MySQL、Oracle)提供的驱动实现类作为实现部分,通过桥接模式实现数据库的灵活切换。 - 日志记录 与 输出目标:在记录日志时,桥接模式可以将 日志记录器(功能)与 不同的日志输出目标(如控制台、文件、数据库等,实现)解耦。这样,可以在不修改日志记录代码的情况下,轻松地切换日志输出目标。例如,在日志框架中,可以定义一个日志记录器接口,不同的日志实现类(如控制台日志实现类、文件日志实现类等)分别实现该接口,通过桥接模式实现日志记录的灵活配置。
- 消息队列 与 协议:在使用消息队列时,桥接模式可以用于分离 消息队列客户端接口(功能)和 不同的消息队列协议(如 RocketMQ、Kafka等,实现)。这样,可以在不修改消息队列代码的情况下,轻松地切换不同的消息队列协议。例如,在消息队列客户端框架中,可以定义一个消息队列客户端接口,不同的消息队列协议实现类分别实现该接口,通过桥接模式实现消息队列的灵活选择。
- 图形用户界面(GUI):在图形用户界面(GUI)框架中,桥接模式可以用于分离 GUI 组件的抽象表示 和 具体渲染实现。例如,在 Swing 或 JavaFX 等 GUI 框架中,可以将组件的抽象行为(如按钮点击事件)与具体的渲染逻辑(如按钮的外观和样式)分离,通过桥接模式实现 GUI 组件的灵活定制和扩展。
- 插件系统:在插件系统中,桥接模式可以用于分离 插件的接口定义 和 具体的插件实现。这样,可以在不修改主程序代码的情况下,通过添加新的插件实现来扩展程序的功能。插件系统广泛应用于各种软件开发中,如 IDE(集成开发环境)、游戏平台等,桥接模式为这些系统提供了良好的扩展性和灵活性。
8 总结
桥接模式 是一种 结构型 设计模式,它的主要目的是将 功能 与 实现 分离,使它们都可以 独立地变化。这种分离方式增强了系统的 灵活性 和 可扩展性,允许在不修改抽象代码的情况下增加新的实现。桥接模式通过 聚合关系 而非 继承关系 来实现这一点,从而减少了类之间的耦合。
此外,在使用本模式之前一定要理清系统的逻辑,认清哪部分是 功能,哪部分是 实现。如果功能和实现变化的程度不大,则 不要为了使用桥接模式而使用桥接模式,这时简单一点,将其耦合起来比较方便。