【Java设计模式 设计模式与范式】结构型模式 四:桥接模式

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 【Java设计模式 设计模式与范式】结构型模式 四:桥接模式

本篇Blog继续学习结构型模式,了解如何更优雅的布局类和对象。结构型模式描述如何将类或对象按某种布局组合以便获得更好、更灵活的结构。虽然面向对象的继承机制提供了最基本的子类扩展父类的功能,但结构型模式不仅仅简单地使用继承,而更多地通过组合与运行期的动态组合来实现更灵活的功能。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。本篇学习的是桥接模式。由于学习的都是设计模式,所有系列文章都遵循如下的目录:

  • 模式档案:包含模式的定义、模式的特点、解决什么问题、优缺点、使用场景等
  • 模式结构:包含模式的角色定义及调用关系以及其模版代码
  • 模式示例:包含模式的实现方式代码举例,生活中的简单问题映射
  • 模式实践:如果工作中或开源项目用到了该模式,就将使用过程贴到这里,并且客观讨论使用的是否恰当
  • 模式对比:如果模式相似,有必要体现其相似点及不同点,区分使用,说明哪些场景下使用哪种模式比较好
  • 模式扩展:如果模式有与标准结构定义不同的变体形式,一并体现出其变体结构

接下来所有设计模式的介绍都暂且遵循此基本行文逻辑吗,如果某一条目没有则无需体现,但条目顺序遵循此结构

模式档案

在现实生活中,某些类具有两个或多个维度的变化,如图形既可按形状分,又可按颜色分。如何设计类似于 Photoshop 这样的软件,能画不同形状和不同颜色的图形呢?如果用继承方式,m 种形状和 n 种颜色的图形就有 m×n 种,不但对应的子类很多,而且扩展困难。当然,这样的例子还有很多,如不同颜色和字体的文字、不同品牌和功率的汽车等。如果用桥接模式就能很好地解决这些问题

模式定义将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度

解决什么问题:一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。通过组合关系来替代继承关系,避免继承层次的指数级爆炸

优点:抽象与实现分离,扩展能力强;符合开闭原则;符合合成复用原则;其实现细节对客户透明

缺点:由于聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,能正确地识别出系统中两个独立变化的维度,这增加了系统的理解与设计难度

使用场景:当一个类内部具备两种或多种变化维度时,使用桥接模式可以解耦这些变化的维度,使高层代码架构稳定

  • 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。
  • 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
  • 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时

桥接模式的一个常见使用场景就是替换继承。我们知道,继承拥有很多优点,比如,抽象、封装、多态等,父类封装共性,子类实现特性。继承可以很好的实现代码复用(封装)的功能,但这也是继承的一大缺点。因为父类拥有的方法,子类也会继承得到,无论子类需不需要,这说明继承具备强侵入性(父类代码侵入子类),同时会导致子类臃肿

模式结构

桥接(Bridge)模式包含以下四种主要角色。

  • 抽象化(Abstraction)角色:定义抽象类,并包含一个对实现化对象的引用。
  • 扩展抽象化(Refined Abstraction)角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
  • 实现化(Implementor)角色:定义实现化角色的接口,供扩展抽象化角色调用。
  • 具体实现化(Concrete Implementor)角色:给出实现化角色接口的具体实现

整体结构如下:

模式实现

依据桥接模式的关系图定义四类角色代码如下:

实现化角色

//实现化角色
interface Implementor {
    public void OperationImpl();
}

具体实现化角色

//具体实现化角色
class ConcreteImplementorA implements Implementor {
    public void OperationImpl() {
        System.out.println("具体实现化(Concrete Implementor)角色被访问");
    }
}

抽象化角色

//抽象化角色
abstract class Abstraction {
    protected Implementor imple;
    protected Abstraction(Implementor imple) {
        this.imple = imple;
    }
    public abstract void Operation();
}

扩展抽象化角色

//扩展抽象化角色
class RefinedAbstraction extends Abstraction {
    protected RefinedAbstraction(Implementor imple) {
        super(imple);
    }
    public void Operation() {
        System.out.println("扩展抽象化(Refined Abstraction)角色被访问");
        imple.OperationImpl();
    }
}

客户端调用如下:

public class BridgeTest {
    public static void main(String[] args) {
        Implementor imple = new ConcreteImplementorA();
        Abstraction abs = new RefinedAbstraction(imple);
        abs.Operation();
    }
}

打印结果如下:

扩展抽象化(Refined Abstraction)角色被访问
具体实现化(Concrete Implementor)角色被访问

模式实践

我们来看两个实践的例子:JDBC源码分析以及消息系统构建

JDBC源码分析

桥接模式在开源框架实现中有个典型的应用就是JDBC,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的实现

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()
  }
}

结合 com.mysql.jdbc.Driver 的代码实现,我们可以发现,当执行 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<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<DriverInfo>();
  //...
  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 本身就相当于抽象。注意,这里所说的抽象,指的并非抽象类或接口,而是跟具体的数据库无关的、被抽象出来的一套“类库”。具体的 Driver(比如,com.mysql.jdbc.Driver)就相当于实现。注意,这里所说的实现,也并非指接口的实现类,而是跟具体数据库相关的一套类库,JDBC 和 Driver 独立开发,通过对象之间的组合关系,组装在一起。JDBC 的所有逻辑操作,最终都委托给 Driver 来执行,例如prepare方法就是获取到连接后执行。

protected PreparedStatement prepare() throws SQLException {
        this.conn = this.connect();
        try {
            Map var1 = this.getTypeMap();
            if (var1 != null) {
                this.conn.setTypeMap(var1);
            }
            this.ps = this.conn.prepareStatement(this.getCommand(), 1004, 1008);
        } catch (SQLException var2) {
            System.err.println(this.resBundle.handleGetObject("jdbcrowsetimpl.prepare").toString() + var2.getLocalizedMessage());
            if (this.ps != null) {
                this.ps.close();
            }
            if (this.conn != null) {
                this.conn.close();
            }
            throw new SQLException(var2.getMessage());
        }
        return this.ps;
    }

而该连接就是通过driver获取的。

public interface Driver {
    Connection connect(String url, java.util.Properties info)
        throws SQLException;
    //...
}

设计一个可扩展的监控告警方案

我们想设计一套监控告警方案:根据不同的告警规则,触发不同类型的告警。

  • 告警支持多种通知渠道,包括:邮件、短信、微信、自动语音电话。
  • 告警通知的紧急程度有多种类型,包括:SEVERE(严重)、URGENCY(紧急)、NORMAL(普通)、TRIVIAL(无关紧要)。

不同的紧急程度对应不同的通知渠道。比如,SERVE(严重)级别的消息会通过“自动语音电话”告知相关人员,我们先来看最简单、最直接的一种实现方式

public enum NotificationEmergencyLevel {
  SEVERE, URGENCY, NORMAL, TRIVIAL
}
public class Notification {
  private List<String> emailAddresses;
  private List<String> telephones;
  private List<String> wechatIds;
  public Notification() {}
  public void setEmailAddress(List<String> emailAddress) {
    this.emailAddresses = emailAddress;
  }
  public void setTelephones(List<String> telephones) {
    this.telephones = telephones;
  }
  public void setWechatIds(List<String> 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<String> telephones;
  public TelephoneMsgSender(List<String> 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代码结构类似,所以省略...
}

这样当我们有了新的紧急程度或者通知方式,都可以直接扩展一个通知类或消息发送类即可,不需要修改任何代码。

模式对比

这里我们对比下已经学习过的四种结构型设计模式:代理、桥接、装饰器、适配器

代理、桥接、装饰器、适配器四种设计模式

代理、桥接、装饰器、适配器,这 4 种模式是比较常用的结构型设计模式。它们的代码结构非常相似。笼统来说,它们都可以称为 Wrapper 模式,也就是通过 Wrapper 类二次封装原始类。尽管代码结构相似,但这 4 种设计模式的用意完全不同,也就是说要解决的问题、应用场景不同,这也是它们的主要区别

  • 代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。
  • 桥接模式:桥接模式的目的是将抽象部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。
  • 装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。
  • 适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口

其实我觉得从代码结构上去区分这四种结构型模式非常容易搞混,也大可不必,因为它们的结构非常相似,反而是从要解决的问题、应用场景出发,首先对问题有宏观认知,然后再去思考这个场景适配哪种模式,然后再去关心代码结构是个比较不错的思考方式。

总结一下

其实我觉得从代码结构上去区分代理、桥接、装饰器、适配器这四种结构型模式非常容易搞混(甚至退化的装饰器结构能和代理模式玩去一样)也大可不必,因为它们的结构非常相似,反而从要解决的问题、应用场景角度出发,先对问题有个宏观认知,然后再去思考这个场景适配哪种模式,最后再去关心代码结构应该怎么写,才是个比较不错的思考方式。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
22天前
|
设计模式 Java 程序员
[Java]23种设计模式
本文介绍了设计模式的概念及其七大原则,强调了设计模式在提高代码重用性、可读性、可扩展性和可靠性方面的作用。文章还简要概述了23种设计模式,并提供了进一步学习的资源链接。
36 0
[Java]23种设计模式
|
1月前
|
设计模式 PHP 开发者
PHP中的设计模式:桥接模式的解析与应用
在软件开发的浩瀚海洋中,设计模式如同灯塔一般,为开发者们指引方向。本文将深入探讨PHP中的一种重要设计模式——桥接模式。桥接模式巧妙地将抽象与实现分离,通过封装一个抽象的接口,使得实现和抽象可以独立变化。本文将阐述桥接模式的定义、结构、优缺点及其应用场景,并通过具体的PHP示例代码展示如何在实际项目中灵活运用这一设计模式。让我们一起走进桥接模式的世界,感受它的魅力所在。
|
6天前
|
设计模式 JavaScript Java
Java设计模式:建造者模式详解
建造者模式是一种创建型设计模式,通过将复杂对象的构建过程与表示分离,使得相同的构建过程可以创建不同的表示。本文详细介绍了建造者模式的原理、背景、应用场景及实际Demo,帮助读者更好地理解和应用这一模式。
|
1月前
|
设计模式 监控 算法
Java设计模式梳理:行为型模式(策略,观察者等)
本文详细介绍了Java设计模式中的行为型模式,包括策略模式、观察者模式、责任链模式、模板方法模式和状态模式。通过具体示例代码,深入浅出地讲解了每种模式的应用场景与实现方式。例如,策略模式通过定义一系列算法让客户端在运行时选择所需算法;观察者模式则让多个观察者对象同时监听某一个主题对象,实现松耦合的消息传递机制。此外,还探讨了这些模式与实际开发中的联系,帮助读者更好地理解和应用设计模式,提升代码质量。
Java设计模式梳理:行为型模式(策略,观察者等)
|
1月前
|
设计模式 Java
Java设计模式
Java设计模式
28 0
|
1月前
|
设计模式 Java
Java设计模式之外观模式
这篇文章详细解释了Java设计模式之外观模式的原理及其应用场景,并通过具体代码示例展示了如何通过外观模式简化子系统的使用。
29 0
|
1月前
|
设计模式 Java
Java设计模式之桥接模式
这篇文章介绍了Java设计模式中的桥接模式,包括桥接模式的目的、实现方式,并通过具体代码示例展示了如何分离抽象与实现,使得两者可以独立变化。
43 0
|
14天前
|
设计模式 安全 Java
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
|
2月前
|
设计模式 数据库连接 PHP
PHP中的设计模式:提升代码的可维护性与扩展性在软件开发过程中,设计模式是开发者们经常用到的工具之一。它们提供了经过验证的解决方案,可以帮助我们解决常见的软件设计问题。本文将介绍PHP中常用的设计模式,以及如何利用这些模式来提高代码的可维护性和扩展性。我们将从基础的设计模式入手,逐步深入到更复杂的应用场景。通过实际案例分析,读者可以更好地理解如何在PHP开发中应用这些设计模式,从而写出更加高效、灵活和易于维护的代码。
本文探讨了PHP中常用的设计模式及其在实际项目中的应用。内容涵盖设计模式的基本概念、分类和具体使用场景,重点介绍了单例模式、工厂模式和观察者模式等常见模式。通过具体的代码示例,展示了如何在PHP项目中有效利用设计模式来提升代码的可维护性和扩展性。文章还讨论了设计模式的选择原则和注意事项,帮助开发者在不同情境下做出最佳决策。
|
17天前
|
设计模式 开发者 Python
Python编程中的设计模式:工厂方法模式###
本文深入浅出地探讨了Python编程中的一种重要设计模式——工厂方法模式。通过具体案例和代码示例,我们将了解工厂方法模式的定义、应用场景、实现步骤以及其优势与潜在缺点。无论你是Python新手还是有经验的开发者,都能从本文中获得关于如何在实际项目中有效应用工厂方法模式的启发。 ###