桥接设计模式( 99%的面试官都不懂)

共 7950字,需浏览 16分钟

 ·

2022-05-25 07:45

定义

Bridge Design Pattern,将抽象部分与它的实现部分分离,使之任意删减,而无需受其它约束。

Decouple an abstraction from its implementation so that the two can vary independently,将抽象和实现解耦,让它们可以独立变化。

结构型模式。

结构

  • • Abstraction:定义抽象类的接口,维护一个指向Implementor类型对象的指针,将Client的请求转发给它的Implementor.RefinedAbstraction扩充由Abstraction定义的接口.定义了基于基本操作的较高层次的操作

  • • RefinedAbstraction:扩充由Abstraction定义的接口而得的 抽象类

  • • Implementor:定义实现类的接口.仅提供基本操作

  • • ConcreteImplementor:实现Implementor接口并定义它的具体实现

分析

理解桥接模式,重点需要理解如何将抽象化(Abstraction)与实现化(Implementation)脱耦,使得二者可以独立地变化。

抽象化:抽象化就是忽略一些信息,把不同的实体当作同样的实体对待。在面向对象中,将对象的共同性质抽取出来形成类的过程即为抽象化的过程。

实现化:针对抽象化给出的具体实现,就是实现化,抽象化与实现化是一对互逆的概念,实现化产生的对象比抽象化更具体,是对抽象化事物的进一步具体化的产物。

脱耦:脱耦就是将抽象化和实现化之间的耦合解脱开,或者说是将它们之间的强关联改换成弱关联,将两个角色之间的继承关系改为关联关系。桥接模式中的所谓脱耦,就是指在一个软件系统的抽象化和实现化之间使用关联关系(组合或者聚合)而不是继承,从而使两者可以相对独立地变化,这就是桥接模式的用意。

也可理解成:“一个类存在两个(或多个)独立变化的维度,通过组合,让这两个(或多个)维度可独立进行扩展。”组合替代继承,就能避免继承层次的指数级爆炸,即“组合优于继承”设计原则。

优点

  • • 使接口与实现各自独立

  • • 师接口实现类的扩展性大大增强

  • • 保护了部分实现内容,在扩展与变更内容时,无须重新编译原客户程序

  • • 桥接模式有时类似于多继承方案,但是多继承方案违背了类的单一职责原则(即一个类只有一个变化的原因),复用性比较差,而且多继承结构中类的个数非常庞大,桥接模式是比多继承方案更好的解决方法。

  • • 实现细节对客户透明,可以对用户隐藏实现细节。

缺点

  • • 增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程

  • • 要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性。

适用场景

基于此,以下情形可考虑此模式进行设计与实施

  • • 需要增强抽象与具体角色之间的灵活性,以避免两者之间的直接关联

  • • 实现类的变动,不影响客户端的使用

  • • 抽象接口与类的实现通过组合,均可在子类上进行扩展

案例

JDBC驱动

经典应用,利用JDBC驱动查询数据库:

Class.forName("com.mysql.jdbc.Driver");//加载及注册JDBC驱动程序
String url = "jdbc:mysql://localhost:3306/sample_db?user=root&password=your_password";
Connection con = DriverManager.getConnection(url);
Statement stmt = con.createStatement();
String query = "select * from test";
ResultSet rs=stmt.executeQuery(query);
while(rs.next()) {
  rs.getString(1);
  rs.getInt(2);
}

若想把MySQL换成Oracle,只需将第一行代码中的com.mysql.jdbc.Driver换成oracle.jdbc.driver.OracleDriver。还有更灵活实现方式,把需要加载的Driver类写到配置文件,程序启动时自动从配置文件加载,切换数据库时,就无需修改代码,仅修改配置文件然后发布即可。

不管改代码or配置,从一个DB切到另一种DB,都只需改动少代码或完全不需改代码,如此轻量级的数据库切换操作是如何实现的?

package com.mysql.jdbc;
import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

    /**
     * Construct a new driver and register it with DriverManager
     * @throws SQLException if a database error occurs.
     */

    public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}

执行**Class.forName(“com.mysql.jdbc.Driver”)**时,其实执行了:

  • • 要求JVM查找并加载指定Driver类

  • • 执行该类的静态代码,即:将MySQL Driver注册到DriverManager类

DriverManager干嘛的?把具体Driver实现类(如com.mysql.jdbc.Driver)注册到DriverManager后,后续所有对JDBC接口的调用,都会委派到具体Driver实现类执行。而Driver实现类都实现了相同接口(java.sql.Driver ),这也是灵活切换Driver原因。

public class DriverManager {
  private final static CopyOnWriteArrayList registeredDrivers = new CopyOnWriteArrayList();

  //...
  static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
  }
  //...

  public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException {
    if (driver != null) {
      registeredDrivers.addIfAbsent(new DriverInfo(driver));
    } else {
      throw new NullPointerException();
    }
  }

  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()));
  }
  //...
}

桥接模式的定义“将抽象和实现解耦,让它们可以独立变化”。JDBC案例里,啥是“抽象”?啥是“实现”?:

  • • JDBC本身就相当于“抽象”。这里的“抽象”并非“抽象类”或“接口”,而是和具体的数据库无关的、被抽象出来的一套“类库”

  • • 具体Driver(如com.mysql.jdbc.Driver)就是“实现”。这里的“实现”,并非指“接口实现类”,而是和具体DB相关的一套“类库”

JDBC、Driver分别独立开发,通过对象之间的组合关系协作。JDBC所有逻辑操作,最终都委托给具体Driver执行。

告警系统

API监控告警:根据不同告警规则,触发不同类型告警。告警支持多种通知渠道,包括:邮件、短信、微信、自动语音电话。通知紧急程度有多种类型,包括:SEVERE(严重)、URGENCY(紧急)、NORMAL(普通)、TRIVIAL(无关紧要)。不同的紧急程度对应不同的通知渠道。比如,SERVE(严重)级别的消息会通过“自动语音电话”告知责任人。

当时关于发送告警信息,只给出粗略设计,现在来实现。

最简单、最直接的实现

public enum NotificationEmergencyLevel {
  SEVERE, URGENCY, NORMAL, TRIVIAL
}

public class Notification {
  private List emailAddresses;
  private List telephones;
  private List wechatIds;

  public Notification() {}

  public void setEmailAddress(List emailAddress) {
    this.emailAddresses = emailAddress;
  }

  public void setTelephones(List telephones) {
    this.telephones = telephones;
  }

  public void setWechatIds(List wechatIds) {
    this.wechatIds = wechatIds;
  }

  public void notify(NotificationEmergencyLevel level, String message) {
    if (level.equals(NotificationEmergencyLevel.SEVERE)) {
      //...自动语音电话
    } else if (level.equals(NotificationEmergencyLevel.URGENCY)) {
      //...发微信
    } else if (level.equals(NotificationEmergencyLevel.NORMAL)) {
      //...发邮件
    } else if (level.equals(NotificationEmergencyLevel.TRIVIAL)) {
      //...发邮件
    }
  }
}

//在API监控告警的例子中,我们如下方式来使用Notification类:
public class ErrorAlertHandler extends AlertHandler {
  public ErrorAlertHandler(AlertRule rule, Notification notification){
    super(rule, notification);
  }


  @Override
  public void check(ApiStatInfo apiStatInfo) {
    if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi()).getMaxErrorCount()) {
      notification.notify(NotificationEmergencyLevel.SEVERE, "...");
    }
  }
}

Notification类存在大量if/else。若每个分支中的代码都不复杂,后期也没有无限膨胀的可能(增加更多if/else分支判断),那这样设计问题不大,没必要非得摒弃if/else。但Notification显然不是这样。其每个if/else代码逻辑都复杂,发送通知的所有逻辑堆在Notification类。一个类的代码越多,就越难读懂,越难修改,维护的成本也就越高。很多设计模式都是试图将庞大的类拆分成更细小的类,然后再通过某种更合理的结构组装在一起。

针对Notification,将不同渠道的发送逻辑剥离出,形成独立的消息发送类(MsgSender相关类):

  • • Notification类相当于抽象

  • • MsgSender类相当于实现

二者可独立开发,通过组合关系(即桥梁)任意组合。任意组合:不同紧急程度的消息和发送渠道之间的对应关系,不是在代码中固定写死,而是可动态指定(如通过读取配置获取对应关系)。

重构

public interface MsgSender {
  void send(String message);
}

public class TelephoneMsgSender implements MsgSender {
  private List telephones;

  public TelephoneMsgSender(List telephones) {
    this.telephones = telephones;
  }

  @Override
  public void send(String message) {
    //...
  }

}

public class EmailMsgSender implements MsgSender {
  // 与TelephoneMsgSender代码结构类似,所以省略...
}

public class WechatMsgSender implements MsgSender {
  // 与TelephoneMsgSender代码结构类似,所以省略...
}

public abstract class Notification {
  protected MsgSender msgSender;

  public Notification(MsgSender msgSender) {
    this.msgSender = msgSender;
  }

  public abstract void notify(String message);
}

public class SevereNotification extends Notification {
  public SevereNotification(MsgSender msgSender) {
    super(msgSender);
  }

  @Override
  public void notify(String message) {
    msgSender.send(message);
  }
}

public class UrgencyNotification extends Notification {
  // 与SevereNotification代码结构类似,所以省略...
}
public class NormalNotification extends Notification {
  // 与SevereNotification代码结构类似,所以省略...
}
public class TrivialNotification extends Notification {
  // 与SevereNotification代码结构类似,所以省略...
}

总结

对该模式有如下不同理解。在GoF的《设计模式》一书中,桥接模式被定义为:“将抽象和实现解耦,让它们可以独立变化。”在其他资料和书籍中,还有另外一种更加简单的理解方式:“一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。”:

  • • 第一种GoF的理解方式,弄懂定义中“抽象”和“实现”两个概念,是理解它的关键。定义中的“抽象”,指的并非“抽象类”或“接口”,而是被抽象出来的一套“类库”,它只包含骨架代码,真正的业务逻辑需要委派给定义中的“实现”来完成。而定义中的“实现”,也并非“接口的实现类”,而是一套独立的“类库”。“抽象”和“实现”独立开发,通过对象之间的组合关系,组装在一起

  • • 第二种理解方式,它非常类似我们之前讲过的“组合优于继承”设计原则,通过组合关系来替代继承关系,避免继承导致的指数级爆炸

桥接看着就像是面向接口编程这一原则的原旨:将实现与抽象分离。但这会让你疑惑:让两者独立变化的这部分说法。接口不应该保持稳定吗,为何要变化? 但你要注意,是多个维度独立变化。本文的告警案例,紧急度和警报方式是两个不同维度,可有不同组合方式。

这与slf4j日志门面设计有异曲同工之妙。slf4j有三个核心概念:

  • • logger这个日志记录器负责哪个类的日志

  • • appender日志打印到哪里

  • • encoder日志打印的格式

三个维度上可有不同实现,使用者可在每一维度上自定义多个实现,配置文件中将各个维度的某一实现组合在一起即可。logger相当于是桥接器,它的具类包含了appender和encoder的接口实例。

因此,桥接模式就是面向接口编程设计原则的集大成者。面向接口编程只是说在系统的某一功能上将接口和实现解耦,而桥接是详细的分析系统功能,将各独立的维度抽象出来,使用时按需组合。所以,若你单从面相接口编程的角度来看待问题,能轻易写出告警案例的第一种写法(仅抽象告警功能),但从桥接模式考虑才能写出第二种写法(将告警功能细分成严重程度、通知渠道两个维度,分别进行抽象,并在使用时进行自由组合)


欢迎加入后端交流群,关注本公众号添加我本人微信,邀请进群


最近在准备面试BAT,特地整理了一份面试资料,覆盖Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。在这里,我为大家准备了一份2021年最新最全的互联网大厂Java面试经验总结。

想获取史上最简单的Java大厂面试题学习资料
关注如下公众号,后台回复面试」即可白嫖!

往期推荐



精心整理Java大厂面试必读书籍(强烈推荐)

你真的思考过自己写的代码为啥这么垃圾吗?

我是如何一步步让公司的MySQL支撑亿级流量的?

凭借对Condition接口的理解,拿到25K的offer!


浏览 26
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐