结构型设计模式之桥接模式

概述

设计模式是针对软件开发中经常遇到的一些设计问题,总结出来的一套解决方案或者设计思路。

大部分设计模式要解决的都是代码的可扩展性问题。

对于灵活多变的业务,需要用到设计模式,提升扩展性和可维护性,让代码能适应更多的变化;

设计模式的核心就是,封装变化,隔离可变性


设计模式解决的问题:

  • 创建型设计模式主要解决“对象的创建”问题,创建和使用代码解耦;
  • 结构型设计模式主要解决“类或对象的组合或组装”问题,将不同功能代码解耦;
  • 行为型设计模式主要解决的就是“类或对象之间的交互”问题。将不同的行为代码解耦,具体到观察者模式,它是将观察者和被观察者代码解耦。

设计模式关注重点: 了解它们都能解决哪些问题,掌握典型的应用场景,并且懂得不过度应用。

经典的设计模式有 23 种。随着编程语言的演进,一些设计模式(比如 Singleton)也随之过时,甚至成了反模式,一些则被内置在编程语言中(比如 Iterator),另外还有一些新的模式诞生(比如 Monostate)。

创建型模式主要解决类或对象的组合或组装问题,是从程序的结构上实现松耦合,从而可以扩大整体的类结构,用来解决更大的问题。

结构型设计模式是一组用于解决对象和类之间的组织关系、复杂性和交互问题的设计模式。它们主要关注如何通过类和对象的组合来形成更大的结构,并提供了灵活的方式来实现系统的组件之间的通信和协作。结构型设计模式主要解决以下问题:

  1. 对象之间的接口和实现分离:结构型设计模式可以帮助将抽象与实现分离,使得对象之间的接口更加清晰和可扩展。例如,适配器模式可以将不兼容的接口转换为统一的接口,使得不同类之间的交互更加简单。
  2. 类之间的关系管理:结构型设计模式提供了一种有效的方式来管理类之间的关系,以确保系统的灵活性和可维护性。例如,装饰器模式允许在运行时动态地为对象添加功能,而不必修改原始类的结构。
  3. 对象的组合和组件化:结构型设计模式通过对象的组合和组件化,能够更好地处理系统中的复杂性。例如,组合模式允许将对象组织成树状结构,形成部分-整体的层次结构,以便更容易地处理对象集合。
  4. 对象的访问和控制:结构型设计模式提供了一些机制来控制对象的访问和可见性,以及限制对象之间的依赖关系。例如,外观模式可以封装一组复杂子系统的接口,提供简化的接口给客户端使用。
  5. 类和对象的灵活性:结构型设计模式通过将类和对象组合起来,提供了更大的灵活性和可扩展性。例如,桥接模式可以将抽象与实现解耦,使得它们可以独立地变化和演化。

结构型设计模式主要关注对象和类之间的组织关系、复杂性和交互问题。它们通过提供灵活的方式来管理对象之间的接口、关系和行为,帮助我们构建更健壮、灵活和可扩展的软件系统。

  1. 代理模式: 为真实对象提供一个代理,从而控制对真实对象的访问 。
  2. 适配模式 :使原本由于接口不兼容不能一起工作的类可以一起工作 。
  3. 桥接模式 :处理多层继承结构,处理多维度变化的场景,将各个维度设计成独立的继 承结构,使各个维度可以独立的扩展在抽象层建立关联。
  4. 组合模式 :将对象组合成树状结构以表示”部分和整体”层次结构,使得客户可以统一 的调用叶子对象和容器对象 。
  5. 装饰模式 :动态地给一个对象添加额外的功能,比继承灵活 。
  6. 外观模式 :为子系统提供统一的调用接口,使得子系统更加容易使用 。
  7. 享元模式 :运用共享技术有效的实现管理大量细粒度对象,节省内存,提高效率。

结构型

常用的有:代理模式、桥接模式、装饰者模式、适配器模式。

不常用的有:门面模式、组合模式、享元模式。

代理模式是解耦功能和非功能代码的类组合,代理类和被代理类实现或继承共同的父类;

桥接模式是从不同的纬度独立发展有各自不同的父类抽象,他们可以相互组合实现复杂关系的实现;

装饰器是对功能的增强,装饰器类和原始类有这共同的父类;

实际上,符合“组合关系”这种代码结构的设计模式有很多,比如之前讲过的代理模式、桥接模式,还有现在的装饰器模式。尽管它们的代码结构很相似,但是每种设计模式的意图是不同的。就拿比较相似的代理模式和装饰器模式来说吧,代理模式中,代理类附加的是跟原始类无关的功能,而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能。

代理、桥接、装饰器、适配器 4 种设计模式的区别:

代理、桥接、装饰器、适配器,这 4 种模式是比较常用的结构型设计模式。它们的代码结构非常相似。笼统来说,它们都可以称为 Wrapper 模式,也就是通过 Wrapper 类二次封装原始类。

尽管代码结构相似,但这 4 种设计模式的用意完全不同,也就是说要解决的问题、应用场景不同,这也是它们的主要区别。这里我就简单说一下它们之间的区别。

  • 代理模式:

    代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。

  • 桥接模式:

    桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。

  • 装饰器模式:

    装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。

  • 适配器模式:

    适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。

适配器是做接口转换,解决的是原接口和目标接口不匹配的问题。

门面模式做接口整合,解决的是多接口调用带来的问题。

结构型设计模式核心就是代码的组合来达到最大的扩展,不同模式根据解决问题不同,实现方式不同叫不同名字,归根结底都是在一个类中注入另一个类进行组合

代理模式。它在不改变原始类(或者叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。代理模式在平时的开发经常被用到,常用在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志。

桥接模式:

桥接模式的代码实现非常简单,但是理解起来稍微有点难度,并且应用场景也比较局限,所以,相对于代理模式来说,桥接模式在实际的项目中并没有那么常用,你只需要简单了解,见到能认识就可以。

桥接模式有两种理解方式。第一种理解方式是“将抽象和实现解耦,让它们能独立开发”。这种理解方式比较特别,应用场景也不多。另一种理解方式更加简单,类似“组合优于继承”设计原则,这种理解方式更加通用,应用场景比较多。不管是哪种理解方式,它们的代码结构都是相同的,都是一种类之间的组合关系。

将两种纬度独自发展不同的抽象,然后进行组合

定义:

将抽象和实现解耦,让它们可以独立变化。

一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。 通过组合关系来替代继承关系,避免继承层次的指数级爆炸。这种理解方式非常类似于,我们之前讲过的“组合优于继承”设计原则;

桥接模式有两种理解方式:

  • 第一种理解方式是“将抽象和实现解耦,让它们能独立开发”。这种理解方式比较特别,应用场景也不多。
  • 另一种理解方式更加简单,类似“组合优于继承”设计原则,这种理解方式更加通用,应用场景比较多。不管是哪种理解方式,它们的代码结构都是相同的,都是一种类之间的组合关系。

JDBC 驱动是桥接模式的经典应用:

JDBC 驱动来查询数据库代码:

//Class.forName() 官方文档解释:通俗点说就是要求JVM查找并加载指定的类,也就是说JVM会执行该类的静态代码段,并返回与该类相关的Class对象。
//静态代码块,随着类的加载而加载,并且只执行一次,常用来执行类的初始化
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 类写到配置文件中,当程序启动的时候,自动从配置文件中加载,这样在切换数据库的时候,我们都不需要修改代码,只需要修改配置文件就可以了。

不管是改代码还是改配置,在项目中,从一个数据库切换到另一种数据库,都只需要改动很少的代码,或者完全不需要改动代码,那如此优雅的数据库切换是如何实现的呢?

先从 com.mysql.jdbc.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 这个例子中,什么是“抽象”?什么是“实现”呢?

实际上,JDBC 本身就相当于“抽象”。注意,这里所说的“抽象”,指的并非“抽象类”或“接口”,而是跟具体的数据库无关的、被抽象出来的一套“类库”。具体的 Driver(比如,com.mysql.jdbc.Driver)就相当于“实现”。注意,这里所说的“实现”,也并非指“接口的实现类”,而是跟具体数据库相关的一套“类库”。

JDBC 和 Driver 独立开发,通过对象之间的组合关系,组装在一起。JDBC 的所有逻辑操作,最终都委托给 Driver 来执行。

image.png

应用举例:

一个 API 接口监控告警的例子:根据不同的告警规则,触发不同类型的告警。告警支持多种通知渠道,包括:邮件、短信、微信、自动语音电话。通知的紧急程度有多种类型,包括: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代码结构类似,所以省略...
}

在这个设计中,我们定义了MsgSender接口及其具体实现类(如TelephoneMsgSenderEmailMsgSenderWechatMsgSender)来表示不同的消息发送方式。然后,我们定义了抽象类Notification作为桥接模式的桥梁,它持有一个MsgSender对象,并提供了抽象方法notify()用于通知。

各种紧急级别的通知类(如SevereNotificationUrgencyNotification等)继承自Notification,并实现了notify()方法。在具体的通知类中,通过调用msgSender.send(message)来实现消息的发送。

这样,通过使用桥接模式,我们将通知类和消息发送方式解耦,使得它们可以独立变化。在运行时,我们可以根据需要选择不同的消息发送方式并创建对应的通知类,从而实现灵活的消息通知。

使用桥接模式设计复杂的消息系统:

举个例子,我们在平时办公的时候经常通过邮件消息、短信消息或者系统内消息与同事进行沟通。尤其在走一些审批流程的时候,我们需要记录这些过程以备查。根据类型来划分,消息可以分为邮件消息、短信消息和系统内消息。但是,根据紧急程度来划分,消息可以分为普通消息、加急消息和特急消息。显然,整个消息系统可以划分为两个维度,如下图所示。

如果我们用继承,则情况就复杂了,而且也不利于扩展。邮件消息可以是普通的,也可以是加急的;短信消息可以是普通的,也可以是加急的。下面我们用桥接模式来解决这个问题。 首先创建一个IMessage接口担任桥接的角色。

/**



 * 实现消息发送的统一接口
 */



public interface IMessage {
    //要发送的消息的内容和接收人
    void send(String message, String toUser);
}

创建邮件消息实现EmailMessage类、创建短信消息实现SmsMessage类

/**



 * 邮件消息的实现类
 */



public class EmailMessage implements IMessage {
    public void send(String message, String toUser) {
        System.out.println("使用邮件消息发送" + message + "给" + toUser);
    }
}





/**
 * 短信消息的实现类
 * SMS(Short IMessage Service)短信消息服务
 */
public class SmsMessage implements IMessage {
    public void send(String message, String toUser) {
        System.out.println("使用短信消息发送" + message + "给" + toUser);
    }
}

然后创建桥接抽象角色AbstractMessage类。

/**



 * 抽象消息类
 */



public abstract class AbstractMessage {
    //持有一个实现部分的对象
    IMessage message;


    //构造方法,传入实现部分的对象
    public AbstractMessage(IMessage message) {
        this.message = message;
    }


    //发送消息,委派给实现部分的方法
    public void sendMessage(String message, String toUser) {
        this.message.send(message, toUser);
    }

}

创建具体实现普通消息NomalMessage类、创建具体实现加急消息UrgencyMessage类

/**



 * 普通消息类
 */



public class NomalMessage extends AbstractMessage {

    //构造方法,传入实现部分的对象
    public NomalMessage(IMessage message) {
        super(message);
    }



    @Override
    public void sendMessage(String message, String toUser) {
        //对于普通消息,直接调用父类方法发送消息即可
        super.sendMessage(message, toUser);
    }
}




/**
 * 加急消息类
 */
public class UrgencyMessage extends AbstractMessage {


    //构造方法
    public UrgencyMessage(IMessage message) {
        super(message);
    }

    @Override
    public void sendMessage(String message, String toUser) {
        message = "加急:" + message;
        super.sendMessage(message, toUser);
    }


    //扩展它功能,监控某个消息的处理状态
    public Object watch(String messageId) {
        //根据给出的消息编码(messageId)查询消息的处理状态
        //组织成监控的处理状态,然后返回
        return null;
    }
}

最后编写客户端测试代码


 public static void main(String[] args) {
        IMessage message = new SmsMessage();
        AbstractMessage abstractMessage = new NomalMessage(message);
        abstractMessage.sendMessage("加班申请速批", "王总");

        message = new EmailMessage();
        abstractMessage = new UrgencyMessage(message);
        abstractMessage.sendMessage("加班申请速批", "王总");
}

运行结果如下图所示。

在上面的案例中,我们采用桥接模式解耦了“消息类型”和“消息紧急程度”这两个独立变化的维度。后续如果有更多的消息类型,比如微信、钉钉等,则直接新建一个类继承IMessage即可;如果紧急程度需要新增,则同样只需新建一个类实现AbstractMessage类即可。

参考:使用桥接模式设计复杂的消息系统 – 掘金 (juejin.cn)

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYdtqO5e' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片