每天认识一个设计模式-桥接模式:在抽象与实现的平行宇宙架起彩虹桥

embedded/2025/4/1 2:04:55/

一、前言:虚拟机桥接的启示

使用过VMware或者Docker的同学们应该都接触过网络桥接,在虚拟机网络配置里,桥接模式是常用的网络连接方式。选择桥接模式时,虚拟机会通过虚拟交换机与物理网卡相连,获取同网段 IP 地址,如同直接连在同一网络交换机上的两台主机,能直接通信。它可让虚拟机像真实主机一样访问局域网设备,接收 DHCP 分配的 IP,或手动配置网络参数进行通信。​

而在软件开发中也有这么一个设计模式,从软件设计角度,虚拟机桥接模式桥接模式思想一致,桥接模式核心是分离抽象与实现使其独立变化。虚拟机桥接模式中,物理和虚拟机网络相对独立,借虚拟交换机实现连接通信。

软件系统也有类似情况,多种实现方式与抽象层次下,可借鉴桥接模式分离抽象和实现,降低耦合度,让系统更灵活可扩展,今天咱们就来了解一下软件设计中的桥接模式。希望感兴趣的同学能对这一设计模式有所感悟~

二、桥接模式的基础介绍

桥接(Bridge)是用于把抽象化与实现化解耦,使得二者可以独立变化。这种类型的设计模式属于结构型模式,它通过提供抽象化和实现化之间的桥接结构,来实现二者的解耦。

这种模式涉及到一个作为桥接的接口,使得实体类的功能独立于接口实现类,这两种类型的类可被结构化改变而互不影响。

桥接模式的目的是将抽象与实现分离,使它们可以独立地变化,该模式通过将一个对象的抽象部分与它的实现部分分离,使它们可以独立地改变。它通过组合或者聚合的方式,而不是继承的方式,将抽象和实现的部分连接起来。通过引入一个桥接接口,实现抽象类和实现类之间的关联,从而避免了抽象类和实现类之间的紧耦合关系。​

桥接模式的 UML 结构中,主要包含以下几个关键角色:​

  • 抽象(Abstraction):定义抽象接口,通常包含对实现接口的引用。
  • 扩展抽象(Refined Abstraction):对抽象的扩展,可以是抽象类的子类或具体实现类。
  • 实现(Implementor):定义实现接口,提供基本操作的接口。
  • 具体实现(Concrete Implementor):实现实现接口的具体类。

根据这个逻辑我们可以通过简单的代码来实现一个基础的桥接模式的基础使用案例:

// 实现接口
interface MessageTransport {void send(String message, String target);
}// 具体实现类A
class EmailTransport implements MessageTransport {@Overridepublic void send(String message, String target) {System.out.println("通过电子邮件发送消息:" + message + " 到 " + target);}
}// 具体实现类B
class SmsTransport implements MessageTransport {@Overridepublic void send(String message, String target) {System.out.println("通过短信发送消息:" + message + " 到 " + target);}
}// 抽象类
abstract class MessageSender {protected MessageTransport messageTransport;public MessageSender(MessageTransport messageTransport) {this.messageTransport = messageTransport;}public abstract void sendMessage(String message, String target);
}// 修正抽象类
class TextMessageSender extends MessageSender {public TextMessageSender(MessageTransport messageTransport) {super(messageTransport);}@Overridepublic void sendMessage(String message, String target) {messageTransport.send(message, target);}
}// 客户端代码
public class Client {public static void main(String[] args) {MessageTransport emailTransport = new EmailTransport();MessageSender textMessageSenderByEmail = new TextMessageSender(emailTransport);textMessageSenderByEmail.sendMessage("这是一封测试邮件", "test@example.com");MessageTransport smsTransport = new SmsTransport();MessageSender textMessageSenderBySms = new TextMessageSender(smsTransport);textMessageSenderBySms.sendMessage("这是一条测试短信", "12345678900");}
}
  1. MessageTransport 接口:定义了发送消息的方法send,这是实现类需要实现的接口。​
  2. EmailTransport 类和 SmsTransport 类:分别实现了MessageTransport接口,提供了使用电子邮件和短信发送消息的具体实现。​
  3. MessageSender 抽象类:持有一个MessageTransport的引用,通过构造函数进行初始化,并定义了抽象方法sendMessage。​
  4. TextMessageSender 类:继承自MessageSender抽象类,实现了sendMessage方法,在sendMessage方法中调用MessageTransport的send方法来发送消息。​
  5. Client 类:作为客户端,创建了EmailTransport和SmsTransport的实例,然后分别创建了使用这两种传输方式的文本消息发送对象,并调用sendMessage方法进行消息发送。

这样如果我们要新增即时通讯传输技术,只需要新增一个实现MessageTransport接口的ImTransport类,而不需要修改抽象类MessageSender及其子类TextMessageSender等,从而实现了抽象和实现分离。

三、框架设计中的桥接智慧

3.1 JDBC 驱动架构:动态适配数据库

在 Java 数据库编程中,JDBC(Java Database Connectivity)扮演着至关重要的角色,其驱动架构是桥接模式的典型应用。java.sql.Driver接口定义了数据库操作的规范,它是整个 JDBC 驱动架构的核心抽象。这个接口规定了一系列方法,如connect方法用于建立与数据库的连接,acceptsURL方法用于判断驱动是否能够处理指定的 URL 等。

package java.sql;import java.util.Properties;public interface Driver {public interface Driver {/*** Attempts to make a database connection to the given URL.* The driver should return "null" if it realizes it is the wrong kind* of driver to connect to the given URL.  This will be common, as when* the JDBC driver manager is asked to connect to a given URL it passes* the URL to each loaded driver in turn.** <P>The driver should throw an {@code SQLException} if it is the right* driver to connect to the given URL but has trouble connecting to* the database.** <P>The {@code Properties} argument can be used to pass* arbitrary string tag/value pairs as connection arguments.* Normally at least "user" and "password" properties should be* included in the {@code Properties} object.* <p>* <B>Note:</B> If a property is specified as part of the {@code url} and* is also specified in the {@code Properties} object, it is* implementation-defined as to which value will take precedence. For* maximum portability, an application should only specify a property once.** @param url the URL of the database to which to connect* @param info a list of arbitrary string tag/value pairs as* connection arguments. Normally at least a "user" and* "password" property should be included.* @return a {@code Connection} object that represents a*         connection to the URL* @throws SQLException if a database access error occurs or the url is* {@code null}*/Connection connect(String url, java.util.Properties info)throws SQLException;/*** Retrieves whether the driver thinks that it can open a connection* to the given URL.  Typically drivers will return {@code true} if they* understand the sub-protocol specified in the URL and {@code false} if* they do not.** @param url the URL of the database* @return {@code true} if this driver understands the given URL;*         {@code false} otherwise* @throws SQLException if a database access error occurs or the url is* {@code null}*/boolean acceptsURL(String url) throws SQLException;// 其他方法省略
}

通过这些方法,JDBC 为各种数据库操作提供了统一的抽象接口,使得上层应用程序可以以一致的方式与不同的数据库进行交互,而无需关心具体的数据库实现细节。​

在实现层,不同的数据库厂商提供了各自的具体驱动实现。例如,MySQL 数据库的驱动实现类是com.mysql.cj.jdbc.Driver,Oracle 数据库的驱动实现类是oracle.jdbc.driver.OracleDriver。以 MySQL 驱动实现类com.mysql.cj.jdbc.Driver为例:

package com.mysql.cj.jdbc;import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;public class Driver extends NonRegisteringDriver implements java.sql.Driver {static {try {DriverManager.registerDriver(new Driver());} catch (SQLException E) {throw new RuntimeException("Can't register driver!");}}public Driver() throws SQLException {}@Overridepublic Connection connect(String url, Properties info) throws SQLException {// 根据 MySQL 数据库的连接协议和规范,创建相应的网络连接,并进行身份验证和初始化等操作// 这里是简化示意,实际代码更复杂return new ConnectionImpl(url, info);}@Overridepublic boolean acceptsURL(String url) throws SQLException {// 判断是否是 MySQL 数据库的 URLreturn url != null && url.startsWith("jdbc:mysql:");}// 其他方法省略
}

这些具体实现类实现了Driver接口中定义的方法,针对各自数据库的特性和协议,提供了与数据库进行交互的具体逻辑。​

在 JDBC 驱动架构中,DriverManager充当了桥接器的角色,负责管理和加载驱动程序。DriverManager通过反射机制加载具体的驱动实现类。

package java.sql;import java.util.Enumeration;
import java.util.Vector;public class DriverManager {private final static Vector<DriverInfo> registeredDrivers = new Vector<>();public static Connection getConnection(String url, String user, String password) throws SQLException {java.util.Properties info = new java.util.Properties();if (user != null) {info.put("user", user);}if (password != null) {info.put("password", password);}return getConnection(url, info, Reflection.getCallerClass());}private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {for (DriverInfo aDriver : registeredDrivers) {Driver driver = aDriver.driver;if (driver.acceptsURL(url)) {return driver.connect(url, info);}}throw new SQLException("No suitable driver found for " + url);}// 其他方法省略
}

当应用程序调用DriverManager.getConnection方法时,DriverManager会遍历已注册的驱动程序列表,调用每个驱动的acceptsURL方法,判断哪个驱动能够处理指定的 URL。如果找到合适的驱动,就会调用该驱动的connect方法,建立与数据库的连接。

这种机制使得应用程序可以在运行时动态地选择和切换不同的数据库驱动,实现了抽象与实现的分离和动态绑定。

此时如果我们在开发阶段可能使用 MySQL 数据库进行开发和测试,而在生产环境中,为了满足高并发和高可靠性的需求,可能会切换到 Oracle 数据库。通过 JDBC 的桥接模式,只需要在配置文件中修改数据库的 URL 和驱动类名,应用程序的其他部分无需修改,就可以轻松地切换到不同的数据库。

3.2 JUnit 框架中桥接模式的应用

在 JUnit 框架里,桥接模式的运用体现在测试运行器与测试用例的关系上。测试运行器负责执行测试用例,桥接模式则实现了二者的解耦,进而提升框架的灵活性与可扩展性。​

JUnit 的测试运行器是一个抽象概念,定义了执行测试用例的基本流程与规范,但不涉及具体的测试用例实现。比如 JUnit4 中的JUnitCore,它是一个测试运行器,承担着加载和运行测试用例的职责。

import org.junit.runner.JUnitCore;import org.junit.runner.Result;import org.junit.runner.notification.Failure;public class TestRunner {public static void main(String[] args) {// 使用JUnitCore运行MyTest类的测试用例Result result = JUnitCore.runClasses(MyTest.class);// 遍历测试结果中的失败项for (Failure failure : result.getFailures()) {System.out.println(failure.toString());}// 根据测试结果输出相应信息System.out.println(result.wasSuccessful()? "所有测试通过" : "有测试失败");}
}

而测试用例是具体测试逻辑的实现,通过编写测试类和测试方法来定义。

import org.junit.Test;import static org.junit.Assert.*;public class MyTest {@Testpublic void testAddition() {int result = 2 + 3;assertEquals(5, result);}
}

在 JUnit 中,桥接模式借助Runner类及其子类,实现测试运行器和测试用例之间的动态绑定。Runner类是抽象类,定义了获取测试用例、运行测试用例等方法。不同的测试运行器通过继承Runner类,实现其抽象方法,从而提供具体的测试执行逻辑。​

以BlockJUnit4ClassRunner为例,它是Runner的子类,用于运行 JUnit4 风格的测试类。当JUnitCore运行测试类时,会依据测试类的类型,选择合适的Runner子类来运行测试用例。

在运行期,JUnit 的测试运行器会根据测试类的元数据,比如注解等,创建相应的Runner实例,然后通过这个实例执行测试用例。这种方式实现了测试运行器和测试用例之间的解耦,使得更换测试运行器或测试用例变得轻松,无需大量修改代码。​

除了上述 JUnit4 风格的测试类,JUnit 还支持 JUnit 5 的测试风格。JUnit 5 引入了全新的编程模型,在 JUnit 5 中,测试类和测试方法使用@Test、@BeforeEach、@AfterEach等注解进行定义,并且支持使用断言库进行断言操作。例如:

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;public class JUnit5Test {@Testpublic void testMultiplication() {int result = 3 * 4;assertEquals(12, result);}
}

JUnit 5 中测试运行器也发生了变化,JUnitPlatform作为测试运行器的核心。JUnitPlatform通过发现机制找到所有的测试类,并委托给相应的测试引擎执行测试。

不同的测试引擎可以看作是Runner的变体,针对 JUnit 5 的测试风格进行适配。例如JUnit Jupiter引擎专门用于运行 JUnit 5 风格的测试类,它会解析 JUnit 5 的注解,构建测试执行上下文,然后执行测试方法。

当项目中需要运行 JUnit 5 风格的测试用例时,只需要将JUnit Jupiter引擎集成到项目中,JUnitPlatform会自动识别并使用该引擎来运行 JUnit 5 风格的测试类,而无需对其他部分代码进行大规模修改,这进一步体现了桥接模式在 JUnit 框架中对不同类型测试用例和测试运行器的灵活支持。​

从使用结果来看,借助桥接模式,JUnit 框架能灵活支持不同类型的测试用例和测试运行器。在项目中,我们可根据需求选择合适的测试运行器,也能方便地编写和扩展测试用例。

同时,桥接模式让 JUnit 框架的代码结构更清晰,易于维护和扩展。比如,要添加新的测试运行器或者新的测试用例类型,只需继承Runner类并实现相应逻辑,不会对其他部分代码造成影响。

四、 桥接模式业务场景的基本应用

在供应链管理中,物流配送是一个关键环节。这里我们以一个大型电商企业的供应链物流配送系统为例,来理解桥接模式的应用。

假设电商企业需要与多家物流供应商(如顺丰、中通、韵达)合作,同时支持不同的配送方式(如标准快递、加急快递、同城当日达)。如果不使用桥接模式,传统的实现方式会导致代码的高度耦合和难以维护。

比如,为每种物流供应商和配送方式的组合创建单独的类,随着物流供应商和配送方式的增加,类的数量会呈指数级增长,代码复杂度也会急剧上升。​

而使用桥接模式,我们可以将物流供应商和配送方式进行分离,使它们能够独立变化。

首先,我们可以定义物流供应商接口LogisticsProvider,并创建顺丰、中通、韵达等物流供应商的实现类。然后,定义配送方式接口DeliveryMethod,并创建标准快递、加急快递、同城当日达的实现类。最后,通过一个配送抽象类Delivery,将物流供应商和配送方式进行关联,实现两者的解耦。

// 物流供应商接口
public interface LogisticsProvider {void deliver(String orderId);
}// 顺丰物流供应商实现
@Component
public class SFExpressProvider implements LogisticsProvider {@Overridepublic void deliver(String orderId) {System.out.println("顺丰正在配送订单:" + orderId);}
}// 中通物流供应商实现
@Component
public class ZTOExpressProvider implements LogisticsProvider {@Overridepublic void deliver(String orderId) {System.out.println("中通正在配送订单:" + orderId);}
}// 韵达物流供应商实现
@Component
public class YundaExpressProvider implements LogisticsProvider {@Overridepublic void deliver(String orderId) {System.out.println("韵达正在配送订单:" + orderId);}
}// 配送方式接口
public interface DeliveryMethod {void executeDelivery(LogisticsProvider provider, String orderId);
}// 标准快递配送方式实现
@Component
public class StandardDeliveryMethod implements DeliveryMethod {@Overridepublic void executeDelivery(LogisticsProvider provider, String orderId) {System.out.println("使用标准快递配送");provider.deliver(orderId);}
}// 加急快递配送方式实现
@Component
public class ExpressDeliveryMethod implements DeliveryMethod {@Overridepublic void executeDelivery(LogisticsProvider provider, String orderId) {System.out.println("使用加急快递配送");provider.deliver(orderId);}
}// 同城当日达配送方式实现
@Component
public class SameDayDeliveryMethod implements DeliveryMethod {@Overridepublic void executeDelivery(LogisticsProvider provider, String orderId) {System.out.println("使用同城当日达配送");provider.deliver(orderId);}
}// 配送抽象类
public abstract class Delivery {protected LogisticsProvider logisticsProvider;protected DeliveryMethod deliveryMethod;public Delivery(LogisticsProvider logisticsProvider, DeliveryMethod deliveryMethod) {this.logisticsProvider = logisticsProvider;this.deliveryMethod = deliveryMethod;}public void processDelivery(String orderId) {deliveryMethod.executeDelivery(logisticsProvider, orderId);}
}// 具体配送实现类
@Component
public class OnlineDelivery extends Delivery {public OnlineDelivery(LogisticsProvider logisticsProvider, DeliveryMethod deliveryMethod) {super(logisticsProvider, deliveryMethod);}
}

当我们需要新增一种物流供应商,比如圆通时,只需要创建一个实现LogisticsProvider接口的YTOExpressProvider类,实现其中的deliver方法,即可完成新供应商的添加,而无需修改现有的配送方式和配送逻辑。

// 圆通物流供应商实现
@Component
public class YTOExpressProvider implements LogisticsProvider {@Overridepublic void deliver(String orderId) {System.out.println("圆通正在配送订单:" + orderId);}
}

如果要新增一种配送方式,比如定时配送,只需创建一个实现DeliveryMethod接口的ScheduledDeliveryMethod类,实现executeDelivery方法,并在需要使用定时配送的地方,将ScheduledDeliveryMethod实例注入到相应的Delivery对象中,就可以轻松实现新配送方式的集成,不会影响到已有的物流供应商和其他配送方式。

// 定时配送方式实现
@Component
public class ScheduledDeliveryMethod implements DeliveryMethod {@Overridepublic void executeDelivery(LogisticsProvider provider, String orderId) {System.out.println("使用定时配送");provider.deliver(orderId);}
}

五、模式总结

虽然一定程度上桥接模式赋予系统极高的灵活性,由于抽象与实现分离,在面对业务需求变更时,我们能够独立地对抽象层或实现层进行扩展,而不会对整体架构造成大规模影响。这种分离机制极大地提升了代码的复用性,底层实现可以被多个不同的抽象所共享,减少了重复代码的编写。

比如当我们在一个大型项目中,日志组件可能需要根据不同的环境或需求动态更换,从简单的控制台日志记录切换到文件日志记录,甚至是分布式日志系统。通过桥接模式,抽象的日志记录接口与具体的日志实现类分离,在运行时可以轻松切换不同的日志实现,而无需修改大量的业务代码。

但是桥接模式并非完美无缺。引入多层抽象无疑增加了系统的复杂度,开发人员需要花费更多精力去理解和设计抽象与实现之间的关系。

同时,这种分层设计也带来了一定的学习成本,对于新手而言,理解桥接模式的运作机制需要一定的时间和实践。所以各位一定要量力而行,根据业务场景和架构情况选择合适的设计模式是至关重要的。


http://www.ppmy.cn/embedded/177605.html

相关文章

【PySpark大数据分析概述】01 大数据分析概述

【作者主页】Francek Chen 【专栏介绍】 ⌈ ⌈ ⌈PySpark大数据分析与应用 ⌋ ⌋ ⌋ PySpark作为Apache Spark的Python API&#xff0c;融合Python易用性与Spark分布式计算能力&#xff0c;专为大规模数据处理设计。支持批处理、流计算、机器学习 (MLlib) 和图计算 (GraphX)&am…

Pyside6介绍和开发第一个程序

Pyside6介绍 PySide6 是一个用于创建 图形用户界面&#xff08;GUI&#xff09; 的 Python 库&#xff0c;它是 Qt 框架的官方 Python 绑定。Qt 是一个功能强大的跨平台 C 框架&#xff0c;广泛用于开发桌面应用程序、移动应用程序和嵌入式系统。PySide6 允许开发者使用 Pytho…

Polhemus FastScan 单摄像头3D激光扫描器

FastSCAN Cobra是Polhemus公司研制的手持激光扫描仪。与以前的产品比较&#xff0c;它节省了30&#xff05;的费用&#xff0c;体积也减小了一半 &#xff0c;但仍然保留了所有功能&#xff0c;使用和携带都更加方便。作为超小的手持激光扫描仪,FastSCAN Cobra对扫描三维物体具…

HCIP(VLAN综合实验)

实验拓补图 实验分析 一、实验目的 掌握VLAN的创建和配置方法理解VLAN在局域网中的作用学习如何通过VLAN实现网络隔离和通信 二、实验环境 交换机&#xff08;SW1、SW2、SW3&#xff09;个人电脑&#xff08;PC1、PC2、PC3、PC4、PC5、PC6&#xff09;路由器&#xff08;R1…

记录一次TDSQL事务太大拆过binlog阈值报错

记录一次TDSQL事务太大拆过binlog阈值报错处理过程 1、排查任何类型数据库故障的第一步&#xff0c; 同步实例信息、报错内容、报错时间段、当前是否恢复、如何感知到数据库问题的、对应用有什么影响、系统允许的时间窗口。 2、明确报错内容为单次写入binlog量超过阈值 3、登陆…

【极速版 -- 大模型入门到进阶】LORA:大模型轻量级微调

文章目录 &#x1f30a; 有没有低成本的方法微调大模型&#xff1f;&#x1f30a; LoRA 的核心思想&#x1f30a; LoRA 的初始化和 r r r 的值设定&#x1f30a; LoRA 实战&#xff1a;LoraConfig参数详解 论文指路&#xff1a;LORA: LOW-RANK ADAPTATION OF LARGE LANGUAGE M…

鸿蒙项目源码-记账本app个人财物管理-原创!原创!原创!

鸿蒙记账项目源码个人财务管理含文档包运行成功ArkTS语言。 我一个月写的原创作品&#xff0c;请尊重原创。 原创作品&#xff0c;盗版必究&#xff01;&#xff01;&#xff01; api12 SDK5.0.0仅适用于最新的2024版本DevEco studio 共9个页面&#xff1a;广告倒计时页、登录、…

Go语言手动内存对齐的四大场景与实践指南

Go语言手动内存对齐的四大场景与实践指南 引言&#xff1a;Go的内存对齐机制 Go语言通过编译器自动处理内存对齐问题&#xff0c;开发者通常无需关心底层细节。然而&#xff0c;在特定场景下&#xff0c;手动干预内存对齐是避免程序崩溃或数据错乱的必要操作。本文将深入探讨G…