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

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

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

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

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

二、桥接模式的基础介绍

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

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

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

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

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

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

java 复制代码
// 实现接口
interface MessageTransport {
    void send(String message, String target);
}

// 具体实现类A
class EmailTransport implements MessageTransport {
    @Override
    public void send(String message, String target) {
        System.out.println("通过电子邮件发送消息:" + message + " 到 " + target);
    }
}

// 具体实现类B
class SmsTransport implements MessageTransport {
    @Override
    public 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);
    }

    @Override
    public 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("这是一封测试邮件", "[email protected]");

        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 等。

java 复制代码
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为例:

java 复制代码
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 {
    }

    @Override
    public Connection connect(String url, Properties info) throws SQLException {
        // 根据 MySQL 数据库的连接协议和规范,创建相应的网络连接,并进行身份验证和初始化等操作
        // 这里是简化示意,实际代码更复杂
        return new ConnectionImpl(url, info);
    }

    @Override
    public boolean acceptsURL(String url) throws SQLException {
        // 判断是否是 MySQL 数据库的 URL
        return url != null && url.startsWith("jdbc:mysql:");
    }
    // 其他方法省略
}

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

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

java 复制代码
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,它是一个测试运行器,承担着加载和运行测试用例的职责。

java 复制代码
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()? "所有测试通过" : "有测试失败");
    }
}

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

java 复制代码
import org.junit.Test;

import static org.junit.Assert.*;

public class MyTest {

    @Test
    public 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等注解进行定义,并且支持使用断言库进行断言操作。例如:

java 复制代码
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class JUnit5Test {
    @Test
    public 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,将物流供应商和配送方式进行关联,实现两者的解耦。

java 复制代码
// 物流供应商接口
public interface LogisticsProvider {
    void deliver(String orderId);
}

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

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

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

// 配送方式接口
public interface DeliveryMethod {
    void executeDelivery(LogisticsProvider provider, String orderId);
}

// 标准快递配送方式实现
@Component
public class StandardDeliveryMethod implements DeliveryMethod {
    @Override
    public void executeDelivery(LogisticsProvider provider, String orderId) {
        System.out.println("使用标准快递配送");
        provider.deliver(orderId);
    }
}

// 加急快递配送方式实现
@Component
public class ExpressDeliveryMethod implements DeliveryMethod {
    @Override
    public void executeDelivery(LogisticsProvider provider, String orderId) {
        System.out.println("使用加急快递配送");
        provider.deliver(orderId);
    }
}

// 同城当日达配送方式实现
@Component
public class SameDayDeliveryMethod implements DeliveryMethod {
    @Override
    public 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方法,即可完成新供应商的添加,而无需修改现有的配送方式和配送逻辑。

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

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

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

五、模式总结

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

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

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

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

相关推荐
三金C_C4 小时前
单例模式解析
单例模式·设计模式·线程锁
ShareBeHappy_Qin6 小时前
设计模式——设计模式理念
java·设计模式
木子庆五8 小时前
Android设计模式之代理模式
android·设计模式·代理模式
前端_ID林10 小时前
前端必须知道的设计模式
设计模式
麦客奥德彪12 小时前
设计模式分类与应用指南
设计模式
小宋要上岸12 小时前
设计模式-单例模式
单例模式·设计模式
程序员JerrySUN13 小时前
设计模式 Day 1:单例模式(Singleton Pattern)详解
单例模式·设计模式
古力德15 小时前
代码重构之[过长参数列表]
设计模式·代码规范
OpenSeek16 小时前
【设计模式】面向对象的设计模式概述
设计模式·c#·设计原则
十五年专注C++开发18 小时前
设计模式之适配器模式(二):STL适配器
c++·设计模式·stl·适配器模式·包装器