24天学透设计模式之桥接模式

135 阅读8分钟

什么是桥接模式

问题定义

在软件设计中,当我们发现有两个或多个维度的类会产生变化,并且类之间存在"多对多"的关系时,这就是应用桥接模式的理想场景。

让我们以一个在线电子商务平台为例,该平台需要支持多种支付方式,如信用卡支付、电子钱包支付等。此外,因为服务的地区不同,可能会有一些特定的支付规则或步骤。例如,美国的用户可能需要对信用卡进行AVS(Address Verification System)检查,而欧洲用户可能需要遵循更严格的GDPR规定。如果没有采用桥接模式,我们可能会为每个地区和支付方式组合创建一个独立的类。例如,USCreditCardPaymentUSWalletPaymentEUCreditCardPaymentEUWalletPayment等。

public interface Payment {
    public void pay(double amount);
}

public class USCreditCardPayment implements Payment {
    public void pay(double amount) {
        // 初始化信用卡网关
        // 进行AVS检查
        // 扣款操作
        // ...
    }
}

public class USWalletPayment implements Payment {
    public void pay(double amount) {
        // 链接到电子钱包
        // 扣款操作
        // ...
    }
}
// 更多类:EUCreditCardPayment, EUWalletPayment...

这样的问题在于,如果我们需要添加新的支付方式(例如PayPal,Apple Pay等)或者需要支持新的地区(例如亚洲),就需要新增大量的类。代码复杂度高,可扩展性差,维护困难。之所以需要新增大量的类,本质还是因为我们只声明了一个接口,而这个接口却有多个维度。

应用桥接模式

在软件设计中,当我们发现有两个或多个维度的类会产生变化,并且这些变化彼此独立,正是应用桥接模式的理想场景。换句话说,当你发现你需要为类的每个可能的组合(这就是"多对多"关系)创建子类时,那么桥接模式很可能是解决问题的适当方法。说的更直白一些,桥接模式可以将m*n个实现类转换为m+n个实现类。

就拿上面的在线电子商务平台为例,该平台需要支持多种支付方式,如信用卡支付、电子钱包支付等。此外,由于服务的地区不同,可能会有一些特定的支付规则或步骤。使用桥接模式,我们可以将“地区”和“支付方式”分离成两个独立的层次结构,然后通过组合的方式来表达各种可能的组合。

public interface Payment {
    void pay(double amount);
}

public interface Region {
    void applyRegionRules(Payment payment);
}

public class USRegion implements Region {
    @Override
    public void applyRegionRules(Payment payment) {
        // 应用美国特定的规则
    }
}

public class CreditCardPayment implements Payment {
    @Override
    public void pay(double amount) {
        // 信用卡支付逻辑
    }
}

public class WalletPayment implements Payment {
    @Override
    public void pay(double amount) {
        // 电子钱包支付逻辑
    }
}

桥接模式的角色

  1. 抽象(Abstraction) :这是一个接口或者抽象类,它定义了对象的高层控制逻辑,但将底层实现延迟到其具体实现子类(也就是Implementor)。可以理解为一位“指挥家”,他知道自己要做什么并且能告诉其他人去做,但具体如何实现他并不关心,这部分工作交给专门的实现者。
  2. 具体的抽象(Concrete Abstraction) :这是抽象的扩展,增加了更多的方法和属性。具体来说,它是基础抽象类的派生类,可以添加新的操作或覆盖基础类定义的方法。就像“指挥家”的助手,他不仅遵循“指挥家”的命令,还可能在此基础上增加自己的想法和实现。
  3. 实现者(Implementor) :这也是一个接口或者抽象类,通常与Abstraction定义的接口有所不同。它定义了基础操作,如数据访问、硬件控制等。这就像那些被“指挥家”指挥的技术人员,他们需要完成一些具体的工作,这些工作的细节是“指挥家”并不清楚的。
  4. 具体实现者(Concrete Implementor) :这个角色负责实现在Implementor接口中定义的方法。这就像具体执行工作的技术人员,他们根据技术要求完成实际的工作。

桥接模式的核心思想就是将抽象(Abstraction)和实现(Implementor)分离开来,并使它们可以独立地变化。这样做的好处是可以避免在更改抽象类或实现类时对另一方产生影响,同时也提高了可扩展性和代码的可重用性。

UML

image.png

  • Abstraction表示抽象角色,它声明了一个或多个方法(如operation()),并持有一个对实现者角色(Implementor)的引用。
  • ConcreteAbstraction表示改良化抽象角色,它是抽象角色(Abstraction)的子类,可以扩展抽象角色的行为。
  • Implementor表示实现者角色,它声明了一些基础操作(如implementation())。
  • ConcreteImplementorAConcreteImplementorB表示具体实现者角色,它们分别实现了实现者角色(Implementor)定义的接口。

这个类图描述了桥接模式的总体结构:抽象角色和实现者角色分离,使得抽象部分和实现部分可以独立地变化。

应用场景

  1. 数据库驱动开发:如果你正在开发一个数据库驱动,那么桥接模式也非常适用。在这个情况下,你可以将“数据库连接”(Abstraction)和“数据库实现”(Implementor)分离出来。这样一来,无论是更换数据库(例如从MySQL切换到PostgreSQL),还是修改连接方式(例如从JDBC切换到ODBC),你都无需修改已存在的代码。
  2. 电商平台支付系统:在一个电子商务平台上,你可能会接受信用卡、电子钱包(如 PayPal)、银行转账等多种支付方式。在这种情况下,你可以使用桥接模式来将'支付方式'(就像是实现者 Implementor)与 '订单处理'(抽象 Abstraction)解耦。具体来说,你可能有一个基本的 "Payment" 接口(即实现者)并且对于每种支付方式都有一个具体的实现类(具体实现者 ConcreteImplementor)。同时,你也有一个 "Order" 类(抽象),这个类持有 "Payment" 的引用,并在处理订单时使用它。如果未来有新的支付方式出现,你只需要添加相应的实现类即可,无需修改 '订单处理' 的代码。
  3. 网络设备和协议:假设你正在为一个网络设备制造商工作,公司产品线包括路由器、交换机等,并且要求支持各种不同的网络协议(比如IP, IPv6, NetBIOS等)。在这种情况下,网络设备(抽象部分)和它们所使用的协议(实现部分)可以分别独立变化,那么桥接模式就非常适用。

优缺点

优点

  1. 解耦抽象和实现:最大的优点就是能够分离抽象部分和实现部分。这使得它们可以独立地进行改变或者扩展,而不会相互影响。
  2. 提高了可扩展性:你可以在两个独立的维度上对系统进行扩展,即抽象的维度和实现的维度。
  3. 实现细节对客户透明:由于桥接模式隐藏了实现细节,客户端并不需要关心这部分,从而使代码更加简洁易读。

缺点

  1. 增加了系统的复杂性:使用桥接模式会引入更多的类和对象,这会增加系统的复杂性。
  2. 理解和设计难度较高:桥接模式的使用需要理解其背后的原理,不然很容易设计出过度复杂的系统结构。因此,这个模式的使用门槛相对较高。

无法解决的问题

  1. 性能敏感的系统:由于桥接模式在抽象和实现之间添加了一个额外的间接层,它可能会引入一些运行时的开销。例如,在游戏开发中,你可能需要直接访问底层硬件以达到最佳性能,此时使用桥接模式可能会降低渲染速度或者响应时间。
  2. 系统抽象和实现稳定且不需要变更:如果你正在开发的系统的抽象部分和实现部分都很少或根本不需要改变,那么使用桥接模式可能导致过度设计。例如,如果你正在编写一个只需要支持单一数据库类型(如MySQL)的程序,使用桥接模式来准备支持其他类型的数据库可能就没必要。
  3. 大量简单对象的情况:对于包含大量简单对象的系统,采用桥接模式可能会增加不必要的复杂性。例如,对于一个电子表格应用程序,每个单元格可能仅需要存储一个数据值以及一些格式信息。这种情况下,使用桥接模式将数据值(抽象)与格式信息(实现)分离,可能会导致代码冗余且难以管理。
  4. 代码可读性和理解的问题:桥接模式涉及一定数量的类和对象,对于初学者来说,理解和掌握起来可能相对困难。例如,一个新手开发者在阅读使用了桥接模式的代码时,可能需要花费更多时间去理解抽象部分和实现部分之间的关系,增加了学习的难度。